1   /*
2    *  Copyright (c) 1998-2004, The University of Sheffield.
3    *
4    *  This file is part of GATE (see http://gate.ac.uk/), and is free
5    *  software, licenced under the GNU Library General Public License,
6    *  Version 2, June 1991 (in the distribution as file licence.html,
7    *  and also available at http://gate.ac.uk/gate/licence.html).
8    *
9    *  Valentin Tablan 25/10/2001
10   *
11   *  $Id: PersistenceManager.java,v 1.14 2004/07/21 17:10:09 akshay Exp $
12   *
13   */
14  package gate.util.persistence;
15  
16  import java.io.*;
17  import java.net.MalformedURLException;
18  import java.net.URL;
19  import java.text.NumberFormat;
20  import java.util.*;
21  
22  import gate.*;
23  import gate.creole.*;
24  import gate.event.ProgressListener;
25  import gate.event.StatusListener;
26  import gate.gui.MainFrame;
27  import gate.persist.PersistenceException;
28  import gate.util.*;
29  
30  /**
31   * This class provides utility methods for saving resources through
32   * serialisation via static methods.
33   */
34  public class PersistenceManager {
35  
36    /**
37     * A reference to an object; it uses the identity hashcode and the equals
38     * defined by object identity.
39     * These values will be used as keys in the
40     * {link #existingPersitentReplacements} map.
41     */
42    static protected class ObjectHolder{
43      ObjectHolder(Object target){
44        this.target = target;
45      }
46  
47      public int hashCode(){
48        return System.identityHashCode(target);
49      }
50  
51      public boolean equals(Object obj){
52        if(obj instanceof ObjectHolder)
53          return ((ObjectHolder)obj).target == this.target;
54        else return false;
55      }
56  
57      public Object getTarget(){
58        return target;
59      }
60  
61      private Object target;
62    }//static class ObjectHolder{
63  
64    /**
65     * This class is used as a marker for types that should NOT be serialised when
66     * saving the state of a gate object.
67     * Registering this type as the persistent equivalent for a specific class
68     * (via {@link PersistenceManager#registerPersitentEquivalent(Class , Class)})
69     * effectively stops all values of the specified type from being serialised.
70     *
71     * Maps that contain values that should not be serialised will have that entry
72     * removed. In any other places where such values occur they will be replaced
73     * by null after deserialisation.
74     */
75    public static class SlashDevSlashNull implements Persistence{
76      /**
77       * Does nothing
78       */
79      public void extractDataFromSource(Object source)throws PersistenceException{
80      }
81  
82      /**
83       * Returns null
84       */
85      public Object createObject()throws PersistenceException,
86                                         ResourceInstantiationException{
87        return null;
88      }
89      static final long serialVersionUID = -8665414981783519937L;
90    }
91  
92    /**
93     * URLs get upset when serialised and deserialised so we need to convert them
94     * to strings for storage.
95     * In the case of "file:" URLs the relative path to the persistence
96     * file will actually be stored.
97     */
98    public static class URLHolder implements Persistence{
99      /**
100      * Populates this Persistence with the data that needs to be stored from the
101      * original source object.
102      */
103     public void extractDataFromSource(Object source)throws PersistenceException{
104       try{
105         URL url = (URL)source;
106         if(url.getProtocol().equals("file")){
107           try{
108             urlString = relativePathMarker +
109                         getRelativePath(persistenceFile.toURL(), url);
110           }catch(MalformedURLException mue){
111             urlString = ((URL)source).toExternalForm();
112           }
113         }else{
114           urlString = ((URL)source).toExternalForm();
115         }
116       }catch(ClassCastException cce){
117         throw new PersistenceException(cce);
118       }
119     }
120 
121     /**
122      * Creates a new object from the data contained. This new object is supposed
123      * to be a copy for the original object used as source for data extraction.
124      */
125     public Object createObject()throws PersistenceException{
126       try{
127         if(urlString.startsWith(relativePathMarker)){
128           URL context = persistenceFile.toURL();
129           return new URL(context,
130                          urlString.substring(relativePathMarker.length()));
131         }else return new URL(urlString);
132       }catch(MalformedURLException mue){
133         throw new PersistenceException(mue);
134       }
135     }
136     String urlString;
137     /**
138      * This string will be used to start the serialisation of URL that represent
139      * relative paths.
140      */
141     private static final String relativePathMarker = "$relpath$";
142     static final long serialVersionUID = 7943459208429026229L;
143   }
144 
145   public static class ClassComparator implements Comparator{
146     /**
147      * Compares two {@link Class} values in terms of specificity; the more
148      * specific class is said to be "smaller" than the more generic
149      * one hence the {@link Object} class is the "largest" possible
150      * class.
151      * When two classes are not comparable (i.e. not assignable from each other)
152      * in either direction a NotComparableException will be thrown.
153      * both input objects should be Class values otherwise a
154      * {@link ClassCastException} will be thrown.
155      *
156      */
157     public int compare(Object o1, Object o2){
158       Class c1 = (Class)o1;
159       Class c2 = (Class)o2;
160 
161       if(c1.equals(c2)) return 0;
162       if(c1.isAssignableFrom(c2)) return 1;
163       if(c2.isAssignableFrom(c1)) return -1;
164       throw new NotComparableException();
165     }
166   }
167 
168   /**
169    * Thrown by a comparator when the values provided for comparison are not
170    * comparable.
171    */
172   public static class NotComparableException extends RuntimeException{
173     public NotComparableException(String message){
174       super(message);
175     }
176     public NotComparableException(){
177     }
178   }
179 
180   /**
181    * Recursively traverses the provided object and replaces it and all its
182    * contents with the appropriate persistent equivalent classes.
183    *
184    * @param target the object to be analysed and translated into a persistent
185    * equivalent.
186    * @return the persistent equivalent value for the provided target
187    */
188   static Serializable getPersistentRepresentation(Object target)
189                       throws PersistenceException{
190     if(target == null) return null;
191     //first check we don't have it already
192     Persistence res = (Persistence)existingPersitentReplacements.
193                       get(new ObjectHolder(target));
194     if(res != null) return res;
195 
196     Class type = target.getClass();
197     Class newType = getMostSpecificPersistentType(type);
198     if(newType == null){
199       //no special handler
200       if(target instanceof Serializable) return (Serializable)target;
201       else throw new PersistenceException(
202                      "Could not find a serialisable replacement for " + type);
203     }
204 
205     //we have a new type; create the new object, populate and return it
206     try{
207       res = (Persistence)newType.newInstance();
208     }catch(Exception e){
209       throw new PersistenceException(e);
210     }
211     if(target instanceof NameBearer){
212       StatusListener sListener = (StatusListener)MainFrame.getListeners().
213                                  get("gate.event.StatusListener");
214       if(sListener != null){
215         sListener.statusChanged("Storing " + ((NameBearer)target).getName());
216       }
217     }
218     res.extractDataFromSource(target);
219     existingPersitentReplacements.put(new ObjectHolder(target), res);
220     return res;
221   }
222 
223 
224   static Object getTransientRepresentation(Object target)
225                       throws PersistenceException,
226                              ResourceInstantiationException{
227 
228     if(target == null || target instanceof SlashDevSlashNull) return null;
229     if(target instanceof Persistence){
230       Object resultKey = new ObjectHolder(target);
231       //check the cached values; maybe we have the result already
232       Object result = existingTransientValues.get(resultKey);
233       if(result != null) return result;
234 
235       //we didn't find the value: create it
236       result = ((Persistence)target).createObject();
237       existingTransientValues.put(resultKey, result);
238       return result;
239     }else return target;
240   }
241 
242 
243   /**
244    * Finds the most specific persistent replacement type for a given class.
245    * Look for a type that has a registered persistent equivalent starting from
246    * the provided class continuing with its superclass and implemented
247    * interfaces and their superclasses and implemented interfaces and so on
248    * until a type is found.
249    * Classes are considered to be more specific than interfaces and in
250    * situations of ambiguity the most specific types are considered to be the
251    * ones that don't belong to either java or GATE followed by the ones  that
252    * belong to GATE and followed by the ones that belong to java.
253    *
254    * E.g. if there are registered persitent types for {@link gate.Resource} and
255    * for {@link gate.LanguageResource} than such a request for a
256    * {@link gate.Document} will yield the registered type for
257    * {@link gate.LanguageResource}.
258    */
259   protected static Class getMostSpecificPersistentType(Class type){
260     //this list will contain all the types we need to expand to superclass +
261     //implemented interfaces. We start with the provided type and work our way
262     //up the ISA hierarchy
263     List expansionSet = new ArrayList();
264     expansionSet.add(type);
265 
266     //algorithm:
267     //1) check the current expansion set
268     //2) expand the expansion set
269 
270     //at each expansion stage we'll have a class and three lists of interfaces:
271     //the user defined ones; the GATE ones and the java ones.
272     List userInterfaces = new ArrayList();
273     List gateInterfaces = new ArrayList();
274     List javaInterfaces = new ArrayList();
275     while(!expansionSet.isEmpty()){
276       //1) check the current set
277       Iterator typesIter = expansionSet.iterator();
278       while(typesIter.hasNext()){
279         Class result = (Class)persistentReplacementTypes.get(typesIter.next());
280         if(result != null){
281           return result;
282         }
283       }
284       //2) expand the current expansion set;
285       //the expanded expansion set will need to be ordered according to the
286       //rules (class >> interface; user interf >> gate interf >> java interf)
287 
288       //at each point we only have at most one class
289       if(type != null) type = type.getSuperclass();
290 
291 
292       userInterfaces.clear();
293       gateInterfaces.clear();
294       javaInterfaces.clear();
295 
296       typesIter = expansionSet.iterator();
297       while(typesIter.hasNext()){
298         Class aType = (Class)typesIter.next();
299         Class[] interfaces = aType.getInterfaces();
300         //distribute them according to their type
301         for(int i = 0; i < interfaces.length; i++){
302           Class anIterf = interfaces[i];
303           String interfType = anIterf.getName();
304           if(interfType.startsWith("java")){
305             javaInterfaces.add(anIterf);
306           }else if(interfType.startsWith("gate")){
307             gateInterfaces.add(anIterf);
308           }else userInterfaces.add(anIterf);
309         }
310       }
311 
312       expansionSet.clear();
313       if(type != null) expansionSet.add(type);
314       expansionSet.addAll(userInterfaces);
315       expansionSet.addAll(gateInterfaces);
316       expansionSet.addAll(javaInterfaces);
317     }
318     //we got out the while loop without finding anything; return null;
319     return null;
320 
321 //    SortedSet possibleTypesSet = new TreeSet(classComparator);
322 //
323 //    Iterator typesIter = persistentReplacementTypes.keySet().iterator();
324 //    //we store all the types that could not be analysed
325 //    List lostTypes = new ArrayList();
326 //    while(typesIter.hasNext()){
327 //      Class aType = (Class)typesIter.next();
328 //      if(aType.isAssignableFrom(type)){
329 //        try{
330 //          possibleTypesSet.add(aType);
331 //        }catch(NotComparableException nce){
332 //          lostTypes.add(aType);
333 //        }
334 //      }
335 //    }
336 //
337 //    if(possibleTypesSet.isEmpty())  return null;
338 //
339 //    Class resultKey = (Class)possibleTypesSet.first();
340 //    Class result = (Class) persistentReplacementTypes.get(resultKey);
341 //
342 //    //check whether we lost anything important
343 //    typesIter = lostTypes.iterator();
344 //    while(typesIter.hasNext()){
345 //      Class aType = (Class)typesIter.next();
346 //      try{
347 //        if(classComparator.compare(aType, resultKey) < 0){
348 //          Err.prln("Found at least two incompatible most specific types for " +
349 //          type.getName() + ":\n " +
350 //          aType.toString() + " was discarded in favour of" + result.getName() +
351 //          ".\nSome of your saved results may have been lost!");
352 //        }
353 //      }catch(NotComparableException nce){
354 //        Err.prln("Found at least two incompatible most specific types for " +
355 //        type.getName() + ":\n " +
356 //        aType.toString() + " was discarded in favour of" + result.getName() +
357 //        ".\nSome of your saved results may have been lost!");
358 //      }
359 //    }
360 //
361 //    return result;
362   }
363 
364   /**
365    * Calculates the relative path for a file: URL starting from a given context
366    * which is also a file: URL.
367    * @param context the URL to be used as context.
368    * @param target the URL for which the relative path is computed.
369    * @return a String value representing the relative path. Constructing a URL
370    * from the context URL and the relative path should result in the target URL.
371    */
372   public static String getRelativePath(URL context, URL target){
373     if(context.getProtocol().equals("file") &&
374        target.getProtocol().equals("file")){
375 
376       //normalise the two file URLS
377       try{
378         context = new File(context.getPath()).toURL();
379       }catch(MalformedURLException mue){
380         throw new GateRuntimeException("Could not normalise the file URL:\n"+
381                                        context + "\nThe problem was:\n" +mue);
382       }
383       try{
384         target = new File(target.getPath()).toURL();
385       }catch(MalformedURLException mue){
386         throw new GateRuntimeException("Could not normalise the file URL:\n"+
387                                        target + "\nThe problem was:\n" +mue);
388       }
389       List targetPathComponents = new ArrayList();
390       File aFile = new File(target.getPath()).getParentFile();
391       while(aFile != null){
392         targetPathComponents.add(0, aFile);
393         aFile = aFile.getParentFile();
394       }
395       List contextPathComponents = new ArrayList();
396       aFile = new File(context.getPath()).getParentFile();
397       while(aFile != null){
398         contextPathComponents.add(0, aFile);
399         aFile = aFile.getParentFile();
400       }
401       //the two lists can have 0..n common elements (0 when the files are
402       //on separate roots
403       int commonPathElements = 0;
404       while(commonPathElements < targetPathComponents.size() &&
405             commonPathElements < contextPathComponents.size() &&
406             targetPathComponents.get(commonPathElements).
407             equals(contextPathComponents.get(commonPathElements)))
408         commonPathElements++;
409       //construct the string for the relative URL
410       String relativePath = "";
411       for(int i = commonPathElements;
412           i < contextPathComponents.size(); i++){
413         if(relativePath.length() == 0) relativePath += "..";
414         else relativePath += "/..";
415       }
416       for(int i = commonPathElements; i < targetPathComponents.size(); i++){
417         String aDirName = ((File)targetPathComponents.get(i)).getName();
418         if(aDirName.length() == 0){
419           aDirName = ((File)targetPathComponents.get(i)).getAbsolutePath();
420           if(aDirName.endsWith(File.separator)){
421             aDirName = aDirName.substring(0, aDirName.length() -
422                                              File.separator.length());
423           }
424         }
425 //Out.prln("Adding \"" + aDirName + "\" name for " + targetPathComponents.get(i));
426         if(relativePath.length() == 0){
427           relativePath += aDirName;
428         }else{
429           relativePath += "/" + aDirName;
430         }
431       }
432       //we have the directory; add the file name
433       if(relativePath.length() == 0){
434         relativePath += new File(target.getPath()).getName();
435       }else{
436         relativePath += "/" + new File(target.getPath()).getName();
437       }
438 
439       return relativePath;
440     }else{
441       throw new GateRuntimeException("Both the target and the context URLs " +
442                                      "need to be \"file:\" URLs!");
443     }
444   }
445 
446   public static void saveObjectToFile(Object obj, File file)
447                      throws PersistenceException, IOException {
448     ProgressListener pListener = (ProgressListener)MainFrame.getListeners()
449                                  .get("gate.event.ProgressListener");
450     StatusListener sListener = (gate.event.StatusListener)
451                                MainFrame.getListeners().
452                                get("gate.event.StatusListener");
453     long startTime = System.currentTimeMillis();
454     if(pListener != null) pListener.progressChanged(0);
455     ObjectOutputStream oos = null;
456     persistenceFile = file;
457     try{
458       //insure a clean start
459       existingPersitentReplacements.clear();
460       existingPersitentReplacements.clear();
461 
462       oos = new ObjectOutputStream(new FileOutputStream(file));
463 
464       //always write the list of creole URLs first
465       List urlList = new ArrayList(Gate.getCreoleRegister().getDirectories());
466       Object persistentList = getPersistentRepresentation(urlList);
467       oos.writeObject(persistentList);
468 
469       //now write the object
470       Object persistentObject = getPersistentRepresentation(obj);
471       oos.writeObject(persistentObject);
472     }finally{
473       persistenceFile = null;
474       if(oos != null){
475         oos.flush();
476         oos.close();
477       }
478       long endTime = System.currentTimeMillis();
479       if(sListener != null) sListener.statusChanged(
480           "Storing completed in " +
481           NumberFormat.getInstance().format(
482           (double)(endTime - startTime) / 1000) + " seconds");
483       if(pListener != null) pListener.processFinished();
484     }
485   }
486 
487   public static Object loadObjectFromFile(File file)
488                      throws PersistenceException, IOException,
489                             ResourceInstantiationException {
490     exceptionOccured = false;
491     ProgressListener pListener = (ProgressListener)MainFrame.getListeners().
492                                  get("gate.event.ProgressListener");
493     StatusListener sListener = (gate.event.StatusListener)
494                                 MainFrame.getListeners()
495                                 .get("gate.event.StatusListener");
496     if(pListener != null) pListener.progressChanged(0);
497     long startTime = System.currentTimeMillis();
498     persistenceFile = file;
499     ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
500     Object res = null;
501     try{
502       //first read the list of creole URLs
503       Iterator urlIter = ((Collection)
504                           getTransientRepresentation(ois.readObject())).
505                           iterator();
506       while(urlIter.hasNext()){
507         URL anUrl = (URL)urlIter.next();
508         try{
509           if(!Gate.getCreoleRegister().getDirectories().contains(anUrl))
510             Gate.getCreoleRegister().registerDirectories(anUrl);
511         }catch(GateException ge){
512           Err.prln("Could not reload creole directory " +
513                    anUrl.toExternalForm());
514         }
515       }
516       //now we can read the saved object
517       res = ois.readObject();
518       ois.close();
519 
520       //ensure a fresh start
521       existingTransientValues.clear();
522       res = getTransientRepresentation(res);
523       existingTransientValues.clear();
524       long endTime = System.currentTimeMillis();
525               if(sListener != null) sListener.statusChanged(
526                   "Loading completed in " +
527                   NumberFormat.getInstance().format(
528                   (double)(endTime - startTime) / 1000) + " seconds");
529               if(pListener != null) pListener.processFinished();
530       if(exceptionOccured){
531         throw new PersistenceException("There were errors!\n" +
532                                        "See messages for details...");
533       }
534       return res;
535     }catch(ResourceInstantiationException rie){
536       if(sListener != null) sListener.statusChanged("Loading failed!");
537       if(pListener != null) pListener.processFinished();
538       throw rie;
539     }catch(Exception ex){
540       if(sListener != null) sListener.statusChanged("Loading failed!");
541       if(pListener != null) pListener.processFinished();
542       throw new PersistenceException(ex);
543     }finally{
544       persistenceFile = null;
545     }
546   }
547 
548 
549   /**
550    * Sets the persistent equivalent type to be used to (re)store a given type
551    * of transient objects.
552    * @param transientType the type that will be replaced during serialisation
553    * operations
554    * @param persistentType the type used to replace objects of transient type
555    * when serialising; this type needs to extend {@link Persistence}.
556    * @return the persitent type that was used before this mapping if such
557    * existed.
558    */
559   public static Class registerPersitentEquivalent(Class transientType,
560                                           Class persistentType)
561                throws PersistenceException{
562     if(!Persistence.class.isAssignableFrom(persistentType)){
563       throw new PersistenceException(
564         "Persistent equivalent types have to implement " +
565         Persistence.class.getName() + "!\n" +
566         persistentType.getName() + " does not!");
567     }
568     return (Class)persistentReplacementTypes.put(transientType, persistentType);
569   }
570 
571 
572   /**
573    * A dictionary mapping from java type (Class) to the type (Class) that can
574    * be used to store persistent data for the input type.
575    */
576   private static Map persistentReplacementTypes;
577 
578   /**
579    * Stores the persistent replacements created during a transaction in order to
580    * avoid creating two different persistent copies for the same object.
581    * The keys used are {@link ObjectHolder}s that contain the transient values
582    * being converted to persistent equivalents.
583    */
584   private static Map existingPersitentReplacements;
585 
586   /**
587    * Stores the transient values obtained from persistent replacements during a
588    * transaction in order to avoid creating two different transient copies for
589    * the same persistent replacement.
590    * The keys used are {@link ObjectHolder}s that hold persistent equivalents.
591    * The values are the transient values created by the persisten equivalents.
592    */
593   private static Map existingTransientValues;
594 
595   private static ClassComparator classComparator = new ClassComparator();
596 
597   /**
598    * This flag is set to true when an exception occurs. It is used in order to
599    * allow error reporting without interrupting the current operation.
600    */
601   static boolean exceptionOccured = false;
602 
603   /**
604    * The file currently used to write/read the persisten representation.
605    * Will only have a non-null value during storing and restorin operations.
606    */
607   static File persistenceFile;
608 
609   static{
610     persistentReplacementTypes = new HashMap();
611     try{
612       //VRs don't get saved, ....sorry guys :)
613       registerPersitentEquivalent(VisualResource.class,
614                                   SlashDevSlashNull.class);
615 
616       registerPersitentEquivalent(URL.class, URLHolder.class);
617 
618       registerPersitentEquivalent(Map.class, MapPersistence.class);
619       registerPersitentEquivalent(Collection.class,
620                                   CollectionPersistence.class);
621 
622       registerPersitentEquivalent(ProcessingResource.class,
623                                   PRPersistence.class);
624 
625       registerPersitentEquivalent(DataStore.class,
626                                   DSPersistence.class);
627 
628       registerPersitentEquivalent(LanguageResource.class,
629                                   LRPersistence.class);
630 
631       registerPersitentEquivalent(Corpus.class,
632                                   CorpusPersistence.class);
633 
634       registerPersitentEquivalent(Controller.class,
635                                   ControllerPersistence.class);
636 
637       registerPersitentEquivalent(ConditionalController.class,
638                                   ConditionalControllerPersistence.class);
639 
640       registerPersitentEquivalent(LanguageAnalyser.class,
641                                   LanguageAnalyserPersistence.class);
642 
643       registerPersitentEquivalent(SerialAnalyserController.class,
644                                   SerialAnalyserControllerPersistence.class);
645 
646       registerPersitentEquivalent(gate.persist.JDBCDataStore.class,
647                                   JDBCDSPersistence.class);
648       registerPersitentEquivalent(gate.creole.AnalyserRunningStrategy.class,
649                                   AnalyserRunningStrategyPersistence.class);
650     }catch(PersistenceException pe){
651       //builtins shouldn't raise this
652       pe.printStackTrace();
653     }
654     existingPersitentReplacements = new HashMap();
655     existingTransientValues = new HashMap();
656   }
657 }