PackageGappTask.java
001 /*
002  *  Copyright (c) 1995-2010, The University of Sheffield. See the file
003  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  *
005  *  This file is part of GATE (see http://gate.ac.uk/), and is free
006  *  software, licenced under the GNU Library General Public License,
007  *  Version 2, June 1991 (in the distribution as file licence.html,
008  *  and also available at http://gate.ac.uk/gate/licence.html).
009  *
010  *  Ian Roberts, 10/02/2009
011  *
012  *  $Id: PackageGappTask.java 13373 2011-01-29 15:25:08Z ian_roberts $
013  */
014 package gate.util.ant.packager;
015 
016 import gate.util.Files;
017 import gate.util.persistence.PersistenceManager;
018 
019 import java.io.File;
020 import java.io.IOException;
021 import java.net.MalformedURLException;
022 import java.net.URL;
023 import java.util.ArrayList;
024 import java.util.Comparator;
025 import java.util.HashMap;
026 import java.util.HashSet;
027 import java.util.Iterator;
028 import java.util.LinkedHashMap;
029 import java.util.List;
030 import java.util.Map;
031 import java.util.Set;
032 import java.util.SortedMap;
033 import java.util.TreeMap;
034 import java.util.TreeSet;
035 
036 import org.apache.tools.ant.BuildException;
037 import org.apache.tools.ant.Project;
038 import org.apache.tools.ant.Task;
039 import org.apache.tools.ant.taskdefs.Copy;
040 import org.apache.tools.ant.taskdefs.Property;
041 import org.apache.tools.ant.types.FileSet;
042 import org.apache.tools.ant.types.Path;
043 import org.apache.tools.ant.types.PatternSet.NameEntry;
044 import org.apache.tools.ant.util.FileUtils;
045 import org.jdom.Document;
046 import org.jdom.Element;
047 import org.jdom.JDOMException;
048 import org.jdom.input.SAXBuilder;
049 import org.jdom.xpath.XPath;
050 
051 /**
052  * Ant task to copy a gapp file, rewriting any relative paths it
053  * contains to point within the same directory as the target file
054  * location and copy the referenced files into the right locations. The
055  * resulting structure is self-contained and can be packaged up (e.g. in
056  * a zip file) to send to a third party.
057  
058  @author Ian Roberts
059  */
060 public class PackageGappTask extends Task {
061   
062   /**
063    * Comparator to compare URLs by lexicographic ordering of their
064    * getPath() values.  <code>null</code> compares less-than anything
065    * not <code>null</code>.
066    */
067   public static final Comparator<URL> PATH_COMPARATOR = new Comparator<URL>() {
068     public int compare(URL a, URL b) {
069       if(a == null) {
070         return (b == null: -1;
071       }
072       if(b == null) {
073         return 1;
074       }
075       return a.getPath().compareTo(b.getPath());
076     }
077   };
078 
079   /**
080    * The file into which the modified gapp will be written.
081    */
082   private File destFile;
083 
084   /**
085    * The original file containing the gapp to package.
086    */
087   private File src;
088 
089   /**
090    * The location of the GATE home directory.  Only required if the GAPP file
091    * to be packaged contains URLs relative to $gatehome$.
092    */
093   private File gateHome;
094 
095   /**
096    * Should we copy the complete contents of referenced plugin
097    * directories into the right place relative to the destFile? If not,
098    * only the creole.xmls, any JARs they directly include, and directly
099    * referenced resource files will be copied. Anything else needs to be
100    * declared in an &lt;extrafiles&gt; sub-element.
101    */
102   private boolean copyPlugins = true;
103 
104   /**
105    * Should we copy the complete contents of the parent directories of
106    * any referenced resource files? If true, whenever the gapp
107    * references a resource file <code>f</code> we will also include
108    * the whole contents of <code>f.getParentFile()</code>.
109    */
110   private boolean copyResourceDirs = false;
111   
112   /**
113    * Path-like structure listing extra resources that should be packaged
114    * with the gapp, as if they had been referenced by relpaths from
115    * within the gapp file. Their target locations are determined by the
116    * plugins and mapping hints in the usual way. Typically this would be
117    * used for other resource files that are not referenced directly by
118    * the gapp file but are referenced indirectly by the PRs in the
119    * application (e.g. the .lst files corresponding to a gazetteer
120    * .def).
121    */
122   private List<Path> extraResourcesPaths = new ArrayList<Path>();
123 
124   /**
125    * Enumeration of the actions to take when there are unresolved
126    * resources. Options are to fail the build, to make the paths
127    * absolute in the new gapp, or to recover by gathering the unresolved
128    * files into an "application-resources" directory.
129    */
130   public static enum UnresolvedAction {
131     fail, absolute, recover
132   }
133 
134   /**
135    * The action to take when there are unresolved resources. By default,
136    * unresolved resources will fail the build.
137    */
138   private UnresolvedAction onUnresolved = UnresolvedAction.fail;
139 
140   /**
141    * List of mapping hint sub-elements.
142    */
143   private List<MappingHint> hintTasks = new ArrayList<MappingHint>();
144 
145   /**
146    * Map of mapping hints.  This is an insertion-ordered LinkedHashMap, so
147    * where two hints could apply to the same path, the one specified first in
148    * the configuration wins.
149    */
150   private Map<URL, String> mappingHints = new LinkedHashMap<URL, String>();
151 
152   /**
153    * Get the destination file to which the modified gapp will be
154    * written.
155    */
156   public File getDestFile() {
157     return destFile;
158   }
159 
160   /**
161    * Set the destination file to which the modified gapp will be
162    * written.
163    */
164   public void setDestFile(File destFile) {
165     this.destFile = destFile;
166   }
167 
168   /**
169    * Get the original gapp file that is to be modified.
170    */
171   public File getSrc() {
172     return src;
173   }
174 
175   /**
176    * Set the location of the original gapp file which is to be modified.
177    */
178   public void setSrc(File src) {
179     this.src = src;
180   }
181 
182   /**
183    * Get the location of the GATE home directory, used to resolve $gatehome$
184    * relative paths in the GAPP file.
185    */
186   public File getGateHome() {
187     return gateHome;
188   }
189 
190   /**
191    * Set the location of the GATE home directory, used to resolve $gatehome$
192    * relative paths in the GAPP file.
193    */
194   public void setGateHome(File gateHome) {
195     this.gateHome = gateHome;
196   }
197 
198   /**
199    * Will the task copy the complete contents of referenced plugins into
200    * the target location?
201    */
202   public boolean isCopyPlugins() {
203     return copyPlugins;
204   }
205 
206   /**
207    * Will the task copy the complete contents of referenced plugins into
208    * the target location? If false, only the bare minimum will be copied
209    * (the creole.xml files, any JARs referenced therein, and any
210    * directly referenced resource files). Anything extra must be copied
211    * in separately, typically with extra &lt;copy&gt; tasks after the
212    * &lt;packagegapp&gt; one.
213    */
214   public void setCopyPlugins(boolean copyPlugins) {
215     this.copyPlugins = copyPlugins;
216   }
217 
218   /**
219    * Will the task copy the complete contents of directories containing
220    * referenced resources into the target location or just the
221    * referenced resources themselves?
222    */
223   public boolean isCopyResourceDirs() {
224     return copyResourceDirs;
225   }
226 
227   /**
228    * Will the task copy the complete contents of directories containing
229    * referenced resources into the target location? By default it does
230    * not do this, but only includes the directly-referenced resource
231    * files - for example, if the gapp refers to a <code>.def</code>
232    * file defining gazetteer lists, the lists themselves will not be
233    * included. If copyResourceDirs is false, the additional resources
234    * will need to be included using an appropriate
235    * &lt;extraresourcespath&gt;.
236    */
237   public void setCopyResourceDirs(boolean copyResourceDirs) {
238     this.copyResourceDirs = copyResourceDirs;
239   }
240 
241   /**
242    * Get the action performed when there are unresolved resources.
243    */
244   public UnresolvedAction getOnUnresolved() {
245     return onUnresolved;
246   }
247 
248   /**
249    * What should we do if there are unresolved relpaths within the gapp
250    * file? By default the build will fail, but instead you can opt to
251    * have the relative paths replaced by absolute paths to the same URL,
252    * or to have the task recover by putting the files into an
253    * "application-resources" directory.
254    */
255   public void setOnUnresolved(UnresolvedAction onUnresolved) {
256     this.onUnresolved = onUnresolved;
257   }
258 
259   /**
260    * Create and add the representation for a nested &lt;hint from="X"
261    * to="Y" /&gt; element.
262    */
263   public MappingHint createHint() {
264     MappingHint hint = new MappingHint();
265     hint.setProject(this.getProject());
266     hint.setTaskName(this.getTaskName());
267     hint.setLocation(this.getLocation());
268     hint.init();
269     hintTasks.add(hint);
270     return hint;
271   }
272 
273   /**
274    * Add a path containing extra resources that should be treated as if
275    * they had been referenced by relpaths within the gapp file. The
276    * locations to which these extra resources will be copied are
277    * determined by the plugins and mapping hints in the usual way.
278    */
279   public void addExtraResourcesPath(Path path) {
280     extraResourcesPaths.add(path);
281   }
282 
283   @Override
284   public void execute() throws BuildException {
285     // process the hints
286     for(MappingHint h : hintTasks) {
287       h.perform();
288     }
289 
290     // map to store the necessary file copy operations
291     Map<URL, URL> fileCopyMap = new HashMap<URL, URL>();
292     Map<URL, URL> dirCopyMap = new HashMap<URL, URL>();
293     TreeMap<URL, URL> pluginCopyMap = new TreeMap<URL, URL>(PATH_COMPARATOR);
294 
295     log("Packaging gapp file " + src);
296     // do the work
297     GappModel gappModel = null;
298     URL newFileURL = null;
299     try {
300       URL gateHomeURL = null;
301       // convert gateHome to a URL, if it was provided
302       if(gateHome != null) {
303         gateHomeURL = gateHome.toURI().toURL();
304       }
305       gappModel = new GappModel(src.toURI().toURL(), gateHomeURL);
306       newFileURL = destFile.toURI().toURL();
307       gappModel.setGappFileURL(newFileURL);
308     }
309     catch(MalformedURLException e) {
310       throw new BuildException("Couldn't convert src or dest file to URL", e,
311               getLocation());
312     }
313 
314     // we use TreeSet for these sets so we will process the paths
315     // higher up the directory tree before paths pointing to their
316     // subdirectories.
317     Set<URL> plugins = new TreeSet<URL>(PATH_COMPARATOR);
318     plugins.addAll(gappModel.getPluginURLs());
319     Set<URL> resources = new TreeSet<URL>(PATH_COMPARATOR);
320     resources.addAll(gappModel.getResourceURLs());
321 
322     // process the extraresourcespath elements (if any)
323     processExtraResourcesPaths(resources);
324 
325     // first look at the explicit mapping hints
326     if(mappingHints != null && !mappingHints.isEmpty()) {
327       Iterator<URL> resourcesIt = resources.iterator();
328       while(resourcesIt.hasNext()) {
329         URL resource = resourcesIt.next();
330         for(URL hint : mappingHints.keySet()) {
331           String hintString = hint.toExternalForm();
332           if(resource.equals(hint)
333                   || (hintString.endsWith("/"&& resource.toExternalForm()
334                           .startsWith(hintString))) {
335             // found this resource under this hint
336             log("Found resource " + resource + " under mapping hint URL "
337                     + hint, Project.MSG_VERBOSE);
338             String hintTarget = mappingHints.get(hint);
339             URL newResourceURL = null;
340             if(hintTarget == null) {
341               // hint asks to map to an absolute URL
342               log("  Converting to absolute URL", Project.MSG_VERBOSE);
343               newResourceURL = resource;
344             }
345             else {
346               // relativize the URL against the hint source and
347               // construct the new URL relative to the hint target
348               try {
349                 URL mappedHint = new URL(newFileURL, hintTarget);
350                 String resourceRelpath =
351                         PersistenceManager.getRelativePath(hint, resource);
352                 newResourceURL = new URL(mappedHint, resourceRelpath);
353                 fileCopyMap.put(resource, newResourceURL);
354                 if(copyResourceDirs) {
355                   dirCopyMap.put(new URL(resource, ".")new URL(
356                           newResourceURL, "."));
357                 }
358               }
359               catch(MalformedURLException e) {
360                 throw new BuildException("Couldn't construct URL relative to "
361                         + hintTarget + " for " + resource, e, getLocation());
362               }
363               log("  Relocating to " + newResourceURL, Project.MSG_VERBOSE);
364             }
365             // do the relocation
366             gappModel.updatePathForURL(resource, newResourceURL,
367                     hintTarget != null);
368             // we've now dealt with this resource, so don't need to
369             // handle it later
370             resourcesIt.remove();
371             break;
372           }
373         }
374       }
375     }
376 
377     // Any resources that aren't covered by the hints, try and
378     // resolve them relative to the plugins referenced by the
379     // application.
380     Iterator<URL> pluginsIt = plugins.iterator();
381     while(pluginsIt.hasNext()) {
382       URL pluginURL = pluginsIt.next();
383       pluginsIt.remove();
384       URL newPluginURL = null;
385       
386       String pluginName = pluginURL.getFile();
387       log("Processing plugin " + pluginName, Project.MSG_VERBOSE);
388 
389       // first check whether this plugin is a subdirectory of another plugin
390       // we have already processed
391       SortedMap<URL, URL> possibleAncestors = pluginCopyMap.headMap(pluginURL);
392       URL ancestorPlugin = null;
393       if(!possibleAncestors.isEmpty()) ancestorPlugin = possibleAncestors.lastKey();
394       if(ancestorPlugin != null && pluginURL.toExternalForm().startsWith(
395               ancestorPlugin.toExternalForm())) {
396         // this plugin is under one we have already dealt with
397         log("  Plugin is located under another plugin " + ancestorPlugin, Project.MSG_VERBOSE);
398         String relPath = PersistenceManager.getRelativePath(ancestorPlugin, pluginURL);
399         try {
400           newPluginURL = new URL(pluginCopyMap.get(ancestorPlugin), relPath);
401         }
402         catch(MalformedURLException e) {
403           throw new BuildException("Couldn't construct URL relative to plugins/"
404                   " for " + pluginURL, e, getLocation());
405         }
406       }
407       else {
408         // normal case, this plugin is not a subdir of another plugin
409         boolean addSlash = false;
410         // we will map the plugin whose directory name is X to plugins/X
411         if(pluginName.endsWith("/")) {
412           addSlash = true;
413           pluginName = pluginName.substring(
414                   pluginName.lastIndexOf('/', pluginName.length() 21,
415                   pluginName.length() 1);
416         }
417         else {
418           pluginName = pluginName.substring(pluginName.lastIndexOf('/'1);
419         }
420         log("  Plugin name is " + pluginName, Project.MSG_VERBOSE);
421         try {
422           newPluginURL = new URL(newFileURL, "plugins/" + pluginName + (addSlash ? "/" ""));
423           // a gapp may refer to two or more plugins with the same name.
424           // If plugins/{pluginName} is already taken, try
425           // plugins/{pluginName}-2, plugins/{pluginName}-3, etc.,
426           // until we find one that is available.
427           if(pluginCopyMap.containsValue(newPluginURL)) {
428             int index = 2;
429             do {
430               newPluginURL =
431                       new URL(newFileURL, "plugins/" + pluginName + "-"
432                               (index++(addSlash ? "/" ""));
433             while(pluginCopyMap.containsValue(newPluginURL));
434           }
435         }
436         catch(MalformedURLException e) {
437           throw new BuildException("Couldn't construct URL relative to plugins/"
438                   " for " + pluginURL, e, getLocation());
439         }
440       }
441       log("  Relocating to " + newPluginURL, Project.MSG_VERBOSE);
442 
443       // deal with the plugin URL itself (in the urlList)
444       gappModel.updatePathForURL(pluginURL, newPluginURL, true);
445       pluginCopyMap.put(pluginURL, newPluginURL);
446 
447       // now look for resources located under that plugin
448       String pluginUri = pluginURL.toExternalForm();
449       if(!pluginUri.endsWith("/")) {
450         pluginUri += "/";
451       }
452       Iterator<URL> resourcesIt = resources.iterator();
453       while(resourcesIt.hasNext()) {
454         URL resourceURL = resourcesIt.next();
455         try {
456           if(resourceURL.toExternalForm().startsWith(
457                   pluginUri)) {
458             // found a resource under this plugin, so relocate it to be
459             // under the re-located plugin dir
460             resourcesIt.remove();
461             String resourceRelpath =
462                     PersistenceManager.getRelativePath(pluginURL, resourceURL);
463             log("    Found resource " + resourceURL, Project.MSG_VERBOSE);
464             URL newResourceURL = null;
465             newResourceURL = new URL(newPluginURL, resourceRelpath);
466             log("    Relocating to " + newResourceURL, Project.MSG_VERBOSE);
467             gappModel.updatePathForURL(resourceURL, newResourceURL, true);
468             fileCopyMap.put(resourceURL, newResourceURL);
469             if(copyResourceDirs) {
470               dirCopyMap.put(new URL(resourceURL, ".")new URL(newResourceURL,
471                       "."));
472             }
473           }
474         }
475         catch(MalformedURLException e) {
476           throw new BuildException("Couldn't construct URL relative to "
477                   + newPluginURL + " for " + resourceURL, e, getLocation());
478         }
479       }
480     }
481 
482     // anything left over, handle according to onUnresolved
483     if(!resources.isEmpty()) {
484       switch(onUnresolved) {
485         case fail:
486           // easy case - fail the build
487           log("There were unresolved resources:", Project.MSG_ERR);
488           for(URL res : resources) {
489             log(res.toExternalForm(), Project.MSG_ERR);
490           }
491           log("Either set onUnresolved=\"absolute|recover\" or add the "
492                   "relevant mapping hints", Project.MSG_ERR);
493           throw new BuildException("There were unresolved resources",
494                   getLocation());
495 
496         case absolute:
497           // convert all unresolved resources to absolute URLs
498           log("There were unresolved resources, which have been made absolute",
499                   Project.MSG_WARN);
500           for(URL res : resources) {
501             gappModel.updatePathForURL(res, res, false);
502             log(res.toExternalForm(), Project.MSG_VERBOSE);
503           }
504           break;
505 
506         case recover:
507           // the clever case - recover by putting all the unresolved
508           // resources into subdirectories of an "application-resources"
509           // directory under the output dir
510           URL unresolvedResourcesDir = null;
511           try {
512             unresolvedResourcesDir =
513                     new URL(newFileURL, "application-resources/");
514           }
515           catch(MalformedURLException e) {
516             throw new BuildException("Can't construct URL relative to "
517                     + newFileURL + " for application-resources", e,
518                     getLocation());
519           }
520           // map to track where under application-resources we should map
521           // each directory that contains unresolved resources
522           TreeMap<URL, URL> unresolvedResourcesSubDirs = new TreeMap<URL, URL>(PATH_COMPARATOR);
523           log("There were unresolved resources, which have been gathered into "
524                   + unresolvedResourcesDir, Project.MSG_INFO);
525           for(URL res : resources) {
526             URL resourceDir = null;
527             try {
528               resourceDir = new URL(res, ".");
529             }
530             catch(MalformedURLException e) {
531               throw new BuildException(
532                       "Can't construct URL to parent directory of " + res, e,
533                       getLocation());
534             }
535             URL targetDir =
536                     getUnresolvedResourcesTarget(unresolvedResourcesSubDirs,
537                             unresolvedResourcesDir, resourceDir);
538             String resName = res.getFile();
539             resName = resName.substring(resName.lastIndexOf('/'1);
540             URL newResourceURL = null;
541             try {
542               newResourceURL = new URL(targetDir, resName);
543             }
544             catch(MalformedURLException e) {
545               throw new BuildException("Can't construct URL relative to "
546                       + unresolvedResourcesDir + " for " + resName, e,
547                       getLocation());
548             }
549             gappModel.updatePathForURL(res, newResourceURL, true);
550             fileCopyMap.put(res, newResourceURL);
551             if(copyResourceDirs) {
552               dirCopyMap.put(resourceDir, targetDir);
553             }
554           }
555           break;
556 
557         default:
558           throw new BuildException("Unrecognised UnresolvedAction",
559                   getLocation());
560       }
561     }
562 
563     // write out the fixed GAPP file
564     try {
565       log("Writing modified gapp to " + destFile);
566       gappModel.write();
567     }
568     catch(IOException e) {
569       throw new BuildException("Error writing out modified GAPP file", e,
570               getLocation());
571     }
572 
573     // now copy the files that it references
574     if(fileCopyMap.size() 0) {
575       log("Copying " + fileCopyMap.size() " resources");
576     }
577     for(Map.Entry<URL, URL> resEntry : fileCopyMap.entrySet()) {
578       File source = Files.fileFromURL(resEntry.getKey());
579       File dest = Files.fileFromURL(resEntry.getValue());
580       if(source.isDirectory()) {
581         // source URL points to a directory, so create a corresponding
582         // directory dest
583         dest.mkdirs();
584       }
585       else {
586         // source URL doesn't point to a directory, so
587         // ensure parent directory exists
588         dest.getParentFile().mkdirs();
589         if(source.isFile()) {
590           // source URL points to an existing file, copy it
591           try {
592             log("Copying " + source + " to " + dest, Project.MSG_VERBOSE);
593             FileUtils.getFileUtils().copyFile(source, dest);
594           }
595           catch(IOException e) {
596             throw new BuildException(
597                     "Error copying file " + source + " to " + dest, e,
598                     getLocation());
599           }
600         }
601       }
602     }
603 
604     // handle the plugins
605     if(pluginCopyMap.size() 0) {
606       log("Copying " + pluginCopyMap.size() " plugins");
607       if(copyPlugins) {
608         log("Also copying complete plugin contents", Project.MSG_VERBOSE);
609       }
610     }
611     copyDirectories(pluginCopyMap, !copyPlugins);
612 
613     // handle extra directories
614     if(dirCopyMap.size() 0) {
615       log("Copying " + dirCopyMap.size() " resource directories");
616     }
617     copyDirectories(dirCopyMap, false);
618   }
619 
620   /**
621    * Process any extraresourcespath elements provided to this task and
622    * include the resources they refer to in the given set.
623    */
624   private void processExtraResourcesPaths(Set<URL> resources) {
625     for(Path p : extraResourcesPaths) {
626       for(String resource : p.list()) {
627         File resourceFile = new File(resource);
628         try {
629           resources.add(resourceFile.toURI().toURL());
630         }
631         catch(MalformedURLException e) {
632           throw new BuildException("Couldn't construct URL for extra resource "
633                   + resourceFile, e, getLocation());
634         }
635       }
636     }
637   }
638 
639   /**
640    * Copy directories as specified by the given map.
641    
642    @param copyMap map specifying the directories to copy and the
643    *          target locations to which they should be copied.
644    @param minimalPlugin if true, treat the directory as a GATE plugin
645    *          and copy just the minimal files needed for the plugin to
646    *          work (creole.xml and any referenced jars).
647    */
648   private void copyDirectories(Map<URL, URL> copyMap, boolean minimalPlugin) {
649     for(Map.Entry<URL, URL> copyEntry : copyMap.entrySet()) {
650       File source = Files.fileFromURL(copyEntry.getKey());
651       if(!source.exists()) {
652         return;
653       }
654       File dest = Files.fileFromURL(copyEntry.getValue());
655       // set up a copy task to do the copying
656       Copy copyTask = new Copy();
657       copyTask.setProject(getProject());
658       copyTask.setLocation(getLocation());
659       copyTask.setTaskName(getTaskName());
660       copyTask.setTodir(dest);
661       // ensure the target directory exists
662       dest.mkdirs();
663       FileSet fileSet = new FileSet();
664       copyTask.addFileset(fileSet);
665       fileSet.setDir(source);
666       if(minimalPlugin) {
667         // just copy creole.xml and JARs
668         NameEntry include = fileSet.createInclude();
669         include.setName("creole.xml");
670         URL creoleXml;
671         try {
672           creoleXml =
673                   new URL(copyEntry.getKey().toExternalForm() "/creole.xml");
674         }
675         catch(MalformedURLException e) {
676           throw new BuildException(
677                   "Error creating URL for creole.xml in plugin "
678                           + copyEntry.getKey());
679         }
680         for(String jarString : getJars(creoleXml)) {
681           NameEntry jarInclude = fileSet.createInclude();
682           jarInclude.setName(jarString);
683         }
684       }
685 
686       // do the copying
687       copyTask.init();
688       copyTask.perform();
689     }
690   }
691 
692   /**
693    * Extract the text values from any &lt;JAR&gt; elements contained in
694    * the referenced creole.xml file.
695    
696    @return a set with one element for each unique &lt;JAR&gt; entry in
697    *         the given creole.xml.
698    */
699   private Set<String> getJars(URL creoleXml) {
700     try {
701       Set<String> jars = new HashSet<String>();
702       // the XPath is a bit ugly, but needed to match the element name
703       // case-insensitively.
704       XPath jarXPath =
705               XPath
706                       .newInstance("//*[translate(local-name(), 'ajr', 'AJR') = 'JAR']");
707       SAXBuilder builder = new SAXBuilder();
708       Document creoleDoc = builder.build(creoleXml);
709       // technically unsafe, but we know that the above XPath expression
710       // can only match elements.
711       List<Element> jarElts = jarXPath.selectNodes(creoleDoc);
712       for(Element e : jarElts) {
713         jars.add(e.getTextTrim());
714       }
715 
716       return jars;
717     }
718     catch(JDOMException e) {
719       throw new BuildException("Error extracting JAR elements from "
720               + creoleXml, e, getLocation());
721     }
722     catch(IOException e) {
723       throw new BuildException("Error loading " + creoleXml
724               " to extract JARs", e, getLocation());
725     }
726   }
727 
728   /**
729    * Get a URL for a directory to which the given (unresolved) resource
730    * directory should be mapped.
731    
732    @param unresolvedResourcesSubDirs a map from URLs of directories
733    *          containing unresolved resources to the URLs under the
734    *          target unresolved-resources directory that they will be
735    *          mapped to. This map is updated by this method.
736    @param unresolvedResourcesDir the top-level application-resources
737    *          directory in the target location.
738    @param resourceDir a directory containing an unresolved resource.
739    @return the URL under application-resources to which this directory
740    *         should be mapped. For a resourceDir of the form .../foo,
741    *         the returned URL would typically be
742    *         &lt;applicationResourcesDir&gt;/foo, but if a different
743    *         directory with the same name has already been mapped then
744    *         we will return the first ..../foo-2, foo-3, etc. that is
745    *         not already in use.  If one of the directory's ancestors
746    *         has already been mapped then we return a URL pointing
747    *         to the same relative path inside that ancestor's mapping,
748    *         e.g. if .../foo has already been mapped to a-r/foo-2 then
749    *         .../foo/bar/baz will map to a-r/foo-2/bar/baz.
750    */
751   private URL getUnresolvedResourcesTarget(
752           TreeMap<URL, URL> unresolvedResourcesSubDirs, URL unresolvedResourcesDir,
753           URL resourceDirthrows BuildException {
754     URL targetDir = unresolvedResourcesSubDirs.get(resourceDir);
755     try {
756       if(targetDir == null) {
757         // no exact match, try an ancestor match
758         SortedMap<URL, URL> possibleAncestors = unresolvedResourcesSubDirs.headMap(resourceDir);
759         URL nearestAncestor = null;
760         if(!possibleAncestors.isEmpty()) nearestAncestor = possibleAncestors.lastKey();
761         if(nearestAncestor != null && resourceDir.toExternalForm().startsWith(
762                 nearestAncestor.toExternalForm())) {
763           // found an ancestor mapping, so take the relative path
764           // from the ancestor to this dir and map it to the same
765           // path under the ancestor's mapping.
766           String relPath = PersistenceManager.getRelativePath(nearestAncestor, resourceDir);
767           targetDir = new URL(unresolvedResourcesSubDirs.get(nearestAncestor), relPath);
768         }
769         else {
770           // no ancestors currently mapped, so start a new sub-dir of
771           // unresolvedResourcesDir whose name is the last path
772           // component of the source URL
773           String resourcePath = resourceDir.getFile();
774           if(resourcePath.endsWith("/")) {
775             resourcePath = resourcePath.substring(0, resourcePath.length() 1);
776           }
777           String targetDirName =
778                   resourcePath.substring(resourcePath.lastIndexOf('/'1);
779           if(targetDirName.length() == 0) {
780             // edge case, if the source URL points to the root directory "/"
781             targetDirName = "resources";
782           }
783           // try application-resources/{targetDirName} as the target
784           targetDir = new URL(unresolvedResourcesDir, targetDirName + "/");
785           // if this is already taken, try
786           // application-resources/{targetDirName}-2,
787           // application-resources/{targetDirName}-3, etc., until we find
788           // one that is available.
789           if(unresolvedResourcesSubDirs.containsValue(targetDir)) {
790             int index = 2;
791             do {
792               targetDir =
793                       new URL(unresolvedResourcesDir, targetDirName + "-"
794                               (index++"/");
795             while(unresolvedResourcesSubDirs.containsValue(targetDir));
796           }
797         }
798         
799         // store the mapping for future use
800         unresolvedResourcesSubDirs.put(resourceDir, targetDir);
801       }
802     }
803     catch(MalformedURLException e) {
804       throw new BuildException("Can't construct target URL for directory "
805               + resourceDir, e, getLocation());
806     }
807 
808     return targetDir;
809   }
810 
811   /**
812    * Class to represent a nested <code>hint</code> element. Typically
813    * this will be a simple <code>&lt;hint from="X" to="Y" /&gt;</code>
814    * but the MappingHint class actually extends the property task, so it
815    * can read hints in Properties-file format using
816    <code>&lt;hint file="hints.properties" /&gt;</code>.
817    */
818   public class MappingHint extends Property {
819     private boolean absolute = false;
820 
821     public void setFrom(File from) {
822       super.setName(from.getAbsolutePath());
823     }
824 
825     public void setTo(String to) {
826       super.setValue(to);
827     }
828 
829     /**
830      * Should files matching this hint be made absolute? If true, the
831      * "to" value is ignored.
832      */
833     public void setAbsolute(boolean absolute) {
834       if(absolute) {
835         super.setValue("dummy");
836       }
837       this.absolute = absolute;
838     }
839 
840     /**
841      * Rather than adding properties to the project, add mapping hints
842      * to the task.
843      */
844     @Override
845     protected void addProperty(String n, String v) {
846       try {
847         // resolve relative paths against project basedir
848         File source = getProject().resolveFile(n);
849         // add a trailing slash to the hint target if necessary
850         if(source.isDirectory() && v != null && !v.endsWith("/")) {
851           v += "/";
852         }
853         mappingHints.put(source.toURI().toURL(), absolute ? null : v);
854       }
855       catch(MalformedURLException e) {
856         PackageGappTask.this.log("Couldn't interpret \"" + n
857                 "\" as a file path, ignored", Project.MSG_WARN);
858       }
859     }
860 
861   }
862 }