001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: /*
020: * Since the initial version of this file was developed on the clock on
021: * an NSF grant I should say the following boilerplate:
022: *
023: * This material is based upon work supported by the National Science
024: * Foundaton under Grant No. EIA-0196404. Any opinions, findings, and
025: * conclusions or recommendations expressed in this material are those
026: * of the author and do not necessarily reflect the views of the
027: * National Science Foundation.
028: */
029:
030: package org.apache.tools.ant.taskdefs.optional.unix;
031:
032: import java.io.File;
033: import java.io.IOException;
034: import java.io.PrintStream;
035: import java.io.FileInputStream;
036: import java.io.FileOutputStream;
037: import java.io.BufferedInputStream;
038: import java.io.BufferedOutputStream;
039: import java.io.FileNotFoundException;
040:
041: import java.util.Vector;
042: import java.util.HashSet;
043: import java.util.Iterator;
044: import java.util.Hashtable;
045: import java.util.Properties;
046:
047: import org.apache.tools.ant.Project;
048: import org.apache.tools.ant.BuildException;
049: import org.apache.tools.ant.DirectoryScanner;
050: import org.apache.tools.ant.dispatch.DispatchTask;
051: import org.apache.tools.ant.dispatch.DispatchUtils;
052: import org.apache.tools.ant.taskdefs.Execute;
053: import org.apache.tools.ant.taskdefs.LogOutputStream;
054: import org.apache.tools.ant.types.FileSet;
055: import org.apache.tools.ant.types.Commandline;
056: import org.apache.tools.ant.util.FileUtils;
057:
058: /**
059: * Creates, Deletes, Records and Restores Symlinks.
060: *
061: * <p> This task performs several related operations. In the most trivial
062: * and default usage, it creates a link specified in the link attribute to
063: * a resource specified in the resource attribute. The second usage of this
064: * task is to traverse a directory structure specified by a fileset,
065: * and write a properties file in each included directory describing the
066: * links found in that directory. The third usage is to traverse a
067: * directory structure specified by a fileset, looking for properties files
068: * (also specified as included in the fileset) and recreate the links
069: * that have been previously recorded for each directory. Finally, it can be
070: * used to remove a symlink without deleting the associated resource.
071: *
072: * <p> Usage examples:
073: *
074: * <p> Make a link named "foo" to a resource named
075: * "bar.foo" in subdir:
076: * <pre>
077: * <symlink link="${dir.top}/foo" resource="${dir.top}/subdir/bar.foo"/>
078: * </pre>
079: *
080: * <p> Record all links in subdir and its descendants in files named
081: * "dir.links":
082: * <pre>
083: * <symlink action="record" linkfilename="dir.links">
084: * <fileset dir="${dir.top}" includes="subdir/**" />
085: * </symlink>
086: * </pre>
087: *
088: * <p> Recreate the links recorded in the previous example:
089: * <pre>
090: * <symlink action="recreate">
091: * <fileset dir="${dir.top}" includes="subdir/**/dir.links" />
092: * </symlink>
093: * </pre>
094: *
095: * <p> Delete a link named "foo" to a resource named
096: * "bar.foo" in subdir:
097: * <pre>
098: * <symlink action="delete" link="${dir.top}/foo"/>
099: * </pre>
100: *
101: * <p><strong>LIMITATIONS:</strong> Because Java has no direct support for
102: * handling symlinks this task divines them by comparing canonical and
103: * absolute paths. On non-unix systems this may cause false positives.
104: * Furthermore, any operating system on which the command
105: * <code>ln -s link resource</code> is not a valid command on the command line
106: * will not be able to use action="delete", action="single"
107: * or action="recreate", but action="record" should still
108: * work. Finally, the lack of support for symlinks in Java means that all links
109: * are recorded as links to the <strong>canonical</strong> resource name.
110: * Therefore the link: <code>link --> subdir/dir/../foo.bar</code> will be
111: * recorded as <code>link=subdir/foo.bar</code> and restored as
112: * <code>link --> subdir/foo.bar</code>.
113: *
114: */
115: public class Symlink extends DispatchTask {
116: private static final FileUtils FILE_UTILS = FileUtils
117: .getFileUtils();
118:
119: private String resource;
120: private String link;
121: private Vector fileSets = new Vector();
122: private String linkFileName;
123: private boolean overwrite;
124: private boolean failonerror;
125: private boolean executing = false;
126:
127: /**
128: * Initialize the task.
129: * @throws BuildException on error.
130: */
131: public void init() throws BuildException {
132: super .init();
133: setDefaults();
134: }
135:
136: /**
137: * The standard method for executing any task.
138: * @throws BuildException on error.
139: */
140: public synchronized void execute() throws BuildException {
141: if (executing) {
142: throw new BuildException(
143: "Infinite recursion detected in Symlink.execute()");
144: }
145: try {
146: executing = true;
147: DispatchUtils.execute(this );
148: } finally {
149: executing = false;
150: }
151: }
152:
153: /**
154: * Create a symlink.
155: * @throws BuildException on error.
156: * @since Ant 1.7
157: */
158: public void single() throws BuildException {
159: try {
160: if (resource == null) {
161: handleError("Must define the resource to symlink to!");
162: return;
163: }
164: if (link == null) {
165: handleError("Must define the link name for symlink!");
166: return;
167: }
168: doLink(resource, link);
169: } finally {
170: setDefaults();
171: }
172: }
173:
174: /**
175: * Delete a symlink.
176: * @throws BuildException on error.
177: * @since Ant 1.7
178: */
179: public void delete() throws BuildException {
180: try {
181: if (link == null) {
182: handleError("Must define the link name for symlink!");
183: return;
184: }
185: log("Removing symlink: " + link);
186: deleteSymlink(link);
187: } catch (FileNotFoundException fnfe) {
188: handleError(fnfe.toString());
189: } catch (IOException ioe) {
190: handleError(ioe.toString());
191: } finally {
192: setDefaults();
193: }
194: }
195:
196: /**
197: * Restore symlinks.
198: * @throws BuildException on error.
199: * @since Ant 1.7
200: */
201: public void recreate() throws BuildException {
202: try {
203: if (fileSets.isEmpty()) {
204: handleError("File set identifying link file(s) "
205: + "required for action recreate");
206: return;
207: }
208: Properties links = loadLinks(fileSets);
209:
210: for (Iterator kitr = links.keySet().iterator(); kitr
211: .hasNext();) {
212: String lnk = (String) kitr.next();
213: String res = links.getProperty(lnk);
214: // handle the case where lnk points to a directory (bug 25181)
215: try {
216: File test = new File(lnk);
217: if (!FILE_UTILS.isSymbolicLink(null, lnk)) {
218: doLink(res, lnk);
219: } else if (!test.getCanonicalPath().equals(
220: new File(res).getCanonicalPath())) {
221: deleteSymlink(lnk);
222: doLink(res, lnk);
223: } // else lnk exists, do nothing
224: } catch (IOException ioe) {
225: handleError("IO exception while creating link");
226: }
227: }
228: } finally {
229: setDefaults();
230: }
231: }
232:
233: /**
234: * Record symlinks.
235: * @throws BuildException on error.
236: * @since Ant 1.7
237: */
238: public void record() throws BuildException {
239: try {
240: if (fileSets.isEmpty()) {
241: handleError("Fileset identifying links to record required");
242: return;
243: }
244: if (linkFileName == null) {
245: handleError("Name of file to record links in required");
246: return;
247: }
248: // create a hashtable to group them by parent directory:
249: Hashtable byDir = new Hashtable();
250:
251: // get an Iterator of file objects representing links (canonical):
252: for (Iterator litr = findLinks(fileSets).iterator(); litr
253: .hasNext();) {
254: File this Link = (File) litr.next();
255: File parent = this Link.getParentFile();
256: Vector v = (Vector) byDir.get(parent);
257: if (v == null) {
258: v = new Vector();
259: byDir.put(parent, v);
260: }
261: v.addElement(this Link);
262: }
263: // write a Properties file in each directory:
264: for (Iterator dirs = byDir.keySet().iterator(); dirs
265: .hasNext();) {
266: File dir = (File) dirs.next();
267: Vector linksInDir = (Vector) byDir.get(dir);
268: Properties linksToStore = new Properties();
269:
270: // fill up a Properties object with link and resource names:
271: for (Iterator dlnk = linksInDir.iterator(); dlnk
272: .hasNext();) {
273: File lnk = (File) dlnk.next();
274: try {
275: linksToStore.put(lnk.getName(), lnk
276: .getCanonicalPath());
277: } catch (IOException ioe) {
278: handleError("Couldn't get canonical name of parent link");
279: }
280: }
281: writePropertyFile(linksToStore, dir);
282: }
283: } finally {
284: setDefaults();
285: }
286: }
287:
288: /**
289: * Return all variables to their default state for the next invocation.
290: * @since Ant 1.7
291: */
292: private void setDefaults() {
293: resource = null;
294: link = null;
295: linkFileName = null;
296: failonerror = true; // default behavior is to fail on an error
297: overwrite = false; // default behavior is to not overwrite
298: setAction("single"); // default behavior is make a single link
299: fileSets.clear();
300: }
301:
302: /**
303: * Set overwrite mode. If set to false (default)
304: * the task will not overwrite existing links, and may stop the build
305: * if a link already exists depending on the setting of failonerror.
306: *
307: * @param owrite If true overwrite existing links.
308: */
309: public void setOverwrite(boolean owrite) {
310: this .overwrite = owrite;
311: }
312:
313: /**
314: * Set failonerror mode. If set to true (default) the entire build fails
315: * upon error; otherwise the error is logged and the build will continue.
316: *
317: * @param foe If true throw BuildException on error, else log it.
318: */
319: public void setFailOnError(boolean foe) {
320: this .failonerror = foe;
321: }
322:
323: /**
324: * Set the action to be performed. May be "single",
325: * "delete", "recreate" or "record".
326: *
327: * @param action The action to perform.
328: */
329: public void setAction(String action) {
330: super .setAction(action);
331: }
332:
333: /**
334: * Set the name of the link. Used when action = "single".
335: *
336: * @param lnk The name for the link.
337: */
338: public void setLink(String lnk) {
339: this .link = lnk;
340: }
341:
342: /**
343: * Set the name of the resource to which a link should be created.
344: * Used when action = "single".
345: *
346: * @param src The resource to be linked.
347: */
348: public void setResource(String src) {
349: this .resource = src;
350: }
351:
352: /**
353: * Set the name of the file to which links will be written.
354: * Used when action = "record".
355: *
356: * @param lf The name of the file to write links to.
357: */
358: public void setLinkfilename(String lf) {
359: this .linkFileName = lf;
360: }
361:
362: /**
363: * Add a fileset to this task.
364: *
365: * @param set The fileset to add.
366: */
367: public void addFileset(FileSet set) {
368: fileSets.addElement(set);
369: }
370:
371: /**
372: * Delete a symlink (without deleting the associated resource).
373: *
374: * <p>This is a convenience method that simply invokes
375: * <code>deleteSymlink(java.io.File)</code>.
376: *
377: * @param path A string containing the path of the symlink to delete.
378: *
379: * @throws FileNotFoundException When the path results in a
380: * <code>File</code> that doesn't exist.
381: * @throws IOException If calls to <code>File.rename</code>
382: * or <code>File.delete</code> fail.
383: */
384: public static void deleteSymlink(String path) throws IOException,
385: FileNotFoundException {
386: deleteSymlink(new File(path));
387: }
388:
389: /**
390: * Delete a symlink (without deleting the associated resource).
391: *
392: * <p>This is a utility method that removes a unix symlink without removing
393: * the resource that the symlink points to. If it is accidentally invoked
394: * on a real file, the real file will not be harmed, but an exception
395: * will be thrown when the deletion is attempted. This method works by
396: * getting the canonical path of the link, using the canonical path to
397: * rename the resource (breaking the link) and then deleting the link.
398: * The resource is then returned to its original name inside a finally
399: * block to ensure that the resource is unharmed even in the event of
400: * an exception.
401: *
402: * @param linkfil A <code>File</code> object of the symlink to delete.
403: *
404: * @throws FileNotFoundException When the path results in a
405: * <code>File</code> that doesn't exist.
406: * @throws IOException If calls to <code>File.rename</code>,
407: * <code>File.delete</code> or
408: * <code>File.getCanonicalPath</code>
409: * fail.
410: */
411: public static void deleteSymlink(File linkfil) throws IOException,
412: FileNotFoundException {
413: if (!linkfil.exists()) {
414: throw new FileNotFoundException("No such symlink: "
415: + linkfil);
416: }
417: // find the resource of the existing link:
418: File canfil = linkfil.getCanonicalFile();
419:
420: // rename the resource, thus breaking the link:
421: File temp = FILE_UTILS.createTempFile("symlink", ".tmp", canfil
422: .getParentFile());
423: try {
424: try {
425: FILE_UTILS.rename(canfil, temp);
426: } catch (IOException e) {
427: throw new IOException(
428: "Couldn't rename resource when attempting to delete "
429: + linkfil);
430: }
431: // delete the (now) broken link:
432: if (!linkfil.delete()) {
433: throw new IOException(
434: "Couldn't delete symlink: "
435: + linkfil
436: + " (was it a real file? is this not a UNIX system?)");
437: }
438: } finally {
439: // return the resource to its original name:
440: try {
441: FILE_UTILS.rename(temp, canfil);
442: } catch (IOException e) {
443: throw new IOException("Couldn't return resource "
444: + temp + " to its original name: "
445: + canfil.getAbsolutePath()
446: + "\n THE RESOURCE'S NAME ON DISK HAS "
447: + "BEEN CHANGED BY THIS ERROR!\n");
448: }
449: }
450: }
451:
452: /**
453: * Write a properties file. This method uses <code>Properties.store</code>
454: * and thus may throw exceptions that occur while writing the file.
455: *
456: * @param properties The properties object to be written.
457: * @param dir The directory for which we are writing the links.
458: */
459: private void writePropertyFile(Properties properties, File dir)
460: throws BuildException {
461: BufferedOutputStream bos = null;
462: try {
463: bos = new BufferedOutputStream(new FileOutputStream(
464: new File(dir, linkFileName)));
465: properties.store(bos, "Symlinks from " + dir);
466: } catch (IOException ioe) {
467: throw new BuildException(ioe, getLocation());
468: } finally {
469: FileUtils.close(bos);
470: }
471: }
472:
473: /**
474: * Handle errors based on the setting of failonerror.
475: *
476: * @param msg The message to log, or include in the
477: * <code>BuildException</code>.
478: */
479: private void handleError(String msg) {
480: if (failonerror) {
481: throw new BuildException(msg);
482: }
483: log(msg);
484: }
485:
486: /**
487: * Conduct the actual construction of a link.
488: *
489: * <p> The link is constructed by calling <code>Execute.runCommand</code>.
490: *
491: * @param res The path of the resource we are linking to.
492: * @param lnk The name of the link we wish to make.
493: */
494: private void doLink(String res, String lnk) throws BuildException {
495: File linkfil = new File(lnk);
496: if (overwrite && linkfil.exists()) {
497: try {
498: deleteSymlink(linkfil);
499: } catch (FileNotFoundException fnfe) {
500: handleError("Symlink disappeared before it was deleted: "
501: + lnk);
502: } catch (IOException ioe) {
503: handleError("Unable to overwrite preexisting link: "
504: + lnk);
505: }
506: }
507: String[] cmd = new String[] { "ln", "-s", res, lnk };
508: log(Commandline.toString(cmd));
509: Execute.runCommand(this , cmd);
510: }
511:
512: /**
513: * Find all the links in all supplied filesets.
514: *
515: * <p> This method is invoked when the action attribute is
516: * "record". This means that filesets are interpreted
517: * as the directories in which links may be found.
518: *
519: * @param v The filesets specified by the user.
520: * @return A HashSet of <code>File</code> objects containing the
521: * links (with canonical parent directories).
522: */
523: private HashSet findLinks(Vector v) {
524: HashSet result = new HashSet();
525: for (int i = 0; i < v.size(); i++) {
526: FileSet fs = (FileSet) v.get(i);
527: DirectoryScanner ds = fs.getDirectoryScanner(getProject());
528: String[][] fnd = new String[][] { ds.getIncludedFiles(),
529: ds.getIncludedDirectories() };
530: File dir = fs.getDir(getProject());
531: for (int j = 0; j < fnd.length; j++) {
532: for (int k = 0; k < fnd[j].length; k++) {
533: try {
534: File f = new File(dir, fnd[j][k]);
535: File pf = f.getParentFile();
536: String name = f.getName();
537: if (FILE_UTILS.isSymbolicLink(pf, name)) {
538: result.add(new File(pf.getCanonicalFile(),
539: name));
540: }
541: } catch (IOException e) {
542: handleError("IOException: " + fnd[j][k]
543: + " omitted");
544: }
545: }
546: }
547: }
548: return result;
549: }
550:
551: /**
552: * Load links from properties files included in one or more FileSets.
553: *
554: * <p> This method is only invoked when the action attribute is set to
555: * "recreate". The filesets passed in are assumed to specify the
556: * names of the property files with the link information and the
557: * subdirectories in which to look for them.
558: *
559: * @param v The <code>FileSet</code>s for this task.
560: * @return The links to be made.
561: */
562: private Properties loadLinks(Vector v) {
563: Properties finalList = new Properties();
564: // loop through the supplied file sets:
565: for (int i = 0; i < v.size(); i++) {
566: FileSet fs = (FileSet) v.elementAt(i);
567: DirectoryScanner ds = new DirectoryScanner();
568: fs.setupDirectoryScanner(ds, getProject());
569: ds.setFollowSymlinks(false);
570: ds.scan();
571: String[] incs = ds.getIncludedFiles();
572: File dir = fs.getDir(getProject());
573:
574: // load included files as properties files:
575: for (int j = 0; j < incs.length; j++) {
576: File inc = new File(dir, incs[j]);
577: File pf = inc.getParentFile();
578: Properties lnks = new Properties();
579: try {
580: lnks.load(new BufferedInputStream(
581: new FileInputStream(inc)));
582: pf = pf.getCanonicalFile();
583: } catch (FileNotFoundException fnfe) {
584: handleError("Unable to find " + incs[j]
585: + "; skipping it.");
586: continue;
587: } catch (IOException ioe) {
588: handleError("Unable to open " + incs[j]
589: + " or its parent dir; skipping it.");
590: continue;
591: }
592: lnks.list(new PrintStream(new LogOutputStream(this ,
593: Project.MSG_INFO)));
594: // Write the contents to our master list of links
595: // This method assumes that all links are defined in
596: // terms of absolute paths, or paths relative to the
597: // working directory:
598: for (Iterator kitr = lnks.keySet().iterator(); kitr
599: .hasNext();) {
600: String key = (String) kitr.next();
601: finalList.put(new File(pf, key).getAbsolutePath(),
602: lnks.getProperty(key));
603: }
604: }
605: }
606: return finalList;
607: }
608: }
|