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