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