|
SerialDataStore |
|
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
|
SerialDataStore |
|