Dependencies.java
0001 /*
0002  *  Dependencies.java
0003  *  Copyright (c) 1998-2008, The University of Sheffield.
0004  *
0005  *  This code is from the GATE project (http://gate.ac.uk/) and is free
0006  *  software licenced under the GNU General Public License version 3. It is
0007  *  distributed without any warranty. For more details see COPYING.txt in the
0008  *  top level directory (or at http://gatewiki.sf.net/COPYING.txt).
0009  */
0010 
0011 package gate.yam.depend;
0012 
0013 import gate.persist.PersistenceException;
0014 import gate.util.GateException;
0015 import gate.yam.YamFile;
0016 import org.apache.log4j.Logger;
0017 
0018 import java.io.*;
0019 import java.util.*;
0020 import java.util.concurrent.ConcurrentHashMap;
0021 
0022 
0023 //todo: serialization methods throw PersistenceException. Deal with in method?.
0024 
0025 
0026 /**
0027  * Dependencies keeps track of the link and inclusion dependencies between
0028  * YamFiles and other Files.
0029  @author Angus Roberts
0030  */
0031 public class Dependencies implements Serializable {
0032 
0033   /** UID for stable serialisation */
0034   private static final long serialVersionUID = 1L;
0035 
0036   /**
0037    * Logger
0038    */
0039   private static final Logger log =
0040     Logger.getLogger("gate.yam.depend.Dependencies");
0041 
0042   /**
0043    * A Map of wiki area identifiers to Dependencies instances. Implemented with
0044    * ConcurrenthashMap, and so is thread-safe. ConcurrenthashMap provides its
0045    * own locking for updates. Construction uses the default concurrency factor
0046    * (16), which should be ample for allowing multiple threads to update,
0047    * without being so high as to impact on performance.  
0048    */
0049   private static Map<String, Dependencies> wikiDependencies
0050     new ConcurrentHashMap<String, Dependencies>();
0051 
0052   /**
0053    * Clear out the current Dependencies. Does not remove serialized
0054    * Dependencies.
0055    */
0056   public static void clear() {
0057     log.info("Dependencies.clear()");
0058     wikiDependencies.clear();
0059   }
0060 
0061   /**
0062    * The prefix used for serialized dependency files.
0063    */
0064   private static String serializationFilePrefix = "Dependencies_";
0065 
0066   /**
0067    * The suffix used for serialized dependency files.
0068    */
0069   private static String serializationFileSuffix = ".ser";
0070 
0071   /**
0072    * The directory to which Dependencies are serialized
0073    */
0074   private static File serializationDirectory;
0075 
0076   /**
0077    * Set the directory to which Dependencies are serialized. This is not
0078    * synchonised. It is expected that serializationDirectory will be set once by
0079    * the wiki on start up, and that neither users nor administrators will change
0080    * it while the wiki is running.
0081    *
0082    @param directory The directory to which Dependencies are serialized
0083    */
0084   public static void setSerializationDirectory(File directory) {
0085     serializationDirectory = directory;
0086     log.info("Dependencies.setSerializationDirectory(" + directory + ")");
0087   }
0088 
0089   /**
0090    * Set the prefix for serialized dependency file names. This is not
0091    * synchonised. It is expected that it will be set once by the wiki on start
0092    * up, and that neither users nor administrators will change it while the
0093    * wiki is running.
0094    *
0095    @param prefix The prefix for serialized dependency files. The dependency
0096    * file name is made up of this prefix, the wiki ID, and the configured
0097    * suffix.
0098    */
0099   public static void setSerializationFilePrefix(String prefix) {
0100     serializationFilePrefix = prefix;
0101   }
0102 
0103   /**
0104    * Set the suffix for serialized dependency file names. This is not
0105    * synchonised. It is expected that it will be set once by the wiki on start
0106    * up, and that neither users nor administrators will change it while the
0107    * wiki is running.
0108    *
0109    @param suffix The suffix for serialized dependency files. The dependency
0110    * file name is made up of the configured prefix, the wiki ID, and this
0111    * suffix.
0112    */
0113   public static void setSerializationFileSuffix(String suffix) {
0114     serializationFileSuffix = suffix;
0115   }
0116 
0117   /**
0118    * Get the Dependencies instance for a wiki area. Lazily loads if
0119    * serialized. Creates a new Dependencies if none exists for a wiki area.
0120    *
0121    @param wikiID The identifier for the wiki area
0122    @return The Dependencies for the wiki area, a new Dependencies if one
0123    * did not previously exist, null if given a null wiki identifier.
0124    @throws gate.persist.PersistenceException If a Dependencies
0125    * serialization file cannot be deserialized
0126    */
0127   public static Dependencies get(String wikiIDthrows PersistenceException {
0128 
0129     log.debug("Dependencies.get(" + wikiID + ")");
0130     if(wikiID == null || wikiID.equals("null"))
0131       return null;
0132 
0133     // Have we already loaded these dependencies?
0134     Dependencies depsForID = wikiDependencies.get(wikiID);
0135     if (depsForID != null) {
0136       log.debug("Found Dependencies in Map for ID: " + wikiID);
0137       return depsForID;
0138     }
0139 
0140     // Maybe we have serialized the Dependencies?
0141     String serializationFileName =
0142       serializationFilePrefix + wikiID + serializationFileSuffix;
0143     File serializationFile =
0144       new File(serializationDirectory, serializationFileName);
0145 
0146     // Does this serialization file exist? If not, we don't try to
0147     // deserialize and our Dependecies remains null
0148     if (serializationFile.exists()) {
0149       try {
0150         log.info(
0151           "Deserializing wiki ID: " + wikiID + " from: " +
0152           serializationFileName
0153         );
0154 
0155         FileInputStream fis = new FileInputStream(serializationFile);
0156         ObjectInputStream ois = new ObjectInputStream(fis);
0157         depsForID = (Dependenciesois.readObject();
0158         ois.close();
0159 
0160         // Stuff it in the map for next time we want it
0161         wikiDependencies.put(wikiID, depsForID);
0162       catch (IOException ioe) {
0163         throw new PersistenceException(ioe);
0164       catch (ClassNotFoundException cnfe) {
0165         throw new PersistenceException(cnfe);
0166       }
0167     }
0168 
0169     // Still null: we don't have a Dependencies for this wiki (maybe it was
0170     // deleted?), so create one.
0171     if (depsForID == null) {
0172       log.info("Creating new Dependencies for wiki ID: " + wikiID);
0173       depsForID = new Dependencies();
0174       wikiDependencies.put(wikiID, depsForID);
0175     }
0176 
0177     return depsForID;
0178   }
0179 
0180   /** Get Dependencies from a Long id. */
0181   public static Dependencies get(Long wikiIDthrows PersistenceException {
0182     return get(wikiID.toString());
0183   // get(Long)
0184 
0185   /**
0186    * Do we have the Dependencies for a wiki, either loaded or serialized?
0187    *
0188    @param wikiID The ID of the wiki to be checked for Dependencies
0189    @return true if Dependencies exist for wikiID
0190    */
0191   public static boolean exists(String wikiID) {
0192     // Is it in our map of wikis
0193     if (wikiDependencies.containsKey(wikiID)) {
0194       return true;
0195     else {
0196       // Does it exist in serialized form?
0197       String serializationFileName =
0198         serializationFilePrefix + wikiID + serializationFileSuffix;
0199       File serializationFile =
0200         new File(serializationDirectory, serializationFileName);
0201 
0202       return serializationFile.exists();
0203     }
0204   }
0205 
0206   /**
0207    * Remove the Dependencies for a wiki area. Removes from current
0208    * Dependencies, and any serialization.
0209    *
0210    @param wikiID The id of the wiki area for which Dependecies will be
0211    * removed.
0212    @throws PersistenceException If a serialized Dependencies could not be
0213    * deleted.
0214    */
0215   public static void remove(String wikiIDthrows PersistenceException {
0216     log.debug("Dependencies.removeDependencies(" + wikiID + ")");
0217 
0218     // First, remove from our map of loaded Dependencies
0219     wikiDependencies.remove(wikiID);
0220 
0221     // Now remove any serialization
0222     String serializationFileName =
0223       serializationFilePrefix + wikiID + serializationFileSuffix;
0224     File serializationFile =
0225       new File(serializationDirectory, serializationFileName);
0226     if (serializationFile.exists()) {
0227       log.info("Deleting Dependencies serialization: " + serializationFileName);
0228       if (!serializationFile.delete()) {
0229         throw new PersistenceException(
0230           "Failed to delete serialized Dependencies: " + serializationFileName
0231         );
0232       }
0233     }
0234   }
0235 
0236   /**
0237    * Serialize all Dependencies for every wiki to the serialization directory
0238    *
0239    @throws PersistenceException when serialization gives a filesystem error.
0240    */
0241   public static void serialize() throws PersistenceException {
0242 
0243     log.debug("Dependencies.serialize()");
0244 
0245     // We need to synchronize here. Although keySet is thread-safe, it is only
0246     // "weakly consistent" according to the javadoc. ConcurrentHashMap
0247     // iterators are only designed to be used by a single thread.
0248     synchronized(wikiDependencies) {
0249       for (String wikiID : wikiDependencies.keySet()) {
0250         serialize(wikiID);
0251       }
0252     }
0253 
0254   }
0255 
0256   /**
0257    * Serialize the Dependencies for a wiki to the serialization directory
0258    @param wikiID The ID of the wiki for which the Dependencies will be
0259    * serialized
0260    @throws PersistenceException when serialization gives a filesystem error
0261    */
0262   public static void serialize(String wikiIDthrows PersistenceException {
0263 
0264     log.debug("Dependencies.serialize(" + wikiID + ")");
0265 
0266     try {
0267       Dependencies dep = get(wikiID);
0268 
0269       String serializationFileName =
0270               serializationFilePrefix + wikiID + serializationFileSuffix;
0271       File serializationFile =
0272               new File(serializationDirectory, serializationFileName);
0273       FileOutputStream fos = new FileOutputStream(serializationFile);
0274 
0275       log.info(
0276               "Serializing wiki ID: " + wikiID + " to: " + serializationFileName
0277       );
0278       ObjectOutputStream oos = new ObjectOutputStream(fos);
0279       oos.writeObject(dep);
0280       oos.close();
0281     catch (IOException ioe) {
0282       throw new PersistenceException(ioe);
0283     }
0284 
0285   }
0286 
0287 
0288 
0289   /**
0290    * A Map of a File path to a Set of all file paths that link to it. Files are
0291    * given by canonical paths. The key is "linked by" its values. The key is the
0292    * file that is linked to. This Map gives the inverse of the relation in the
0293    * linkedTo Map, and the contents of the two should be kept consistent. Both
0294    * directions of the relation are recorded to make removal of old relations
0295    * easier.
0296    */
0297   private Map<String, Set<String>> linkedBy =
0298     new HashMap<String, Set<String>>();
0299 
0300   /**
0301    * A Map of a File path to a Set of all file paths that it links to. Files are
0302    * given by canonical paths. The key "links to" its values. The key is the
0303    * file that contains the link. This Map gives the inverse of the relation in
0304    * the linkedBy Map, and the contents of the two should be kept consistent.
0305    */
0306   private Map<String, Set<String>> linksTo =
0307           new HashMap<String, Set<String>>();
0308 
0309   /**
0310    * A Map of a File path to a Set of all file paths that include it. Files are
0311    * given by canonical paths. The key is "included by" its values. The key is
0312    * the file that is included. This Map gives the inverse of the relation in
0313    * the includes Map, and the contents of the two should be kept consistent.
0314    * Both directions of the relation are recorded to make removal of old
0315    * relations easier, and makes the calculation of transitive includes easier.
0316    */
0317   private Map<String, Set<String>> includedBy =
0318     new HashMap<String, Set<String>>();
0319 
0320   /**
0321    * A Map of a File path to a Set of all file paths that it includes. Files are
0322    * given by canonical paths. The key "includes" its values. The key is the
0323    * file that contains the include statement. This Map gives the inverse of the
0324    * relation in the includedBy Map, and the contents of the two should be kept
0325    * consistent.
0326    */
0327   private Map<String, Set<String>> includes =
0328     new HashMap<String, Set<String>>();
0329 
0330   /**
0331    * Does this Dependencies contain any information about links or includes?
0332    *
0333    @return true if this Dependencies contains no information about links,
0334    *         and no information about includes
0335    */
0336   public boolean isEmpty() {
0337     synchronized (this) {
0338       return linkedBy.isEmpty() && includedBy.isEmpty();
0339     }
0340   }
0341 
0342   /**
0343    * Are two Dependencies equal? They are if they contain the same links and
0344    * inclusions between the same YamFiles.
0345    *
0346    @param obj the Object against which we test equality
0347    @return true if obj is equal to this Dependencies.
0348    */
0349   public boolean equals(Object obj) {
0350     if (obj == nullreturn false;
0351     if (!(obj instanceof Dependencies)) return false;
0352 
0353     Dependencies other = (Dependenciesobj;
0354 
0355     synchronized (this) {
0356       return
0357         this.linkedBy.equals(other.linkedBy&&
0358         this.includedBy.equals(other.includedBy);
0359     }
0360   }
0361 
0362   /**
0363    * Returns the hash code value for this Dependencies.
0364    *
0365    @return The has code for this Dependencies
0366    */
0367   public int hashCode() {
0368     synchronized (this) {
0369       return linkedBy.hashCode() + includedBy.hashCode();
0370     }
0371   }
0372 
0373   /** String representation */
0374   public String toString() {
0375     StringBuffer buf = new StringBuffer();
0376     buf.append("Linked by: ");
0377     synchronized(this) {
0378       buf.append(linkedBy.toString());
0379       buf.append("\n");
0380       buf.append("Included by: ");
0381       buf.append(includedBy.toString());
0382     }
0383     return buf.toString();
0384   }
0385 
0386   /** Sorted string representation */
0387   public String toSortedString() {
0388     StringBuffer buf = new StringBuffer();
0389     synchronized(this) {
0390 
0391       List<String> linkKeys = new ArrayList<String>(linkedBy.keySet());
0392       List<String> incKeys = new ArrayList<String>(includedBy.keySet());
0393       Collections.sort(linkKeys);
0394       Collections.sort(incKeys);
0395 
0396       buf.append('{');
0397       boolean firstKey = true;
0398       for(String linkKey : linkKeys) {
0399         if(!firstKey) {
0400           buf.append(',');
0401         else {
0402           firstKey = false;
0403         }
0404         buf.append(linkKey);
0405         List<String> linkValues = new ArrayList<String>(linkedBy.get(linkKey));
0406         Collections.sort(linkValues);
0407         buf.append("=[");
0408         boolean firstValue = true;
0409         for(String linkValue : linkValues) {
0410           if(!firstValue) {
0411             buf.append(',');
0412           else {
0413             firstValue = false;
0414           }
0415           buf.append(linkValue);
0416         }
0417         buf.append(']');
0418       }
0419 
0420 
0421       buf.append("}{");
0422       firstKey = true;
0423       for(String incKey : incKeys) {
0424         if(!firstKey) {
0425           buf.append(',');
0426         else {
0427           firstKey = false;
0428         }
0429         buf.append(incKey);
0430         List<String> incValues = new ArrayList<String>(includedBy.get(incKey));
0431         Collections.sort(incValues);
0432         buf.append("=[");
0433         boolean firstValue = true;
0434         for(String incValue : incValues) {
0435           if(!firstValue) {
0436             buf.append(',');
0437           else {
0438             firstValue = false;
0439           }
0440           buf.append(incValue);
0441         }
0442         buf.append(']');
0443       }
0444       buf.append("}");
0445 
0446     }
0447     return buf.toString();
0448   }
0449 
0450 
0451   /**
0452    *
0453    <p>Update the Dependencies with the fact that a YamFile includes a list of
0454    * files. This list is taken to be all the includes from
0455    * the YamFile. Any previously recorded includes for this YamFile are removed.
0456    </p>
0457    *
0458    <p>Transitivity of includes is taken into account internally: the client
0459    * does not need to provide the transitive closure of includes.</p>
0460    <p> Note that this method is not synchronized: it must be synchonized by
0461    * calling methods.
0462    </p>
0463    *
0464    @param includingYamPath The canonical path of the YamFile that includes
0465    * other files
0466    @param includedPaths    A List of other File canonical paths that are
0467    * included in includingYam
0468    */
0469   private void includes(String includingYamPath, List<String> includedPaths) {
0470 
0471     log.debug("dependencies.includes(" + includingYamPath + ", "
0472             + includedPaths + ")");
0473 
0474     log.debug("Debugging includes - entered method");
0475     log.debug("includes = " + includes);
0476     log.debug("includedBy = " + includedBy);
0477 
0478     // Remove all old includes for this includingYamPath. Keep what we remove
0479     // so we can also remove old inverses
0480     Set<String> allIncluded =  includes.remove(includingYamPath);
0481 
0482     // Remove the old inverses in includedBy
0483     if (allIncluded != null) {
0484       for(String included : allIncluded) {
0485         Set<String> currentIncludedBy = includedBy.get(included);
0486         if (currentIncludedBy != null) {
0487           currentIncludedBy.remove(includingYamPath);
0488 
0489           // If we now have an empty Set of includedBy values,
0490           // remove from the Map.
0491           if(currentIncludedBy.isEmpty()) {
0492             includedBy.remove(included);
0493           }
0494         }
0495       }
0496     }
0497 
0498     log.debug("Debugging includes - removed old relationships" );
0499     log.debug("includes = " + includes);
0500     log.debug("includedBy = " + includedBy);
0501 
0502     // We need to get each path P, included by
0503     // the includingYamPath, and add in all of the new includedPaths into
0504     // P's includes.
0505     Set<String> includers = includedBy.get(includingYamPath);
0506     ifincluders != null) {
0507        for(String includer : includers) {
0508          Set<String> includesToUpdate =  includes.get(includer);
0509          ifincludesToUpdate == null) {
0510            // No includes for this includer: we need to add an empty set
0511            includesToUpdate = new HashSet<String>();
0512            includes.put(includer, includesToUpdate);
0513          }
0514          includesToUpdate.addAll(includedPaths);
0515        }
0516     }
0517 
0518     // Now we can add an entry for the new includes. For transitivity, we also
0519     // add in all the includes for the included paths
0520     if!includedPaths.isEmpty()) {
0521       Set<String> transIncluded = new HashSet<String>(includedPaths);
0522       includes.put(includingYamPath, transIncluded);
0523       for(String included : includedPaths) {
0524         Set<String> moreIncludes = includes.get(included);
0525         if(moreIncludes != null) {
0526           transIncluded.addAll(moreIncludes);
0527         }
0528       }
0529     }
0530 
0531     log.debug("Debugging includes - added new includes" );
0532     log.debug("includes = " + includes);
0533     log.debug("includedBy = " + includedBy);
0534 
0535     // Add the new includedBy. We need to do this for each includedPath
0536     forString includedPath : includedPaths) {
0537 
0538       // First, add the includingYamPath into includedBy for this one
0539       Set<String> currentIncludedBy = includedBy.get(includedPath);
0540       ifcurrentIncludedBy == null) {
0541         // Make a new set if there wasn't one
0542         currentIncludedBy = new HashSet<String>();
0543         includedBy.put(includedPath, currentIncludedBy);
0544       }
0545       currentIncludedBy.add(includingYamPath);
0546 
0547       log.debug("Debugging includes -  added direct includedBy for parameter: "
0548                + includedPath);
0549       log.debug("includes = " + includes);
0550       log.debug("includedBy = " + includedBy);
0551 
0552       // Now to ensure transitivity
0553 
0554       // First, add in anything that includingYamPath is itself includedBy.
0555       // Re-use these from above.
0556       if(includers != null) {
0557         currentIncludedBy.addAll(includers);
0558       }
0559 
0560       // Next, get anything that this current includedPath itself includes
0561       Set<String> includedPathIncludes = includes.get(includedPath);
0562       if(includedPathIncludes != null) {
0563         for(String path : includedPathIncludes){
0564 
0565           // Add the includingYamPath as an includedBy of this path
0566           Set<String> pathIncludes = includedBy.get(path);
0567           ifpathIncludes == null) {
0568             // Make a new set if there wasn't one
0569             pathIncludes = new HashSet<String>();
0570             includedBy.put(includedPath, pathIncludes);
0571           }
0572           pathIncludes.add(includingYamPath);
0573 
0574         }
0575       }
0576 
0577 
0578       log.debug("Debugging includes - added transitive includedBy for "
0579               "parameter: " + includedPath);
0580       log.debug("includes = " + includes);
0581       log.debug("includedBy = " + includedBy);
0582                   
0583     }
0584 
0585     log.debug("Debugging includes -  added new includedBy for all parameters" );
0586     log.debug("includes = " + includes);
0587     log.debug("includedBy = " + includedBy);
0588 
0589   }
0590 
0591 
0592   /**
0593    <p>Remove all information about includes from a given YamFile path. I.e.
0594    * remove all includes from that YamFile to others in the includes Map, and
0595    * remove all inverse "includedBy" Map relations. Doesn't remove includes to
0596    * the YamFile.</p>
0597    <p> Note that this method is not synchronized: it must be synchonized by
0598    * calling methods.
0599    </p>
0600    @param includingYamPath The canonical path of a YamFile for which all
0601    * includes originating in that file will be removed
0602    */
0603   private void removeIncludes(String includingYamPath) {
0604 
0605     log.debug("dependencies.removeIncludes(" + includingYamPath + ")");
0606 
0607     // Get any existing includes, and remove their inverses in includedBy
0608     Set<String> allIncluded = includes.get(includingYamPath);
0609     if (allIncluded != null) {
0610       for(String included : allIncluded) {
0611         Set<String> currentIncludedBy = includedBy.get(included);
0612         if (currentIncludedBy!=null) {
0613           currentIncludedBy.remove(includingYamPath);
0614 
0615           // If we now have an empty Set of includedBy values,
0616           // remove from the Map.
0617           if(currentIncludedBy.isEmpty()) {
0618             includedBy.remove(included);
0619           }
0620         }
0621       }
0622     }
0623 
0624     // Now remove the entry from includes
0625     includes.remove(includingYamPath);
0626 
0627   }
0628 
0629   /**
0630    <p>Rename all keys and values in the includes record of this Dependencies,
0631    * from an old File name to a new File name.</p>
0632    <p> Note that this method is not synchronized: it must be synchonized by
0633    * calling methods.
0634    </p>
0635    @param oldName The old name of the File
0636    @param newName The new name of the File
0637    */
0638   private void renameIncludes(String oldName, String newName) {
0639 
0640     log.debug("dependencies.renameIncludes(" + oldName + ", " + newName + ")");
0641 
0642     // Get any existing includes and remove the old key. Keep the values,
0643     // so we can later change inverses
0644     Set<String> allIncluded = includes.remove(oldName);
0645 
0646     // Now put in the new name, and go through the inverses
0647     if (allIncluded != null) {
0648       includes.put(newName, allIncluded);
0649       for(String included : allIncluded) {
0650         Set<String> currentIncludedBy = includedBy.get(included);
0651         if (currentIncludedBy != null) {
0652           currentIncludedBy.remove(oldName);
0653           currentIncludedBy.add(newName);
0654         }
0655       }
0656     }
0657 
0658     // Now looked for includedBys and remove the old key. Keep the values,
0659     // so we can later change inverses
0660     Set<String> allIncluders = includedBy.get(oldName);
0661     includedBy.remove(oldName);
0662 
0663     // Now put in the new name, and go through the inverses
0664     if(allIncluders != null) {
0665       includedBy.put(newName, allIncluders);
0666       for(String includer : allIncluders) {
0667         Set<String> currentIncludes = includes.get(includer);
0668         if(currentIncludes != null) {
0669           currentIncludes.remove(oldName);
0670           currentIncludes.add(newName);
0671         }
0672       }
0673     }
0674 
0675 
0676   }
0677 
0678 
0679   /**
0680    <p>Update the Dependencies with the fact that a YamFile links to a list of
0681    * files. This list is taken to be all the links and the only links from the
0682    * YamFile. Any previously recorded links for this YamFile are removed.</p>
0683    *
0684    <p>Because the end of a link does not affect the actual content of a
0685    * linking file, transitivity of links is not considered, and therefore
0686    * neither is circular linking.</p>
0687    <p> Note that this method is not synchronized: it must be synchonized by
0688    * calling methods.
0689    </p>
0690    *
0691    @param linkingYamPath The path of the YamFile that links to other Files
0692    @param linkedPaths     A List of other File canonical paths that are
0693    * linked to by linkingYam
0694    */
0695   private void linksTo(String linkingYamPath, List<String> linkedPaths) {
0696 
0697    log.debug("dependencies.linksTo(" + linkingYamPath + ", "
0698                + linkedPaths + ")");
0699     
0700     // A file has links to others.
0701     // We might already have link information for this file: we need
0702     // to remove it first.
0703 
0704     // Get any existing linksTo, and remove their inverses in linkedBy
0705     Set<String> allLinked = linksTo.get(linkingYamPath);
0706     if (allLinked != null) {
0707       for(String linked : allLinked) {
0708         Set<String> currentLinkedBy = linkedBy.get(linked);
0709         if (currentLinkedBy!=null) {
0710           currentLinkedBy.remove(linkingYamPath);
0711 
0712           // If we now have an empty Set of linkedBy values,
0713           // remove from the Map.
0714           if(currentLinkedBy.isEmpty()) {
0715            linkedBy.remove(linked);
0716           }
0717         }
0718       }
0719     }
0720 
0721     // Replace the linksTo with our new links - or remove the entry in
0722     // linksTo if there are no links
0723     if(linkedPaths.isEmpty()) {
0724       linksTo.remove(linkingYamPath);
0725     else {
0726       linksTo.put(linkingYamPath, new HashSet<String>(linkedPaths));
0727     }
0728 
0729     // Put all the inverses to our new links in linkedBy
0730     for (String linkedYam : linkedPaths) {
0731       Set<String> currentLinkedBy = linkedBy.get(linkedYam);
0732       if (currentLinkedBy == null) {
0733         currentLinkedBy = new HashSet<String>();
0734         linkedBy.put(linkedYam, currentLinkedBy);
0735       }
0736       currentLinkedBy.add(linkingYamPath);
0737 
0738     }
0739 
0740   }
0741 
0742 
0743   /**
0744    <p>
0745    * Remove all information about links from a given YamFile path. I.e. remove
0746    * all links from that YamFile to others in the linksTo Map, and remove all
0747    * inverse "linkedBy" Map relations. Doesn't remove links to the YamFile.</p>
0748    <p> Note that this method is not synchronized: it must be synchonized by
0749    * calling methods.
0750    </p>
0751    @param linkingYamPath The canonical path of a YamFile for which all links
0752    * originating in that file will be removed
0753    */
0754   private void removeLinks(String linkingYamPath) {
0755 
0756     log.debug("dependencies.removeLinks(" + linkingYamPath + ")");
0757 
0758     // Get any existing linksTo, and remove their inverses in linkedBy
0759     Set<String> allLinked = linksTo.get(linkingYamPath);
0760     if (allLinked != null) {
0761       for(String linked : allLinked) {
0762         Set<String> currentLinkedBy = linkedBy.get(linked);
0763         if (currentLinkedBy!=null) {
0764           currentLinkedBy.remove(linkingYamPath);
0765 
0766           // If we now have an empty Set of linkedBy values,
0767           // remove from the Map.
0768           if(currentLinkedBy.isEmpty()) {
0769             linkedBy.remove(linked);
0770           }
0771         }
0772       }
0773     }
0774 
0775     // Now remove the entry from linksTo
0776     linksTo.remove(linkingYamPath);
0777 
0778   }
0779 
0780   /**
0781    <p>
0782    * Rename all keys and values in the link record of this Dependencies, from
0783    * an old File name to a new File name.</p>
0784    <p> Note that this method is not synchronized: it must be synchonized by
0785    * calling methods.
0786    </p>
0787    @param oldName The old name of the File
0788    @param newName The new name of the File
0789    */
0790   private void renameLinks(String oldName, String newName) {
0791 
0792     log.debug("dependencies.renameLinks(" + oldName + ", " + newName + ")");
0793 
0794     // Get any existing linksTo and remove the old key. Keep the values,
0795     // so we can later change inverses
0796     Set<String> allLinked = linksTo.remove(oldName);   
0797 
0798     // Now put in the new name, and go through the inverses
0799     if (allLinked != null) {
0800       linksTo.put(newName, allLinked);
0801       for(String linked : allLinked) {
0802         Set<String> currentLinkedBy = linkedBy.get(linked);
0803         if (currentLinkedBy != null) {
0804           currentLinkedBy.remove(oldName);
0805           currentLinkedBy.add(newName);
0806         }
0807       }
0808     }
0809 
0810     // Now looked for linkedBys and remove the old key. Keep the values,
0811     // so we can later change inverses
0812     Set<String> allLinkers = linkedBy.get(oldName);
0813     linkedBy.remove(oldName);
0814 
0815     // Now put in the new name, and go through the inverses
0816     if(allLinkers != null) {
0817       linkedBy.put(newName, allLinkers);
0818       for(String linker : allLinkers) {
0819         Set<String> currentLinksTo = linksTo.get(linker);
0820         if(currentLinksTo != null) {
0821           currentLinksTo.remove(oldName);
0822           currentLinksTo.add(newName);
0823         }
0824       }
0825     }
0826 
0827 
0828   }
0829 
0830   /**
0831    <p>Given a modified YamFile, returns a Set of the canonical paths of those
0832    * YamFiles that need regenerating, because they depend on the modified
0833    * YamFile. If the modified YamFile has a canonical path that causes an
0834    * Exception, then the files that need regenerating are not defined, and an
0835    * empty Set is returned.</p>
0836    <p> Updates this Dependencies' record of links and includes for YamFile,
0837    * to take into account the modification.
0838    </p>
0839    <p> This method is synchronised internally.</p>
0840    <p>Note that there is no equivalent modified(File) method for modifying a
0841    * File that is not a YamFile, as they cannot be modified via the wiki</p>
0842    @param modifiedYam The YamFile that has been modified
0843    @return A Set of YamFiles that need regenerating as a result of the
0844    * modification, empty if modifiedFile has an erroneous canonical path.
0845    */
0846   public Set<String> modified(YamFile modifiedYam) {
0847 
0848     log.info("dependencies.modified(" + modifiedYam + ")");
0849 
0850     Set<String> toRegenerate = new HashSet<String>();
0851     try {
0852       String yamPath = modifiedYam.getCanonicalPath();
0853       log.debug("YamFile.getCanonicalPath(): " + yamPath);
0854 
0855       synchronized (this){
0856         // If a file has been modified, then we don't need to regenerate files
0857         // that link to it. They can still link. But we do need to regenerate
0858         // files that include it: they could have changed.
0859         Set<String> inc = includedBy.get(yamPath);
0860         if(inc != nulltoRegenerate.addAll(inc);
0861 
0862         log.debug("dependencies.modified(" + yamPath + ") to regenerate: ");
0863         iflog.isDebugEnabled() )
0864           for(String s : toRegenerate)
0865             log.debug("\t" + s);
0866 
0867         // If a YamFile has been modified, then we need to tell this
0868         // Dependencies what its linksTo and includes are now.
0869         // NB getLinks and getIncludes return null if the YamFile
0870         // has not had a parse tree generated, or if the parent
0871         // directory cannot be resolved.
0872         linksTo(yamPath, modifiedYam.getLinks());
0873         includes(yamPath, modifiedYam.getIncludes());
0874       }
0875 
0876     catch (GateException ge) {
0877       // log the exception, but do nothing else
0878       logEventMethodException("modified", modifiedYam.toString());
0879     }
0880 
0881     log.debug("To regenerate: " + toRegenerate);
0882     log.debug("Dependencies:\n" this);
0883     return toRegenerate;
0884   }
0885 
0886   /**
0887    <p>A non-YamFile File has been "created" as far as the wiki is concerned if
0888    * it has been uploaded.</p>
0889    <p>Given a created File, returns a Set of the canonical paths of those
0890    * YamFiles that need regenerating, because they depend on the created File.
0891    * If the created File has a canonical path that causes an IOException, then
0892    * the files that need regenerating are not defined, and an empty Set is
0893    * returned.</p>
0894    <p> This method is synchronised internally.</p>
0895    @param createdFile The File that has been created
0896    @return A Set of YamFiles that need regenerating as a result of the
0897    * creation, empty if createdFile has an erroneous canonical path.
0898    */
0899   public Set<String> created(File createdFile) {
0900 
0901     log.info("dependencies.created(non-yam file: " + createdFile + ")");
0902 
0903     Set<String> toRegenerate = new HashSet<String>();
0904     try {
0905       String filePath = createdFile.getCanonicalPath();
0906       
0907       synchronized (this){
0908         // If a File has been created / uploaded, then we need to (a) regenerate
0909         // any YamFiles that included it (i.e. previously had an include
0910         // statement to a non-existent File) (b) regenerate any linking
0911         // YamFiles, so that the link is now to the File rather than the create
0912         // page.
0913         Set<String> inc = includedBy.get(filePath);
0914         if(inc != nulltoRegenerate.addAll(inc);
0915         Set<String> lnk = linkedBy.get(filePath);
0916         if(lnk != nulltoRegenerate.addAll(lnk);
0917 
0918 
0919         // If a non-YamFile File has been created / uploaded, then it won't
0920         // itself contain any links or includes that need to be updated in this
0921         // Dependencies.
0922       }
0923       
0924 
0925     catch (IOException ioe) {
0926       // log the exception, but do nothing else
0927       logEventMethodException("created", createdFile.toString());
0928     }
0929 
0930      
0931     log.debug("To regenerate: " + toRegenerate);
0932     log.debug("Dependencies:\n" this);
0933     return toRegenerate;
0934 
0935   }
0936 
0937   /**
0938    <p>Given a created YamFile, returns a Set of the canonical paths of those
0939    * YamFiles that need regenerating, because they depend on the created
0940    * YamFile. If the created YamFile has a canonical path that causes an
0941    * Exception, then the files that need regenerating are not defined, and an
0942    * empty Set is returned.</p>
0943    <p> Updates this Dependencies' record of links and includes for YamFile,
0944    * to take into account the creation.<p>
0945    <p> This method is synchronised internally.</p>
0946    @param createdYam The YamFile that has been created
0947    @return A Set of YamFiles that need regenerating as a result of the
0948    * creation, empty if createdFile has an erroneous canonical path.
0949    */
0950   public Set<String> created(YamFile createdYam) {
0951 
0952     log.info("dependencies.created(yam file:" + createdYam + ")");
0953 
0954     Set<String> toRegenerate = new HashSet<String>();
0955     try {
0956       String yamPath = createdYam.getCanonicalPath();
0957       String htmlPath = createdYam.getHtmlFile().getCanonicalPath();
0958 
0959       synchronized (this){
0960         // If a new YAM file is created, then we must take into account that
0961         // other pages previously linked, or included, a HTML file of the same
0962         // name. So first, we must update all maps to replace the HTML with
0963         // the YAM name.
0964         renameLinks(htmlPath, yamPath);
0965 
0966         // If a file has been created, then we need to (a) regenerate files that
0967         // included it (i.e. previously had an include statement to a
0968         // non-existent file) (b) regenerate any linking files, so that the link
0969         // is now to the file rather than the create page.
0970         Set<String> inc = includedBy.get(yamPath);
0971         if(inc != nulltoRegenerate.addAll(inc);
0972         Set<String> lnk = linkedBy.get(yamPath);
0973         if(lnk != nulltoRegenerate.addAll(lnk);
0974 
0975         // If a YamFile has been created, then we need to tell this
0976         // Dependencies what its linksTo and includes are.
0977         // NB getLinks and getIncludes return null if the YamFile
0978         // has not had a parse tree generated, or if the parent
0979         // directory cannot be resolved.
0980         linksTo(yamPath, createdYam.getLinks());
0981         includes(yamPath, createdYam.getIncludes());
0982       }
0983     catch (IOException ioe) {
0984       // log the exception, but do nothing else
0985       logEventMethodException("created", createdYam.toString());
0986     catch (GateException ge) {
0987       // log the exception, but do nothing else
0988       logEventMethodException("created", createdYam.toString());
0989     }
0990 
0991 
0992     log.debug("To regenerate: " + toRegenerate);
0993     log.debug("Dependencies:\n" this);
0994     return toRegenerate;
0995 
0996   }
0997 
0998   /**
0999    <p>Given a deleted File, returns a Set of the canonical paths of those
1000    * YamFiles that need regenerating, because they depend on the deleted File.
1001    * If the deleted File has a canonical path that causes an IOException, then
1002    * the files that need regenerating are not defined, and an empty Set is
1003    * returned.</p>
1004    <p> This method is synchronised internally.</p>
1005    @param deletedFile The File that has been deleted
1006    @return A Set of YamFiles that need regenerating as a result of the
1007    * deletion, empty if deletedFile has an erroneous canonical path.
1008    */
1009   public Set<String> deleted(File deletedFile) {
1010 
1011     log.info("dependencies.deleted(" + deletedFile + ")");
1012 
1013     Set<String> toRegenerate = new HashSet<String>();
1014     try {
1015       String filePath = deletedFile.getCanonicalPath();
1016 
1017       synchronized (this){
1018         // If a file is deleted, then we need to (a) regenerate any file that
1019         // includes it, (b) regenerate any linking files, so that the link is
1020         // now to the create page rather than the file
1021         Set<String> inc = includedBy.get(filePath);
1022         if(inc != nulltoRegenerate.addAll(inc);
1023         Set<String> lnk = linkedBy.get(filePath);
1024         if(lnk != nulltoRegenerate.addAll(lnk);
1025 
1026         // If a non-YamFile File has been deleted, then it won't itself
1027         // have contained any links or includes that need to be updated in this
1028         // Dependencies. But other files might have linked to it. However,
1029         // they should retain their links to the non-existent file - that's
1030         // allowed.
1031       }
1032 
1033     catch (IOException ioe) {
1034       // log the exception, but do nothing else
1035       logEventMethodException("deleted", deletedFile.toString());
1036     }
1037 
1038     log.debug("To regenerate: " + toRegenerate);
1039     log.debug("Dependencies:\n" this);
1040     return toRegenerate;
1041 
1042   }
1043 
1044   /**
1045    <p>Given a deleted YamFile, returns a Set of the canonical paths of those
1046    * YamFiles that need regenerating, because they depend on the deleted
1047    * YamFile. If the deleted YamFile has a canonical path that causes an
1048    * Exception, then the files that need regenerating are not defined, and an
1049    * empty Set is returned.</p>
1050    <p> Updates this Dependencies' record of links and includes for YamFile,
1051    * to take into account the deletion.<p>
1052    <p> This method is synchronised internally.</p>
1053    @param deletedYam The YamFile that has been deleted
1054    @return A Set of YamFiles that need regenerating as a result of the
1055    * deletion, empty if deletedFile has an erroneous canonical path.
1056    */
1057   public Set<String> deleted(YamFile deletedYam) {
1058 
1059     log.info("dependencies.deleted(" + deletedYam + ")");
1060 
1061     Set<String> toRegenerate = new HashSet<String>();
1062     try {
1063       String yamPath = deletedYam.getCanonicalPath();
1064 
1065       synchronized (this) {
1066         // If a file is deleted, then we need to (a) regenerate any file that
1067         // includes it, (b) regenerate any linking files, so that the link is
1068         // now to the create page rather than the file
1069         Set<String> inc = includedBy.get(yamPath);
1070         if(inc != nulltoRegenerate.addAll(inc);
1071         Set<String> lnk = linkedBy.get(yamPath);
1072         if(lnk != nulltoRegenerate.addAll(lnk);
1073 
1074         // If a YamFile has been deleted, then we need to remove its linksTo and
1075         // includes, and any inverses. Note that we don't remove any linksTo it,
1076         // and their inverses. Any linksTo the deleted file will now point to
1077         // a non-existent file, which is allowed.
1078         removeLinks(yamPath);
1079         removeIncludes(yamPath);
1080       }
1081      
1082     catch (GateException ge) {
1083       // log the exception, but do nothing else
1084       logEventMethodException("deleted", deletedYam.toString());
1085     }
1086     log.debug("To regenerate: " + toRegenerate);
1087     log.debug("Dependencies:\n" this);
1088     return toRegenerate;
1089 
1090   }
1091 
1092   /**
1093    <p>Given the old name and new name of a renamed File, returns a Set of the
1094    * canonical paths of those YamFiles that need regenerating, because they
1095    * depend on the renamed File. If the old or new File has a canonical path
1096    * that causes an IOException, then the files that need regenerating are not
1097    * defined, and an empty Set is returned.</p>
1098    <p> Updates this Dependencies' record of links and includes for the File,
1099    * to take into account the renaming.</p>
1100    <p> This method is synchronised internally.</p>
1101    @param oldFile The File that has been renamed
1102    @param newFile The new File
1103    @return A Set of YamFiles that need regenerating as a result of the
1104    * renaming, empty if renamedFile has an erroneous canonical path.
1105    */
1106   public Set<String> renamed(File oldFile, File newFile) {
1107 
1108     log.info("dependencies.renamed(" + oldFile + ", " + newFile + ")");
1109 
1110     Set<String> toRegenerate = new HashSet<String>();
1111     try {
1112       String oldFilePath = oldFile.getCanonicalPath();
1113       String newFilePath = newFile.getCanonicalPath();
1114 
1115       synchronized (this) {
1116         // If a file is renamed, then the files we need to regenerate are given
1117         // by both linkedBy and updaters.
1118         Set<String> inc = includedBy.get(oldFilePath);
1119         if(inc != nulltoRegenerate.addAll(inc);
1120         Set<String> lnk = linkedBy.get(oldFilePath);
1121         if(lnk != nulltoRegenerate.addAll(lnk);
1122 
1123         // If a non-YamFile File has been renamed, then it won't itself
1124         // contain any links or includes that need to be updated in this
1125         // Dependencies. But, links and includes to it will need to use the
1126         // new Path
1127         renameLinks(oldFilePath, newFilePath);
1128         renameIncludes(oldFilePath, newFilePath);
1129       }
1130 
1131 
1132     catch (IOException ioe) {
1133       // log the exception, but do nothing else
1134       logEventMethodException("renamed", oldFile + ", " + newFile);
1135     }
1136 
1137     log.debug("To regenerate: " + toRegenerate);
1138     log.debug("Dependencies:\n" this);
1139     return toRegenerate;
1140 
1141   }
1142 
1143   /**
1144    <p>Given the old name and new name of a renamed YamFile, returns a Set of
1145    * the canonical paths of those YamFiles that need regenerating, because they
1146    * depend on the renamed YamFile. If the old or new YamFile has a canonical
1147    * path that causes an Exception, then the files that need regenerating are
1148    * not defined, and an empty Set is returned.</p>
1149    <p> Updates this Dependencies' record of links and includes for YamFile,
1150    * to take into account the renaming.</p>
1151    <p> This method is synchronised internally.</p>
1152    @param oldYam The YamFile that has been renamed
1153    @param newYam The new YamFile
1154    @return A Set of YamFiles that need regenerating as a result of the
1155    * renaming, empty if renamedYam has an erroneous canonical path.
1156    */
1157   public Set<String> renamed(YamFile oldYam, YamFile newYam) {
1158 
1159     log.info("dependencies.renamed(" + oldYam + ", " + newYam + ")");
1160 
1161     Set<String> toRegenerate = new HashSet<String>();
1162     try {
1163       String oldYamPath = oldYam.getCanonicalPath();
1164       String newYamPath = newYam.getCanonicalPath();
1165 
1166       synchronized (this) {
1167         // If a file is renamed, then the files we need to regenerate are given
1168         // by both linkedBy and updaters.
1169         Set<String> inc = includedBy.get(oldYamPath);
1170         if(inc != nulltoRegenerate.addAll(inc);
1171         Set<String> lnk = linkedBy.get(oldYamPath);
1172         if(lnk != nulltoRegenerate.addAll(lnk);
1173 
1174         // If a YamFile has been renamed, then we need to change all links and
1175         // includes
1176         renameLinks(oldYamPath, newYamPath);
1177         renameIncludes(oldYamPath, newYamPath);
1178       }
1179 
1180     catch (GateException ge) {
1181       // log the exception, but do nothing else
1182       logEventMethodException("renamed", oldYam + ", " + newYam);
1183     }
1184 
1185     log.debug("To regenerate: " + toRegenerate);
1186     log.debug("Dependencies:\n" this);
1187     return toRegenerate;
1188 
1189   }
1190 
1191   /**
1192    * Log an Exception caught by one of the event methods
1193    @param method The name of the method
1194    @param paramString String representation of the method parameters
1195    */
1196   private void logEventMethodException(String method, String paramString) {
1197 
1198     log.warn("dependencies." + method + "(" + paramString
1199                                          ") caught a GateException");
1200     log.warn("dependencies." + method + "(" + paramString
1201                                           ") could not update dependencies");
1202     log.warn("dependencies." + method + "(" + paramString
1203                                          ") returned no files to regenerate, "
1204                                          "possibly erroneously");
1205   }
1206 
1207   /**
1208    * Create a String representation of a links or includes Map.
1209    @param map The links or includes Map for which a String will be created
1210    @return The String representaiton of map.
1211    */
1212   private String mapToStringMap<String, Set<String>> map) {
1213 
1214     StringBuilder build = new StringBuilder();
1215 
1216     List<String> keys = new ArrayList<String>(map.keySet());
1217     Collections.sort(keys);
1218     for(String key : keys) {
1219       build.append(key);
1220       build.append":[");
1221       List<String> vals = new ArrayList<String>(map.get(key));
1222       Collections.sort(vals);
1223       forString val : vals) {
1224         build.append(val);
1225         build.append(",");
1226       }
1227       // remove the final comma
1228       build.deleteCharAt(build.length() 1);
1229       build.append("];");
1230     }
1231     // remove the final semi-colon
1232     build.deleteCharAt(build.length() 1);
1233 
1234     return build.toString();
1235 
1236   }
1237 
1238   /**
1239    * Create a String representation of the "linksTo" relationships in this
1240    * Dependencies. Intended for checking internal state when testing.
1241    @return The String representation of "linksTo"
1242    */
1243   public String linksToAsString(){
1244     return mapToString(linksTo);
1245   }
1246 
1247   /**
1248    * Create a String representation of the "includes" relationships in this
1249    * Dependencies. Intended for checking internal state when testing.
1250    @return The String representation of "includes"
1251    */
1252   public String includesAsString(){
1253     return mapToString(includes);
1254   }
1255 
1256   /**
1257    * Create a String representation of the "linkedBy" relationships in this
1258    * Dependencies. Intended for checking internal state when testing.
1259    @return The String representation of "linkedBy"
1260    */
1261   public String linkedByAsString(){
1262     return mapToString(linkedBy);
1263   }
1264 
1265   /**
1266    * Create a String representation of the "includedBy" relationships in this
1267    * Dependencies. Intended for checking internal state when testing.
1268    @return The String representation of "includedBy"
1269    */
1270   public String includedByAsString(){
1271     return mapToString(includedBy);
1272   }
1273 
1274 
1275 }