source: other-projects/trunk/gs3-release-maker/apache-ant-1.6.5/src/main/org/apache/tools/ant/taskdefs/optional/unix/Symlink.java@ 14627

Last change on this file since 14627 was 14627, checked in by oranfry, 17 years ago

initial import of the gs3-release-maker

File size: 28.7 KB
Line 
1/*
2 * Copyright 2002-2005 The Apache Software Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18/*
19 * Since the initial version of this file was developed on the clock on
20 * an NSF grant I should say the following boilerplate:
21 *
22 * This material is based upon work supported by the National Science
23 * Foundaton under Grant No. EIA-0196404. Any opinions, findings, and
24 * conclusions or recommendations expressed in this material are those
25 * of the author and do not necessarily reflect the views of the
26 * National Science Foundation.
27 */
28
29package org.apache.tools.ant.taskdefs.optional.unix;
30
31import java.io.File;
32import java.io.IOException;
33import java.io.FileInputStream;
34import java.io.FileOutputStream;
35import java.io.FileNotFoundException;
36
37import java.util.Vector;
38import java.util.Properties;
39import java.util.Enumeration;
40import java.util.Hashtable;
41
42import org.apache.tools.ant.Task;
43import org.apache.tools.ant.BuildException;
44import org.apache.tools.ant.DirectoryScanner;
45
46import org.apache.tools.ant.util.FileUtils;
47
48import org.apache.tools.ant.types.FileSet;
49
50import org.apache.tools.ant.taskdefs.Execute;
51
52/**
53 * Creates, Records and Restores Symlinks.
54 *
55 * <p> This task performs several related operations. In the most trivial
56 * and default usage, it creates a link specified in the link attribute to
57 * a resource specified in the resource attribute. The second usage of this
58 * task is to traverse a directory structure specified by a fileset,
59 * and write a properties file in each included directory describing the
60 * links found in that directory. The third usage is to traverse a
61 * directory structure specified by a fileset, looking for properties files
62 * (also specified as included in the fileset) and recreate the links
63 * that have been previously recorded for each directory. Finally, it can be
64 * used to remove a symlink without deleting the file or directory it points
65 * to.
66 *
67 * <p> Examples of use:
68 *
69 * <p> Make a link named &quot;foo&quot; to a resource named
70 * &quot;bar.foo&quot; in subdir:
71 * <pre>
72 * &lt;symlink link=&quot;${dir.top}/foo&quot; resource=&quot;${dir.top}/subdir/bar.foo&quot;/&gt;
73 * </pre>
74 *
75 * <p> Record all links in subdir and its descendants in files named
76 * &quot;dir.links&quot;
77 * <pre>
78 * &lt;symlink action=&quot;record&quot; linkfilename=&quot;dir.links&quot;&gt;
79 * &lt;fileset dir=&quot;${dir.top}&quot; includes=&quot;subdir&#47;**&quot; /&gt;
80 * &lt;/symlink&gt;
81 * </pre>
82 *
83 * <p> Recreate the links recorded in the previous example:
84 * <pre>
85 * &lt;symlink action=&quot;recreate&quot;&gt;
86 * &lt;fileset dir=&quot;${dir.top}&quot; includes=&quot;subdir&#47;**&#47;dir.links&quot; /&gt;
87 * &lt;/symlink&gt;
88 * </pre>
89 *
90 * <p> Delete a link named &quot;foo&quot; to a resource named
91 * &quot;bar.foo&quot; in subdir:
92 * <pre>
93 * &lt;symlink action=&quot;delete&quot; link=&quot;${dir.top}/foo&quot;/&gt;
94 * </pre>
95 *
96 * <p><strong>LIMITATIONS:</strong> Because Java has no direct support for
97 * handling symlinks this task divines them by comparing canonical and
98 * absolute paths. On non-unix systems this may cause false positives.
99 * Furthermore, any operating system on which the command
100 * <code>ln -s link resource</code> is not a valid command on the command line
101 * will not be able to use action= &quot;delete&quot;, action=&quot;single&quot;
102 * or action=&quot;recreate&quot;, but action=&quot;record&quot; should still
103 * work. Finally, the lack of support for symlinks in Java means that all links
104 * are recorded as links to the <strong>canonical</strong> resource name.
105 * Therefore the link: <code>link --> subdir/dir/../foo.bar</code> will be
106 * recorded as <code>link=subdir/foo.bar</code> and restored as
107 * <code>link --> subdir/foo.bar</code>.
108 *
109 */
110public class Symlink extends Task {
111
112 // Attributes with setter methods:
113 private String resource;
114 private String link;
115 private String action;
116 private Vector fileSets = new Vector();
117 private String linkFileName;
118 private boolean overwrite;
119 private boolean failonerror;
120
121 private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
122
123 /**
124 * Initialize the task.
125 * @throws BuildException on error.
126 */
127 public void init() throws BuildException {
128 super.init();
129 failonerror = true; // default behavior is to fail on an error
130 overwrite = false; // default behavior is to not overwrite
131 action = "single"; // default behavior is make a single link
132 fileSets = new Vector();
133 }
134
135 /**
136 * The standard method for executing any task.
137 * @throws BuildException on error.
138 */
139 public void execute() throws BuildException {
140 try {
141 if (action.equals("single")) {
142 doLink(resource, link);
143 } else if (action.equals("delete")) {
144 try {
145 log("Removing symlink: " + link);
146 Symlink.deleteSymlink(link);
147 } catch (FileNotFoundException fnfe) {
148 handleError(fnfe.toString());
149 } catch (IOException ioe) {
150 handleError(ioe.toString());
151 }
152 } else if (action.equals("recreate")) {
153 Properties listOfLinks;
154 Enumeration keys;
155
156 if (fileSets.size() == 0) {
157 handleError("File set identifying link file(s) "
158 + "required for action recreate");
159 return;
160 }
161 listOfLinks = loadLinks(fileSets);
162
163 keys = listOfLinks.keys();
164
165 while (keys.hasMoreElements()) {
166 link = (String) keys.nextElement();
167 resource = listOfLinks.getProperty(link);
168 // handle the case where the link exists
169 // and points to a directory (bug 25181)
170 try {
171 File test = new File(link);
172 File testRes = new File(resource);
173 if (!FILE_UTILS.isSymbolicLink(test.getParentFile(),
174 test.getName())) {
175 doLink(resource, link);
176 } else {
177 if (!test.getCanonicalPath().
178 equals(testRes.getCanonicalPath())) {
179 Symlink.deleteSymlink(link);
180 doLink(resource, link);
181 } // else the link exists, do nothing
182 }
183 } catch (IOException ioe) {
184 handleError("IO exception while creating "
185 + "link");
186 }
187 }
188 } else if (action.equals("record")) {
189 Vector vectOfLinks;
190 Hashtable byDir = new Hashtable();
191 Enumeration links, dirs;
192
193 if (fileSets.size() == 0) {
194 handleError("Fileset identifying links to "
195 + "record required");
196 return;
197 }
198 if (linkFileName == null) {
199 handleError("Name of file to record links in "
200 + "required");
201 return;
202 }
203 // fill our vector with file objects representing
204 // links (canonical)
205 vectOfLinks = findLinks(fileSets);
206
207 // create a hashtable to group them by parent directory
208 links = vectOfLinks.elements();
209 while (links.hasMoreElements()) {
210 File thisLink = (File) links.nextElement();
211 String parent = thisLink.getParent();
212 if (byDir.containsKey(parent)) {
213 ((Vector) byDir.get(parent)).addElement(thisLink);
214 } else {
215 byDir.put(parent, new Vector());
216 ((Vector) byDir.get(parent)).addElement(thisLink);
217 }
218 }
219 // write a Properties file in each directory
220 dirs = byDir.keys();
221 while (dirs.hasMoreElements()) {
222 String dir = (String) dirs.nextElement();
223 Vector linksInDir = (Vector) byDir.get(dir);
224 Properties linksToStore = new Properties();
225 Enumeration eachlink = linksInDir.elements();
226 File writeTo;
227
228 // fill up a Properties object with link and resource
229 // names
230 while (eachlink.hasMoreElements()) {
231 File alink = (File) eachlink.nextElement();
232 try {
233 linksToStore.put(alink.getName(),
234 alink.getCanonicalPath());
235 } catch (IOException ioe) {
236 handleError("Couldn't get canonical "
237 + "name of a parent link");
238 }
239 }
240 // Get a place to record what we are about to write
241 writeTo = new File(dir + File.separator
242 + linkFileName);
243
244 writePropertyFile(linksToStore, writeTo,
245 "Symlinks from " + writeTo.getParent());
246 }
247 } else {
248 handleError("Invalid action specified in symlink");
249 }
250 } finally {
251 // return all variables to their default state,
252 // ready for the next invocation.
253 resource = null;
254 link = null;
255 action = "single";
256 fileSets = new Vector();
257 linkFileName = null;
258 overwrite = false;
259 failonerror = true;
260 }
261 }
262
263 /* ********************************************************** *
264 * Begin Attribute Setter Methods *
265 * ********************************************************** */
266
267 /**
268 * The setter for the overwrite attribute. If set to false (default)
269 * the task will not overwrite existing links, and may stop the build
270 * if a link already exists depending on the setting of failonerror.
271 *
272 * @param owrite If true overwrite existing links.
273 */
274 public void setOverwrite(boolean owrite) {
275 this.overwrite = owrite;
276 }
277
278 /**
279 * The setter for the failonerror attribute. If set to true (default)
280 * the entire build fails upon error. If set to false, the error is
281 * logged and the build will continue.
282 *
283 * @param foe If true throw BuildException on error else log it.
284 */
285 public void setFailOnError(boolean foe) {
286 this.failonerror = foe;
287 }
288
289 /**
290 * Set the action to be performed. May be
291 * &quot;single&quot;, &quot;multi&quot; or &quot;record&quot;.
292 *
293 * @param typ The action to perform.
294 */
295 public void setAction(String typ) {
296 this.action = typ;
297 }
298
299 /**
300 * Set the same of the link.
301 * Only used when action = &quot;single&quot;.
302 *
303 * @param lnk The name for the link.
304 */
305 public void setLink(String lnk) {
306 this.link = lnk;
307 }
308
309 /**
310 * Set the name of the resource to which a link should be created.
311 * Only used when action = &quot;single&quot;.
312 *
313 * @param src The resource to be linked.
314 */
315 public void setResource(String src) {
316 this.resource = src;
317 }
318
319 /**
320 * Set the name of the file to which links will be written.
321 * Only used when action = &quot;record&quot;.
322 *
323 * @param lf The name of the file to write links to.
324 */
325 public void setLinkfilename(String lf) {
326 this.linkFileName = lf;
327 }
328
329 /**
330 * Add a fileset to this task.
331 *
332 * @param set The fileset to add.
333 */
334 public void addFileset(FileSet set) {
335 fileSets.addElement(set);
336 }
337
338 /* ********************************************************** *
339 * Begin Public Utility Methods *
340 * ********************************************************** */
341
342 /**
343 * Deletes a symlink without deleting the resource it points to.
344 *
345 * <p>This is a convenience method that simply invokes
346 * <code>deleteSymlink(java.io.File)</code>.
347 *
348 * @param path A string containing the path of the symlink to delete.
349 *
350 * @throws FileNotFoundException When the path results in a
351 * <code>File</code> that doesn't exist.
352 * @throws IOException If calls to <code>File.rename</code>
353 * or <code>File.delete</code> fail.
354 */
355 public static void deleteSymlink(String path)
356 throws IOException, FileNotFoundException {
357 deleteSymlink(new File(path));
358 }
359
360 /**
361 * Deletes a symlink without deleting the resource it points to.
362 *
363 * <p>This is a utility method that removes a unix symlink without removing
364 * the resource that the symlink points to. If it is accidentally invoked
365 * on a real file, the real file will not be harmed, but an exception
366 * will be thrown when the deletion is attempted. This method works by
367 * getting the canonical path of the link, using the canonical path to
368 * rename the resource (breaking the link) and then deleting the link.
369 * The resource is then returned to its original name inside a finally
370 * block to ensure that the resource is unharmed even in the event of
371 * an exception.
372 *
373 * @param linkfil A <code>File</code> object of the symlink to delete.
374 *
375 * @throws FileNotFoundException When the path results in a
376 * <code>File</code> that doesn't exist.
377 * @throws IOException If calls to <code>File.rename</code>,
378 * <code>File.delete</code> or
379 * <code>File.getCanonicalPath</code>
380 * fail.
381 */
382
383 public static void deleteSymlink(File linkfil)
384 throws IOException, FileNotFoundException {
385
386 if (!linkfil.exists()) {
387 throw new FileNotFoundException("No such symlink: " + linkfil);
388 }
389 // find the resource of the existing link:
390 String canstr = linkfil.getCanonicalPath();
391 File canfil = new File(canstr);
392
393 // rename the resource, thus breaking the link:
394 String parentStr = canfil.getParent();
395 File parentDir = new File(parentStr);
396
397 File temp = FILE_UTILS.createTempFile("symlink", ".tmp", parentDir);
398 temp.deleteOnExit();
399 try {
400 try {
401 FILE_UTILS.rename(canfil, temp);
402 } catch (IOException e) {
403 throw new IOException("Couldn't rename resource when "
404 + "attempting to delete " + linkfil);
405 }
406 // delete the (now) broken link:
407 if (!linkfil.delete()) {
408 throw new IOException("Couldn't delete symlink: " + linkfil
409 + " (was it a real file? is this not a "
410 + "UNIX system?)");
411 }
412 } finally {
413 // return the resource to its original name:
414 try {
415 FILE_UTILS.rename(temp, canfil);
416 } catch (IOException e) {
417 throw new IOException("Couldn't return resource " + temp
418 + " to its original name: " + canstr
419 + "\n THE RESOURCE'S NAME ON DISK HAS "
420 + "BEEN CHANGED BY THIS ERROR!\n");
421 }
422 }
423 }
424
425 /* ********************************************************** *
426 * Begin Private Methods *
427 * ********************************************************** */
428
429 /**
430 * Writes a properties file.
431 *
432 * This method use <code>Properties.store</code>
433 * and thus report exceptions that occur while writing the file.
434 *
435 * @param properties The properties object to be written.
436 * @param propertyfile The File to write to.
437 * @param comment The comment to place at the head of the file.
438 */
439
440 private void writePropertyFile(Properties properties,
441 File propertyfile,
442 String comment)
443 throws BuildException {
444
445 FileOutputStream fos = null;
446 try {
447 fos = new FileOutputStream(propertyfile);
448 properties.store(fos, comment);
449 } catch (IOException ioe) {
450 throw new BuildException(ioe, getLocation());
451 } finally {
452 FILE_UTILS.close(fos);
453 }
454 }
455
456 /**
457 * Handles errors correctly based on the setting of failonerror.
458 *
459 * @param msg The message to log, or include in the
460 * <code>BuildException</code>.
461 */
462 private void handleError(String msg) {
463 if (failonerror) {
464 throw new BuildException(msg);
465 }
466 log(msg);
467 }
468
469 /**
470 * Conducts the actual construction of a link.
471 *
472 * <p> The link is constructed by calling <code>Execute.runCommand</code>.
473 *
474 * @param resource The path of the resource we are linking to.
475 * @param link The name of the link we wish to make.
476 */
477
478 private void doLink(String resource, String link) throws BuildException {
479
480 if (resource == null) {
481 handleError("Must define the resource to symlink to!");
482 return;
483 }
484 if (link == null) {
485 handleError("Must define the link name for symlink!");
486 return;
487 }
488 File linkfil = new File(link);
489
490 String[] cmd = new String[] {"ln", "-s", resource, link};
491
492 try {
493 if (overwrite && linkfil.exists()) {
494 deleteSymlink(linkfil);
495 }
496 } catch (FileNotFoundException fnfe) {
497 handleError("Symlink disappeared before it was deleted: " + link);
498 } catch (IOException ioe) {
499 handleError("Unable to overwrite preexisting link: " + link);
500 }
501 log(cmd[0] + " " + cmd[1] + " " + cmd[2] + " " + cmd[3]);
502 Execute.runCommand(this, cmd);
503 }
504
505 /**
506 * Simultaneously get included directories and included files.
507 *
508 * @param ds The scanner with which to get the files and directories.
509 * @return A vector of <code>String</code> objects containing the
510 * included file names and directory names.
511 */
512 private Vector scanDirsAndFiles(DirectoryScanner ds) {
513 String[] files, dirs;
514 Vector list = new Vector();
515
516 ds.scan();
517
518 files = ds.getIncludedFiles();
519 dirs = ds.getIncludedDirectories();
520
521 for (int i = 0; i < files.length; i++) {
522 list.addElement(files[i]);
523 }
524 for (int i = 0; i < dirs.length; i++) {
525 list.addElement(dirs[i]);
526 }
527 return list;
528 }
529
530 /**
531 * Finds all the links in all supplied filesets.
532 *
533 * <p> This method is invoked when the action attribute is
534 * &quot;record&quot;. This means that filesets are interpreted
535 * as the directories in which links may be found.
536 *
537 * <p> The basic method followed here is, for each fileset:
538 * <ol>
539 * <li> Compile a list of all matches </li>
540 * <li> Convert matches to <code>File</code> objects </li>
541 * <li> Remove all non-symlinks using
542 * <code>FileUtils.isSymbolicLink</code> </li>
543 * <li> Convert all parent directories to the canonical form </li>
544 * <li> Add the remaining links from each file set to a
545 * master list of links unless the link is already recorded
546 * in the list</li>
547 * </ol>
548 *
549 * @param fileSets The filesets specified by the user.
550 * @return A Vector of <code>File</code> objects containing the
551 * links (with canonical parent directories).
552 */
553
554 private Vector findLinks(Vector fileSets) {
555 Vector result = new Vector();
556
557 // loop through the supplied file sets:
558 FSLoop: for (int i = 0; i < fileSets.size(); i++) {
559 FileSet fsTemp = (FileSet) fileSets.elementAt(i);
560 String workingDir = null;
561 Vector links = new Vector();
562 Vector linksFiles = new Vector();
563 Enumeration enumLinks;
564
565 DirectoryScanner ds;
566
567 File tmpfil = null;
568 try {
569 tmpfil = fsTemp.getDir(this.getProject());
570 workingDir = tmpfil.getCanonicalPath();
571 } catch (IOException ioe) {
572 handleError("Exception caught getting "
573 + "canonical path of working dir " + tmpfil
574 + " in a FileSet passed to the symlink "
575 + "task. Further processing of this "
576 + "fileset skipped");
577 continue FSLoop;
578 }
579 // Get a vector of String with file names that match the pattern:
580 ds = fsTemp.getDirectoryScanner(this.getProject());
581 links = scanDirsAndFiles(ds);
582
583 // Now convert the strings to File Objects
584 // using the canonical version of the working dir:
585 enumLinks = links.elements();
586
587 while (enumLinks.hasMoreElements()) {
588 linksFiles.addElement(new File(workingDir
589 + File.separator
590 + (String) enumLinks
591 .nextElement()));
592 }
593 // Now loop through and remove the non-links:
594
595 enumLinks = linksFiles.elements();
596
597 File parentNext, next;
598 String nameParentNext;
599 Vector removals = new Vector();
600 while (enumLinks.hasMoreElements()) {
601 next = (File) enumLinks.nextElement();
602 nameParentNext = next.getParent();
603
604 parentNext = new File(nameParentNext);
605 try {
606 if (!FILE_UTILS.isSymbolicLink(parentNext, next.getName())) {
607 removals.addElement(next);
608 }
609 } catch (IOException ioe) {
610 handleError("Failed checking " + next
611 + " for symbolic link. FileSet skipped.");
612 continue FSLoop;
613 // Otherwise this file will be falsely recorded as a link,
614 // if failonerror = false, hence the warn and skip.
615 }
616 }
617 enumLinks = removals.elements();
618
619 while (enumLinks.hasMoreElements()) {
620 linksFiles.removeElement(enumLinks.nextElement());
621 }
622 // Now we have what we want so add it to results, ensuring that
623 // no link is returned twice and we have a canonical reference
624 // to the link. (no symlinks in the parent dir)
625
626 enumLinks = linksFiles.elements();
627 while (enumLinks.hasMoreElements()) {
628 File temp, parent;
629 next = (File) enumLinks.nextElement();
630 try {
631 parent = new File(next.getParent());
632 parent = new File(parent.getCanonicalPath());
633 temp = new File(parent, next.getName());
634 if (!result.contains(temp)) {
635 result.addElement(temp);
636 }
637 } catch (IOException ioe) {
638 handleError("IOException: " + next + " omitted");
639 }
640 }
641 // Note that these links are now specified with a full
642 // canonical path irrespective of the working dir of the
643 // file set so it is ok to mix them in the same vector.
644 }
645 return result;
646 }
647
648 /**
649 * Load the links from a properties file.
650 *
651 * <p> This method is only invoked when the action attribute is set to
652 * &quot;multi&quot;. The filesets passed in are assumed to specify the
653 * names of the property files with the link information and the
654 * subdirectories in which to look for them.
655 *
656 * <p> The basic method follwed here is, for each file set:
657 * <ol>
658 * <li> Get the canonical version of the dir attribute </li>
659 * <li> Scan for properties files </li>
660 * <li> load the contents of each properties file found. </li>
661 * </ol>
662 *
663 * @param fileSets The <code>FileSet</code>s for this task
664 * @return The links to be made.
665 */
666 private Properties loadLinks(Vector fileSets) {
667 Properties finalList = new Properties();
668 Enumeration keys;
669 String key, value;
670 String[] includedFiles;
671
672 // loop through the supplied file sets:
673 FSLoop: for (int i = 0; i < fileSets.size(); i++) {
674 String workingDir;
675 FileSet fsTemp = (FileSet) fileSets.elementAt(i);
676
677 DirectoryScanner ds;
678
679 try {
680 File linelength = fsTemp.getDir(this.getProject());
681 workingDir = linelength.getCanonicalPath();
682 } catch (IOException ioe) {
683 handleError("Exception caught getting "
684 + "canonical path of working dir "
685 + "of a FileSet passed to symlink "
686 + "task. FileSet skipped.");
687 continue FSLoop;
688 }
689 ds = fsTemp.getDirectoryScanner(this.getProject());
690 ds.setFollowSymlinks(false);
691 ds.scan();
692 includedFiles = ds.getIncludedFiles();
693
694 // loop through the files identified by each file set
695 // and load their contents.
696 for (int j = 0; j < includedFiles.length; j++) {
697 File inc = new File(workingDir + File.separator
698 + includedFiles[j]);
699 String inDir;
700 Properties propTemp = new Properties();
701
702 try {
703 propTemp.load(new FileInputStream(inc));
704 inDir = inc.getParent();
705 inDir = (new File(inDir)).getCanonicalPath();
706 } catch (FileNotFoundException fnfe) {
707 handleError("Unable to find " + includedFiles[j]
708 + "FileSet skipped.");
709 continue FSLoop;
710 } catch (IOException ioe) {
711 handleError("Unable to open " + includedFiles[j]
712 + " or its parent dir"
713 + "FileSet skipped.");
714 continue FSLoop;
715 }
716 keys = propTemp.keys();
717 propTemp.list(System.out);
718 // Write the contents to our master list of links
719 // This method assumes that all links are defined in
720 // terms of absolute paths, or paths relative to the
721 // working directory
722 while (keys.hasMoreElements()) {
723 key = (String) keys.nextElement();
724 value = propTemp.getProperty(key);
725 finalList.put(inDir + File.separator + key, value);
726 }
727 }
728 }
729 return finalList;
730 }
731}
Note: See TracBrowser for help on using the repository browser.