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