/*
* NOTE: This copyright does *not* cover user programs that use HQ
* program services by normal system calls through the application
* program interfaces provided as part of the Hyperic Plug-in Development
* Kit or the Hyperic Client Development Kit - this is merely considered
* normal use of the program, and does *not* fall under the heading of
* "derived work".
*
* Copyright (C) [2004, 2005, 2006], Hyperic, Inc.
* This file is part of HQ.
*
* HQ is free software; you can redistribute it and/or modify
* it under the terms version 2 of the GNU General Public License as
* published by the Free Software Foundation. This program is distributed
* in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA.
*/
/*
* This class is nifty but useless. I've updated it to make it useful.
* Why was it useless before? Well, let's say you had a directory
* /foo/bar/baz/ and within it, you had a symlink from fileX -> fileY
* This class, as originally implemented, would record the link
* (and recreated it), as fileX -> /foo/bar/baz/fileY
* ...which is totally stupid, as the link record file would not
* be portable in the least. If any parent directory (foo, bar, or baz)
* were ever moved, the link would break.
*
* I added code so that the class is smart enough to create
* relative links instead of absolute ones.
*
* I have also removed the "delete" functionality. Just use
* a regular file delete. Who ever want to delete just the symlinks?
*/
/*
* Copyright 2002-2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* Since the initial version of this file was deveolped on the clock on
* an NSF grant I should say the following boilerplate:
*
* This material is based upon work supported by the National Science
* Foundaton under Grant No. EIA-0196404. Any opinions, findings, and
* conclusions or recommendations expressed in this material are those
* of the author and do not necessarily reflect the views of the
* National Science Foundation.
*/
package org.hyperic.tools.ant;
import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.util.Vector;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Hashtable;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.hyperic.util.file.FileUtil;
/**
* Creates, Records and Restores Symlinks.
*
* <p> This task performs several related operations. In the most trivial,
* and default usage, it creates a link specified in the link atribute to
* a resource specified in the resource atribute. The second usage of this
* task is to traverses a directory structure specified by a fileset,
* and write a properties file in each included directory describing the
* links found in that directory. The third usage is to traverse a
* directory structure specified by a fileset, looking for properties files
* (also specified as included in the fileset) and recreate the links
* that have been previously recorded for each directory. Finally, it can be
* used to remove a symlink without deleting the file or directory it points
* to.
*
* <p> Examples of use:
*
* <p> Make a link named "foo" to a resource named "bar.foo" in subdir:
* <pre>
* <symlink link="${dir.top}/foo" resource="${dir.top}/subdir/bar.foo"/>
* </pre>
*
* <p> Record all links in subdir and it's descendants in files named
* "dir.links"
* <pre>
* <symlink action="record" linkfilename="dir.links">
* <fileset dir="${dir.top}" includes="subdir/**" />
* </symlink>
* </pre>
*
* <p> Recreate the links recorded in the previous example:
* <pre>
* <symlink action="recreate">
* <fileset dir="${dir.top}" includes="subdir/**/dir.links" />
* </symlink>
* </pre>
*
* <p> Delete a link named "foo" to a resource named "bar.foo" in subdir:
* <pre>
* <symlink action="delete" link="${dir.top}/foo"/>
* </pre>
*
* <p><strong>LIMITATIONS:</strong> Because Java has no direct support for
* handling symlinks this task divines them by comparing canoniacal and
* absolute paths. On non-unix systems this may cause false positives.
* Furthermore, any operating system on which the command
* <code>ln -s link resource</code> is not a valid command on the comandline
* will not be able to use action= "delete", action="single" or
* action="recreate", but action="record" should still work. Finally, the
* lack of support for symlinks in Java means that all links are recorded
* as links to the <strong>canonical</strong> resource name. Therefore
* the link: <code>link --> subdir/dir/../foo.bar</code> will be recorded
* as <code>link=subdir/foo.bar</code> and restored as
* <code>link --> subdir/foo.bar</code>
*
*/
public class Symlink extends Task {
// Atributes with setter methods
private String resource;
private String link;
private String action;
private Vector fileSets = new Vector();
private String linkFileName;
private boolean overwrite;
private boolean failonerror;
private boolean relative;
/** Initialize the task. */
public void init() throws BuildException {
super.init();
failonerror = true; // default behavior is to fail on an error
overwrite = false; // devault behavior is to not overwrite
action = "single"; // default behavior is make a single link
fileSets = new Vector();
}
/** The standard method for executing any task. */
public void execute() throws BuildException {
try {
if (action.equals("single")) {
doLink(resource, link);
} else if (action.equals("recreate")) {
Properties listOfLinks;
Enumeration keys;
if (fileSets.size() == 0) {
handleError("File set identifying link file(s) "
+ "required for action recreate");
return;
}
listOfLinks = loadLinks(fileSets);
keys = listOfLinks.keys();
while (keys.hasMoreElements()) {
link = (String) keys.nextElement();
resource = listOfLinks.getProperty(link);
// handle the case where the link exists
// and points to a directory (bug 25181)
try {
FileUtils fu = FileUtils.newFileUtils();
File test = new File(link);
File testRes = new File(resource);
if (!fu.isSymbolicLink(test.getParentFile(),
test.getName())) {
doLink(resource, link);
}
} catch (IOException ioe) {
handleError("IO exception while creating "
+ "link");
}
}
} else if (action.equals("record")) {
Vector vectOfLinks;
Hashtable byDir = new Hashtable();
Enumeration links, dirs;
if (fileSets.size() == 0) {
handleError("File set identifying links to "
+ "record required");
return;
}
if (linkFileName == null) {
handleError("Name of file to record links in "
+ "required");
return;
}
// fill our vector with file objects representing
// links (canonical)
vectOfLinks = findLinks(fileSets);
// create a hashtable to group them by parent directory
links = vectOfLinks.elements();
while (links.hasMoreElements()) {
File thisLink = (File) links.nextElement();
String parent = thisLink.getParent();
if (byDir.containsKey(parent)) {
((Vector) byDir.get(parent)).addElement(thisLink);
} else {
byDir.put(parent, new Vector());
((Vector) byDir.get(parent)).addElement(thisLink);
}
}
// write a Properties file in each directory
dirs = byDir.keys();
while (dirs.hasMoreElements()) {
String dir = (String) dirs.nextElement();
Vector linksInDir = (Vector) byDir.get(dir);
Properties linksToStore = new Properties();
Enumeration eachlink = linksInDir.elements();
File writeTo;
// fill up a Properties object with link and resource
// names
while (eachlink.hasMoreElements()) {
File alink = (File) eachlink.nextElement();
String linkPath = null;
try {
linkPath = getLinkPath(alink);
} catch (IOException ioe) {
handleError("Couldn't get canonical "
+ "name of a parent link");
}
linksToStore.put(alink.getName(),
linkPath);
}
// Get a place to record what we are about to write
writeTo = new File(dir + File.separator
+ linkFileName);
writePropertyFile(linksToStore, writeTo,
"Symlinks from " + writeTo.getParent());
}
} else {
handleError("Invalid action specified in symlink");
}
} finally {
// return all variables to their default state,
// ready for the next invocation.
resource = null;
link = null;
action = "single";
fileSets = new Vector();
linkFileName = null;
overwrite = false;
failonerror = true;
relative = false;
}
}
/* ********************************************************** *
* Begin Atribute Setter Methods *
* ********************************************************** */
/**
* The setter for the overwrite atribute. If set to false (default)
* the task will not overwrite existing links, and may stop the build
* if a link already exists depending on the setting of failonerror.
*
* @param owrite If true overwrite existing links.
*/
public void setOverwrite(boolean owrite) {
this.overwrite = owrite;
}
/**
* The setter for the failonerror atribute. If set to true (default)
* the entire build fails upon error. If set to false, the error is
* logged and the build will continue.
*
* @param foe If true throw build exception on error else log it.
*/
public void setFailOnError(boolean foe) {
this.failonerror = foe;
}
/**
* The setter for the "action" attribute. May be "single" "multi"
* or "record"
*
* @param typ The action of action to perform
*/
public void setAction(String typ) {
this.action = typ;
}
public void setRelative(boolean b) {
this.relative = b;
}
/**
* The setter for the "link" attribute. Only used for action = single.
*
* @param lnk The name for the link
*/
public void setLink(String lnk) {
this.link = lnk;
}
/**
* The setter for the "resource" attribute. Only used for action = single.
*
* @param src The source of the resource to be linked.
*/
public void setResource(String src) {
this.resource = src;
}
/**
* The setter for the "linkfilename" attribute. Only used for action=record.
*
* @param lf The name of the file to write links to.
*/
public void setLinkfilename(String lf) {
this.linkFileName = lf;
}
/**
* Adds a fileset to this task.
*
* @param set The fileset to add.
*/
public void addFileset(FileSet set) {
fileSets.addElement(set);
}
/* ********************************************************** *
* Begin Private Methods *
* ********************************************************** */
/**
* Writes a properties file.
*
* This method use <code>Properties.store</code>
* and thus report exceptions that occur while writing the file.
*
* This is not jdk 1.1 compatible, but ant 1.6 is not anymore.
*
* @param properties The properties object to be written.
* @param propertyfile The File to write to.
* @param comment The comment to place at the head of the file.
*/
private void writePropertyFile(Properties properties,
File propertyfile,
String comment)
throws BuildException {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(propertyfile);
properties.store(fos, /*comment*/ null);
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException ioex) {
log("Failed to close output stream");
}
}
}
}
/**
* Handles errors correctly based on the setting of failonerror.
*
* @param msg The message to log, or include in the
* <code>BuildException</code>
*/
private void handleError(String msg) {
if (failonerror) {
throw new BuildException(msg);
} else {
log(msg);
}
}
/**
* Conducts the actual construction of a link.
*
* <p> The link is constructed by calling <code>Execute.runCommand</code>.
*
* @param resource The path of the resource we are linking to.
* @param link The name of the link we wish to make
*/
private void doLink(String resource, String link) throws BuildException {
if (resource == null) {
handleError("Must define the resource to symlink to!");
return;
}
if (link == null) {
handleError("Must define the link "
+ "name for symlink!");
return;
}
File linkfil = new File(link);
String[] cmd = new String[4];
cmd[0] = "ln";
cmd[1] = "-s";
cmd[2] = resource;
cmd[3] = link;
try {
if (overwrite && linkfil.exists()) {
if (!linkfil.delete()) {
throw new IOException("Error deleting file: " + linkfil);
}
}
} catch (FileNotFoundException fnfe) {
handleError("Symlink dissapeared before it was deleted:" + link);
} catch (IOException ioe) {
handleError("Unable to overwrite preexisting link " + link);
}
log(cmd[0] + " " + cmd[1] + " " + cmd[2] + " " + cmd[3]);
// Borrowed from Execute.runCommand (which is also stupid because
// it doesn't let you specify a working directory)
Execute exe = new Execute(new LogStreamHandler(this,
Project.MSG_INFO,
Project.MSG_ERR));
exe.setAntRun(getProject());
exe.setCommandline(cmd);
if (relative) {
exe.setWorkingDirectory(new File("."));
}
try {
int retval = exe.execute();
if (retval != 0) {
throw new BuildException(cmd[0]
+ " failed with return code " + retval, this.getLocation());
}
} catch (java.io.IOException exc) {
throw new BuildException("Could not launch " + cmd[0] + ": "
+ exc, this.getLocation());
}
}
/**
* Simultaneously get included directories and included files.
*
* @param ds The scanner with which to get the files and directories.
* @return A vector of <code>String</code> objects containing the
* included file names and directory names.
*/
private Vector scanDirsAndFiles(DirectoryScanner ds) {
String[] files, dirs;
Vector list = new Vector();
ds.scan();
files = ds.getIncludedFiles();
dirs = ds.getIncludedDirectories();
for (int i = 0; i < files.length; i++) {
list.addElement(files[i]);
}
for (int i = 0; i < dirs.length; i++) {
list.addElement(dirs[i]);
}
return list;
}
/**
* Finds all the links in all supplied filesets.
*
* <p> This method is invoked when the action atribute is is "record".
* This means that filesets are interpreted as the directories in
* which links may be found.
*
* <p> The basic method follwed here is, for each file set:
* <ol>
* <li> Compile a list of all matches </li>
* <li> Convert matches to <code>File</code> objects </li>
* <li> Remove all non-symlinks using
* <code>FileUtils.isSymbolicLink</code> </li>
* <li> Convert all parent directories to the canonical form </li>
* <li> Add the remaining links from each file set to a
* master list of links unless the link is already recorded
* in the list</li>
* </ol>
*
* @param fileSets The filesets specified by the user.
* @return A vector of <code>File</code> objects containing the
* links (with canonical parent directories)
*/
private Vector findLinks(Vector fileSets) {
Vector result = new Vector();
// loop through the supplied file sets
FSLoop: for (int i = 0; i < fileSets.size(); i++) {
FileSet fsTemp = (FileSet) fileSets.elementAt(i);
String workingDir = null;
Vector links = new Vector();
Vector linksFiles = new Vector();
Enumeration enumLinks;
DirectoryScanner ds;
File tmpfil = null;
try {
tmpfil = fsTemp.getDir(this.getProject());
workingDir = tmpfil.getCanonicalPath();
} catch (IOException ioe) {
handleError("Exception caught getting "
+ "canonical path of working dir " + tmpfil
+ " in a FileSet passed to the symlink "
+ "task. Further processing of this "
+ "fileset skipped");
continue FSLoop;
}
// Get a vector of String with file names that match
// the pattern
ds = fsTemp.getDirectoryScanner(this.getProject());
links = scanDirsAndFiles(ds);
// Now convert the strings to File Objects
// using the canonical version of the working dir
enumLinks = links.elements();
while (enumLinks.hasMoreElements()) {
linksFiles.addElement(new File(workingDir
+ File.separator
+ (String) enumLinks
.nextElement()));
}
// Now loop through and remove the non-links
enumLinks = linksFiles.elements();
File parentNext, next;
String nameParentNext;
FileUtils fu = FileUtils.newFileUtils();
Vector removals = new Vector();
while (enumLinks.hasMoreElements()) {
next = (File) enumLinks.nextElement();
nameParentNext = next.getParent();
parentNext = new File(nameParentNext);
try {
if (!fu.isSymbolicLink(parentNext, next.getName())) {
removals.addElement(next);
}
} catch (IOException ioe) {
handleError("Failed checking " + next
+ " for symbolic link. FileSet skipped.");
continue FSLoop;
// Otherwise this file will be falsely recorded as a link,
// if failonerror = false, hence the warn and skip.
}
}
enumLinks = removals.elements();
while (enumLinks.hasMoreElements()) {
linksFiles.removeElement(enumLinks.nextElement());
}
// Now we have what we want so add it to results, ensuring that
// no link is returned twice and we have a canonical reference
// to the link. (no symlinks in the parent dir)
enumLinks = linksFiles.elements();
while (enumLinks.hasMoreElements()) {
File temp, parent;
next = (File) enumLinks.nextElement();
try {
parent = new File(next.getParent());
parent = new File(parent.getCanonicalPath());
temp = new File(parent, next.getName());
if (!result.contains(temp)) {
result.addElement(temp);
}
} catch (IOException ioe) {
handleError("IOException: " + next + " omitted");
}
}
// Note that these links are now specified with a full
// canonical path irrespective of the working dir of the
// file set so it is ok to mix them in the same vector.
}
return result;
}
/**
* Load the links from a properties file.
*
* <p> This method is only invoked when the action atribute is set to
* "multi". The filesets passed in are assumed to specify the names
* of the property files with the link information and the
* subdirectories in which to look for them.
*
* <p> The basic method follwed here is, for each file set:
* <ol>
* <li> Get the canonical version of the dir atribute </li>
* <li> Scan for properties files </li>
* <li> load the contents of each properties file found. </li>
* </ol>
*
* @param fileSets The <code>FileSet</code>s for this task
* @return The links to be made.
*/
private Properties loadLinks(Vector fileSets) {
Properties finalList = new Properties();
Enumeration keys;
String key, value;
String[] includedFiles;
// loop through the supplied file sets
FSLoop: for (int i = 0; i < fileSets.size(); i++) {
String workingDir;
FileSet fsTemp = (FileSet) fileSets.elementAt(i);
DirectoryScanner ds;
try {
File linelength = fsTemp.getDir(this.getProject());
workingDir = linelength.getCanonicalPath();
} catch (IOException ioe) {
handleError("Exception caught getting "
+ "canonical path of working dir "
+ "of a FileSet passed to symlink "
+ "task. FileSet skipped.");
continue FSLoop;
}
ds = fsTemp.getDirectoryScanner(this.getProject());
ds.setFollowSymlinks(false);
ds.scan();
includedFiles = ds.getIncludedFiles();
// loop through the files identified by each file set
// and load their contents.
for (int j = 0; j < includedFiles.length; j++) {
File inc = new File(workingDir + File.separator
+ includedFiles[j]);
String inDir;
Properties propTemp = new Properties();
FileInputStream fi = null;
try {
fi = new FileInputStream(inc);
propTemp.load(fi);
inDir = inc.getParent();
inDir = (new File(inDir)).getCanonicalPath();
} catch (FileNotFoundException fnfe) {
handleError("Unable to find " + includedFiles[j]
+ "FileSet skipped.");
continue FSLoop;
} catch (IOException ioe) {
handleError("Unable to open " + includedFiles[j]
+ " or it's parent dir"
+ "FileSet skipped.");
continue FSLoop;
} finally {
if (fi != null) try { fi.close(); } catch (IOException e) {
throw new BuildException("Error closing file: " + e, e);
}
}
keys = propTemp.keys();
propTemp.list(System.out);
// Write the contents to our master list of links
// This method assumes that all links are defined in
// terms of absolute paths, or paths relative to the
// working directory
while (keys.hasMoreElements()) {
key = (String) keys.nextElement();
value = propTemp.getProperty(key);
finalList.put(inDir + File.separator + key, value);
}
}
}
return finalList;
}
private String getLinkPath (File link) throws IOException {
if (relative) {
String p = FileUtil.getRelativePath(new File(link.getCanonicalPath()),
link.getParentFile());
if (p.startsWith("./")) return p.substring(2);
return p;
} else {
return link.getCanonicalPath();
}
}
}