1   /*
2    *  SerialDataStore.java
3    *
4    *  Copyright (c) 1998-2005, The University of Sheffield.
5    *
6    *  This file is part of GATE (see http://gate.ac.uk/), and is free
7    *  software, licenced under the GNU Library General Public License,
8    *  Version 2, June 1991 (in the distribution as file licence.html,
9    *  and also available at http://gate.ac.uk/gate/licence.html).
10   *
11   *  Hamish Cunningham, 19/Jan/2001
12   *
13   *  $Id: SerialDataStore.java,v 1.50 2005/11/10 14:37:32 ian_roberts Exp $
14   */
15  
16  package gate.persist;
17  
18  import java.io.*;
19  import java.net.MalformedURLException;
20  import java.net.URL;
21  import java.util.*;
22  import java.util.zip.GZIPInputStream;
23  import java.util.zip.GZIPOutputStream;
24  
25  import gate.*;
26  import gate.corpora.SerialCorpusImpl;
27  import gate.creole.ResourceData;
28  import gate.event.DatastoreEvent;
29  import gate.event.DatastoreListener;
30  import gate.security.*;
31  import gate.security.SecurityException;
32  import gate.util.*;
33  
34  /**
35   * A data store based on Java serialisation.
36   */
37  public class SerialDataStore
38  extends AbstractFeatureBearer implements DataStore {
39  
40    /** Debug flag */
41    private static final boolean DEBUG = false;
42  
43    /** The name of the datastore */
44    protected String name;
45  
46    /**
47     * Construction requires a file protocol URL
48     * pointing to the storage directory used for
49     * the serialised classes. <B>NOTE:</B> should not be called except by
50     * GATE code.
51     */
52    public SerialDataStore(String storageDirUrl) throws PersistenceException {
53      setStorageUrl(storageDirUrl);
54    } // construction from URL
55  
56    /**
57     * Default construction. <B>NOTE:</B> should not be called except by
58     * GATE code.
59     */
60    public SerialDataStore() { };
61  
62    /**
63     * The directory used for the serialised classes.
64     */
65    protected File storageDir;
66  
67    /** Set method for storage URL */
68    public void setStorageDir(File storageDir) { this.storageDir = storageDir; }
69  
70    /** Get method for storage URL */
71    public File getStorageDir() { return storageDir; }
72  
73    /** Set the URL for the underlying storage mechanism. */
74    public void setStorageUrl(String urlString) throws PersistenceException {
75      URL storageUrl = null;
76      try {
77       storageUrl  = new URL(urlString);
78      } catch (java.net.MalformedURLException ex) {
79        throw new PersistenceException(
80          "The URL passed is not correct: " + urlString
81        );
82      }
83      if(! storageUrl.getProtocol().equalsIgnoreCase("file"))
84        throw new PersistenceException(
85          "A serial data store needs a file URL, not " + storageUrl
86        );
87      this.storageDir = new File(storageUrl.getFile());
88    } // setStorageUrl
89  
90    /** Get the URL for the underlying storage mechanism. */
91    public String getStorageUrl() {
92      if(storageDir == null) return null;
93  
94      URL u = null;
95      try { u = storageDir.toURL(); } catch(MalformedURLException e) {
96        // we can assume that this never happens as storageUrl should always
97        // be a valid file and therefore convertable to URL
98      }
99  
100     return u.toString();
101   } // getStorageUrl()
102 
103   /** Create a new data store. This tries to create a directory in
104     * the local file system. If the directory already exists and is
105     * non-empty, or is
106     * a file, or cannot be created, PersistenceException is thrown.
107     */
108   public void create()
109   throws PersistenceException {
110     if(storageDir == null)
111       throw new PersistenceException("null storage directory: cannot create");
112 
113     if(! storageDir.exists()) { // if doesn't exist create it
114       if(! storageDir.mkdir())
115         throw new
116           PersistenceException("cannot create directory " + storageDir);
117     } else { // must be empty
118       String[] existingFiles = storageDir.list();
119       if(! (existingFiles == null || existingFiles.length == 0) )
120         throw new PersistenceException(
121           "directory "+ storageDir +" is not empty: cannot use for data store"
122         );
123     }
124 
125     // dump the version file
126     try {
127       File versionFile = getVersionFile();
128       OutputStreamWriter osw = new OutputStreamWriter(
129         new FileOutputStream(versionFile)
130       );
131       osw.write(versionNumber + Strings.getNl());
132       osw.close();
133     } catch(IOException e) {
134       throw new PersistenceException("couldn't write version file: " + e);
135     }
136   } // create()
137 
138   /** The name of the version file */
139   protected static String versionFileName = "__GATE_SerialDataStore__";
140 
141   /** The protocol version of the currently open data store */
142   protected static String currentProtocolVersion = null;
143 
144   /** Get a File for the protocol version file. */
145   protected File getVersionFile() throws IOException {
146     return new File(storageDir, versionFileName);
147   } // getVersionFile
148 
149   /**
150    * Version number for variations in the storage protocol.
151    * Protocol versions:
152    * <UL>
153    * <LI>
154    * 1.0: uncompressed. Originally had no version file - to read a 1.0
155    * SerialDataStore that has no version file add a version file containing
156    * the line "1.0".
157    * <LI>
158    * 1.1: has a version file. Uses GZIP compression.
159    * </UL>
160    * This variable stores the version of the current level of the
161    * protocol, NOT the level in use in the currently open data store.
162    */
163   protected String versionNumber = "1.1";
164 
165   /** List of valid protocol version numbers. */
166   protected String[] protocolVersionNumbers = {
167     "1.0",
168     "1.1"
169   }; // protocolVersionNumbers
170 
171   /** Check a version number for validity. */
172   protected boolean isValidProtocolVersion(String versionNumber) {
173     if(versionNumber == null)
174       return false;
175 
176     for(int i = 0; i < protocolVersionNumbers.length; i++)
177       if(protocolVersionNumbers[i].equals(versionNumber))
178         return true;
179 
180     return false;
181   } // isValidProtocolVersion
182 
183   /** Delete the data store.
184     */
185   public void delete() throws PersistenceException {
186     if(storageDir == null || ! Files.rmdir(storageDir))
187       throw new PersistenceException("couldn't delete " + storageDir);
188 
189     Gate.getDataStoreRegister().remove(this);
190   } // delete()
191 
192   /** Delete a resource from the data store.
193     */
194   public void delete(String lrClassName, Object lrPersistenceId)
195   throws PersistenceException {
196 
197     // find the subdirectory for resources of this type
198     File resourceTypeDirectory = new File(storageDir, lrClassName);
199     if(
200       (! resourceTypeDirectory.exists()) ||
201       (! resourceTypeDirectory.isDirectory())
202     ) {
203       throw new PersistenceException("Can't find " + resourceTypeDirectory);
204     }
205 
206     // create a File to representing the resource storage file
207     File resourceFile = new File(resourceTypeDirectory, (String)lrPersistenceId);
208     if(! resourceFile.exists() || ! resourceFile.isFile())
209       throw new PersistenceException("Can't find file " + resourceFile);
210 
211     // delete the beast
212     if(! resourceFile.delete())
213       throw new PersistenceException("Can't delete file " + resourceFile);
214 
215     // if there are no more resources of this type, delete the dir too
216     if(resourceTypeDirectory.list().length == 0)
217       if(! resourceTypeDirectory.delete())
218         throw new PersistenceException("Can't delete " + resourceTypeDirectory);
219 
220     //let the world know about it
221     fireResourceDeleted(
222       new DatastoreEvent(
223         this, DatastoreEvent.RESOURCE_DELETED, null, (String) lrPersistenceId
224       )
225     );
226   } // delete(lr)
227 
228   /** Adopt a resource for persistence. */
229   public LanguageResource adopt(LanguageResource lr,SecurityInfo secInfo)
230   throws PersistenceException,gate.security.SecurityException {
231 
232     //ignore security info
233 
234     // check the LR's current DS
235     DataStore currentDS = lr.getDataStore();
236     if(currentDS == null) {  // an orphan - do the adoption
237       LanguageResource res = lr;
238 
239       if (lr instanceof Corpus) {
240         FeatureMap features1 = Factory.newFeatureMap();
241         features1.put("transientSource", lr);
242         try {
243           //here we create the persistent LR via Factory, so it's registered
244           //in GATE
245           res = (LanguageResource)
246             Factory.createResource("gate.corpora.SerialCorpusImpl", features1);
247           //Here the transient corpus is not deleted from the CRI, because
248           //this might not always be the desired behaviour
249           //since we chose that it is for the GUI, this functionality is
250           //now move to the 'Save to' action code in NameBearerHandle
251         } catch (gate.creole.ResourceInstantiationException ex) {
252           throw new GateRuntimeException(ex.getMessage());
253         }
254 
255       }
256 
257       res.setDataStore(this);
258 
259       // let the world know
260       fireResourceAdopted(
261           new DatastoreEvent(this, DatastoreEvent.RESOURCE_ADOPTED, lr, null)
262       );
263       return res;
264     } else if(currentDS.equals(this))         // adopted already here
265       return lr;
266     else {                      // someone else's child
267       throw new PersistenceException(
268         "Can't adopt a resource which is already in a different datastore"
269       );
270     }
271 
272 
273   } // adopt(LR)
274 
275   /** Open a connection to the data store. */
276   public void open() throws PersistenceException {
277     if(storageDir == null)
278       throw new PersistenceException("Can't open: storage dir is null");
279 
280     // check storage directory is readable
281     if(! storageDir.canRead()) {
282       throw new PersistenceException("Can't read " + storageDir);
283     }
284 
285     // check storage directory is a valid serial datastore
286 // if we want to support old style:
287 // String versionInVersionFile = "1.0";
288 // (but this means it will open *any* directory)
289     try {
290       FileReader fis = new FileReader(getVersionFile());
291       BufferedReader isr = new BufferedReader(fis);
292       currentProtocolVersion = isr.readLine();
293       if(DEBUG) Out.prln("opening SDS version " + currentProtocolVersion);
294       isr.close();
295     } catch(IOException e) {
296       throw new PersistenceException(
297         "Invalid storage directory: " + e
298       );
299     }
300     if(! isValidProtocolVersion(currentProtocolVersion))
301       throw new PersistenceException(
302         "Invalid protocol version number: " + currentProtocolVersion
303       );
304 
305   } // open()
306 
307   /** Close the data store. */
308   public void close() throws PersistenceException {
309     Gate.getDataStoreRegister().remove(this);
310   } // close()
311 
312   /** Save: synchonise the in-memory image of the LR with the persistent
313     * image.
314     */
315   public void sync(LanguageResource lr) throws PersistenceException {
316 //    Out.prln("SDS: LR sync called. Saving " + lr.getClass().getName());
317 
318     // check that this LR is one of ours (i.e. has been adopted)
319     if(lr.getDataStore() == null || ! lr.getDataStore().equals(this))
320       throw new PersistenceException(
321         "LR " + lr.getName() + " has not been adopted by this DataStore"
322       );
323 
324     // find the resource data for this LR
325     ResourceData lrData =
326       (ResourceData) Gate.getCreoleRegister().get(lr.getClass().getName());
327 
328     // create a subdirectory for resources of this type if none exists
329     File resourceTypeDirectory = new File(storageDir, lrData.getClassName());
330     if(
331       (! resourceTypeDirectory.exists()) ||
332       (! resourceTypeDirectory.isDirectory())
333     ) {
334       if(! resourceTypeDirectory.mkdir())
335         throw new PersistenceException("Can't write " + resourceTypeDirectory);
336     }
337 
338     // create an indentifier for this resource
339     String lrName = null;
340     Object lrPersistenceId = null;
341     lrName = lr.getName();
342     lrPersistenceId = lr.getLRPersistenceId();
343 
344     if(lrName == null)
345       lrName = lrData.getName();
346     if(lrPersistenceId == null) {
347       lrPersistenceId = constructPersistenceId(lrName);
348       lr.setLRPersistenceId(lrPersistenceId);
349     }
350 
351     //we're saving a corpus. I need to save it's documents first
352     if (lr instanceof Corpus) {
353       //check if the corpus is the one we support. CorpusImpl cannot be saved!
354       if (! (lr instanceof SerialCorpusImpl))
355         throw new PersistenceException("Can't save a corpus which " +
356                                        "is not of type SerialCorpusImpl!");
357       SerialCorpusImpl corpus = (SerialCorpusImpl) lr;
358       //this is a list of the indexes of all newly-adopted documents
359       //which will be used by the SerialCorpusImpl to update the
360       //corresponding document IDs
361       for (int i = 0; i < corpus.size(); i++) {
362         //if the document is not in memory, there's little point in saving it
363         if ( (!corpus.isDocumentLoaded(i)) && corpus.isPersistentDocument(i))
364           continue;
365         if (DEBUG)
366           Out.prln("Saving document at position " + i);
367         if (DEBUG)
368           Out.prln("Document in memory " + corpus.isDocumentLoaded(i));
369         if (DEBUG)
370           Out.prln("is persistent? "+ corpus.isPersistentDocument(i));
371         if (DEBUG)
372           Out.prln("Document name at position" + corpus.getDocumentName(i));
373         Document doc = (Document) corpus.get(i);
374         try {
375           //if the document is not already adopted, we need to do that first
376           if (doc.getLRPersistenceId() == null) {
377             if (DEBUG) Out.prln("Document adopted" + doc.getName());
378             doc = (Document) this.adopt(doc, null);
379             this.sync(doc);
380             if (DEBUG) Out.prln("Document sync-ed");
381             corpus.setDocumentPersistentID(i, doc.getLRPersistenceId());
382             if (DEBUG) Out.prln("new document ID " + doc.getLRPersistenceId());
383           } else //if it is adopted, just sync it
384             this.sync(doc);
385         } catch (Exception ex) {
386           throw new PersistenceException("Error while saving corpus: "
387                                          + corpus
388                                          + "because of an error storing document "
389                                          + ex.getMessage());
390         }
391       }//for loop through documents
392     }
393 
394     // create a File to store the resource in
395     File resourceFile = new File(resourceTypeDirectory, (String) lrPersistenceId);
396 
397     // dump the LR into the new File
398     try {
399       OutputStream os = new FileOutputStream(resourceFile);
400 
401       // after 1.1 the serialised files are compressed
402       if(! currentProtocolVersion.equals("1.0"))
403         os = new GZIPOutputStream(os);
404 
405       ObjectOutputStream oos = new ObjectOutputStream(os);
406       oos.writeObject(lr);
407       oos.close();
408     } catch(IOException e) {
409       throw new PersistenceException("Couldn't write to storage file: " + e);
410     }
411 
412     // let the world know about it
413     fireResourceWritten(
414       new DatastoreEvent(
415         this, DatastoreEvent.RESOURCE_WRITTEN, lr, (String) lrPersistenceId
416       )
417     );
418   } // sync(LR)
419 
420   /** Create a persistent store Id from the name of a resource. */
421   protected String constructPersistenceId(String lrName) {
422     return lrName + "___" + new Date().getTime() + "___" + random();
423   } // constructPersistenceId
424 
425   /** Get a resource from the persistent store.
426     * <B>Don't use this method - use Factory.createResource with
427     * DataStore and DataStoreInstanceId parameters set instead.</B>
428     * (Sometimes I wish Java had "friend" declarations...)
429     */
430   public LanguageResource getLr(String lrClassName, Object lrPersistenceId)
431   throws PersistenceException,SecurityException {
432 
433     // find the subdirectory for resources of this type
434     File resourceTypeDirectory = new File(storageDir, lrClassName);
435     if(
436       (! resourceTypeDirectory.exists()) ||
437       (! resourceTypeDirectory.isDirectory())
438     ) {
439         throw new PersistenceException("Can't find " + resourceTypeDirectory);
440     }
441 
442     // create a File to representing the resource storage file
443     File resourceFile = new File(resourceTypeDirectory, (String)lrPersistenceId);
444     if(! resourceFile.exists() || ! resourceFile.isFile())
445       throw new PersistenceException("Can't find file " + resourceFile);
446 
447     // try and read the file and deserialise it
448     LanguageResource lr = null;
449     try {
450       InputStream is = new FileInputStream(resourceFile);
451 
452       // after 1.1 the serialised files are compressed
453       if(! currentProtocolVersion.equals("1.0"))
454         is = new GZIPInputStream(is);
455 
456       ObjectInputStream ois = new ObjectInputStream(is);
457       lr = (LanguageResource) ois.readObject();
458       ois.close();
459     } catch(IOException e) {
460       throw
461         new PersistenceException("Couldn't read file "+resourceFile+": "+e);
462     } catch(ClassNotFoundException ee) {
463       throw
464         new PersistenceException("Couldn't find class "+lrClassName+": "+ee);
465     }
466 
467     // set the dataStore property of the LR (which is transient and therefore
468     // not serialised)
469     lr.setDataStore(this);
470     lr.setLRPersistenceId(lrPersistenceId);
471 
472     if (DEBUG) Out.prln("LR read in memory: " + lr);
473 
474     return lr;
475   } // getLr(id)
476 
477   /** Get a list of the types of LR that are present in the data store. */
478   public List getLrTypes() throws PersistenceException {
479     if(storageDir == null || ! storageDir.exists())
480       throw new PersistenceException("Can't read storage directory");
481 
482     // filter out the version file
483     String[] fileArray = storageDir.list();
484     List lrTypes = new ArrayList();
485     for(int i=0; i<fileArray.length; i++)
486       if(! fileArray[i].equals(versionFileName))
487         lrTypes.add(fileArray[i]);
488 
489     return lrTypes;
490   } // getLrTypes()
491 
492   /** Get a list of the IDs of LRs of a particular type that are present. */
493   public List getLrIds(String lrType) throws PersistenceException {
494     // a File to represent the directory for this type
495     File resourceTypeDir = new File(storageDir, lrType);
496     if(! resourceTypeDir.exists())
497       return Arrays.asList(new String[0]);
498 
499     return Arrays.asList(resourceTypeDir.list());
500   } // getLrIds(lrType)
501 
502   /** Get a list of the names of LRs of a particular type that are present. */
503   public List getLrNames(String lrType) throws PersistenceException {
504     // the list of files storing LRs of this type; an array for the names
505     String[] lrFileNames = (String[]) getLrIds(lrType).toArray();
506     ArrayList lrNames = new ArrayList();
507 
508     // for each lr file name, munge its name and add to the lrNames list
509     for(int i = 0; i<lrFileNames.length; i++) {
510       String name = getLrName(lrFileNames[i]);
511       lrNames.add(name);
512     }
513 
514     return lrNames;
515   } // getLrNames(lrType)
516 
517   /** Get the name of an LR from its ID. */
518   public String getLrName(Object lrId) {
519     int secondSeparator = ((String) lrId).lastIndexOf("___");
520     lrId = ((String) lrId).substring(0, secondSeparator);
521     int firstSeparator = ((String) lrId).lastIndexOf("___");
522 
523     return ((String) lrId).substring(0, firstSeparator);
524   } // getLrName
525 
526   /** Set method for the autosaving behaviour of the data store.
527     * <B>NOTE:</B> this type of datastore has no auto-save function,
528     * therefore this method throws an UnsupportedOperationException.
529     */
530   public void setAutoSaving(boolean autoSaving)
531   throws UnsupportedOperationException {
532     throw new UnsupportedOperationException(
533       "SerialDataStore has no auto-save capability"
534     );
535   } // setAutoSaving
536 
537   /** Get the autosaving behaviour of the LR. */
538   public boolean isAutoSaving() { return autoSaving; }
539 
540   /** Flag for autosaving behaviour. */
541   protected boolean autoSaving = false;
542 
543   /** Generate a random integer between 0 and 9999 for file naming. */
544   protected static int random() {
545     return randomiser.nextInt(9999);
546   } // random
547 
548   /** Random number generator */
549   protected static Random randomiser = new Random();
550   private transient Vector datastoreListeners;
551 
552   /** String representation */
553   public String toString() {
554     String nl = Strings.getNl();
555     StringBuffer s = new StringBuffer("SerialDataStore: ");
556     s.append("autoSaving: " + autoSaving);
557     s.append("; storageDir: " + storageDir);
558     s.append(nl);
559 
560     return s.toString();
561   } // toString()
562 
563   /** Calculate a hash code based on the class and the storage dir. */
564   public int hashCode(){
565     return getClass().hashCode() ^ storageDir.hashCode();
566   } // hashCode
567 
568   /** Equality: based on storage dir of other. */
569   public boolean equals(Object other) {
570 
571 
572     if (! (other instanceof SerialDataStore))
573       return false;
574 
575     if (! ((SerialDataStore)other).storageDir.equals(storageDir))
576       return false;
577 
578     //check for the name. First with equals, because they can be both null
579     //in which case trying just with equals leads to a null pointer exception
580     if (((SerialDataStore)other).name == name)
581       return true;
582     else
583       return ((SerialDataStore)other).name.equals(name);
584   } // equals
585 
586   public synchronized void removeDatastoreListener(DatastoreListener l) {
587     if (datastoreListeners != null && datastoreListeners.contains(l)) {
588       Vector v = (Vector) datastoreListeners.clone();
589       v.removeElement(l);
590       datastoreListeners = v;
591     }
592   }
593   public synchronized void addDatastoreListener(DatastoreListener l) {
594     Vector v = datastoreListeners == null ? new Vector(2) : (Vector) datastoreListeners.clone();
595     if (!v.contains(l)) {
596       v.addElement(l);
597       datastoreListeners = v;
598     }
599   }
600   protected void fireResourceAdopted(DatastoreEvent e) {
601     if (datastoreListeners != null) {
602       Vector listeners = datastoreListeners;
603       int count = listeners.size();
604       for (int i = 0; i < count; i++) {
605         ((DatastoreListener) listeners.elementAt(i)).resourceAdopted(e);
606       }
607     }
608   }
609   protected void fireResourceDeleted(DatastoreEvent e) {
610     if (datastoreListeners != null) {
611       Vector listeners = datastoreListeners;
612       int count = listeners.size();
613       for (int i = 0; i < count; i++) {
614         ((DatastoreListener) listeners.elementAt(i)).resourceDeleted(e);
615       }
616     }
617   }
618   protected void fireResourceWritten(DatastoreEvent e) {
619     if (datastoreListeners != null) {
620       Vector listeners = datastoreListeners;
621       int count = listeners.size();
622       for (int i = 0; i < count; i++) {
623         ((DatastoreListener) listeners.elementAt(i)).resourceWritten(e);
624       }
625     }
626   }
627 
628   /**
629    * Returns the name of the icon to be used when this datastore is displayed
630    * in the GUI
631    */
632   public String getIconName(){
633     return "ds.gif";
634   }
635 
636   /**
637    * Returns the comment displayed by the GUI for this DataStore
638    */
639   public String getComment(){
640     return "GATE serial datastore";
641   }
642 
643   /**
644    * Checks if the user (identified by the sessionID)
645    *  has read access to the LR
646    */
647   public boolean canReadLR(Object lrID)
648     throws PersistenceException, gate.security.SecurityException{
649 
650     return true;
651   }
652   /**
653    * Checks if the user (identified by the sessionID)
654    * has write access to the LR
655    */
656   public boolean canWriteLR(Object lrID)
657     throws PersistenceException, gate.security.SecurityException{
658 
659     return true;
660   }
661 
662     /** Sets the name of this resource*/
663   public void setName(String name){
664     this.name = name;
665   }
666 
667   /** Returns the name of this resource*/
668   public String getName(){
669     return name;
670   }
671 
672 
673 
674   /** get security information for LR . */
675   public SecurityInfo getSecurityInfo(LanguageResource lr)
676     throws PersistenceException {
677 
678     throw new UnsupportedOperationException("security information is not supported "+
679                                             "for DatabaseDataStore");
680   }
681 
682   /** set security information for LR . */
683   public void setSecurityInfo(LanguageResource lr,SecurityInfo si)
684     throws PersistenceException, gate.security.SecurityException {
685 
686     throw new UnsupportedOperationException("security information is not supported "+
687                                             "for DatabaseDataStore");
688 
689   }
690 
691 
692   /** identify user using this datastore */
693   public void setSession(Session s)
694     throws gate.security.SecurityException {
695 
696     // do nothing
697   }
698 
699 
700 
701   /** identify user using this datastore */
702   public Session getSession(Session s)
703     throws gate.security.SecurityException {
704 
705     return null;
706   }
707 
708   /**
709    * Try to acquire exlusive lock on a resource from the persistent store.
710    * Always call unlockLR() when the lock is no longer needed
711    */
712   public boolean lockLr(LanguageResource lr)
713   throws PersistenceException,SecurityException {
714     return true;
715   }
716 
717   /**
718    * Releases the exlusive lock on a resource from the persistent store.
719    */
720   public void unlockLr(LanguageResource lr)
721   throws PersistenceException,SecurityException {
722     return;
723   }
724 
725   /** Get a list of LRs that satisfy some set or restrictions */
726   public List findLrIds(List constraints) throws PersistenceException {
727     throw new UnsupportedOperationException(
728                               "Serial DataStore does not support document retrieval.");
729   }
730 
731   /**
732    *  Get a list of LRs that satisfy some set or restrictions and are
733    *  of a particular type
734    */
735   public List findLrIds(List constraints, String lrType) throws PersistenceException {
736     throw new UnsupportedOperationException(
737                               "Serial DataStore does not support document retrieval.");
738   }
739 
740 } // class SerialDataStore
741