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) ? 0 : -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 <extrafiles> 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 <copy> tasks after the
212 * <packagegapp> 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 * <extraresourcespath>.
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 <hint from="X"
261 * to="Y" /> 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() - 2) + 1,
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 <JAR> elements contained in
694 * the referenced creole.xml file.
695 *
696 * @return a set with one element for each unique <JAR> 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 * <applicationResourcesDir>/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 resourceDir) throws 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><hint from="X" to="Y" /></code>
814 * but the MappingHint class actually extends the property task, so it
815 * can read hints in Properties-file format using
816 * <code><hint file="hints.properties" /></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 }
|