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 wikiID) throws 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 = (Dependencies) ois.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 wikiID) throws 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 wikiID) throws 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 wikiID) throws 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 == null) return false;
0351 if (!(obj instanceof Dependencies)) return false;
0352
0353 Dependencies other = (Dependencies) obj;
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 if( includers != null) {
0507 for(String includer : includers) {
0508 Set<String> includesToUpdate = includes.get(includer);
0509 if( includesToUpdate == 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 for( String includedPath : includedPaths) {
0537
0538 // First, add the includingYamPath into includedBy for this one
0539 Set<String> currentIncludedBy = includedBy.get(includedPath);
0540 if( currentIncludedBy == 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 if( pathIncludes == 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 != null) toRegenerate.addAll(inc);
0861
0862 log.debug("dependencies.modified(" + yamPath + ") to regenerate: ");
0863 if( log.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 != null) toRegenerate.addAll(inc);
0915 Set<String> lnk = linkedBy.get(filePath);
0916 if(lnk != null) toRegenerate.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 != null) toRegenerate.addAll(inc);
0972 Set<String> lnk = linkedBy.get(yamPath);
0973 if(lnk != null) toRegenerate.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 != null) toRegenerate.addAll(inc);
1023 Set<String> lnk = linkedBy.get(filePath);
1024 if(lnk != null) toRegenerate.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 != null) toRegenerate.addAll(inc);
1071 Set<String> lnk = linkedBy.get(yamPath);
1072 if(lnk != null) toRegenerate.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 != null) toRegenerate.addAll(inc);
1120 Set<String> lnk = linkedBy.get(oldFilePath);
1121 if(lnk != null) toRegenerate.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 != null) toRegenerate.addAll(inc);
1171 Set<String> lnk = linkedBy.get(oldYamPath);
1172 if(lnk != null) toRegenerate.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 mapToString( Map<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 for( String 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 }
|