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: package org.apache.tools.ant.taskdefs;
020:
021: import java.io.File;
022:
023: import java.util.HashSet;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import org.apache.tools.ant.BuildException;
029: import org.apache.tools.ant.DirectoryScanner;
030: import org.apache.tools.ant.Project;
031: import org.apache.tools.ant.Task;
032: import org.apache.tools.ant.types.AbstractFileSet;
033: import org.apache.tools.ant.types.FileSet;
034: import org.apache.tools.ant.types.PatternSet;
035: import org.apache.tools.ant.types.Resource;
036: import org.apache.tools.ant.types.ResourceCollection;
037: import org.apache.tools.ant.types.selectors.FileSelector;
038: import org.apache.tools.ant.types.selectors.NoneSelector;
039:
040: /**
041: * Synchronize a local target directory from the files defined
042: * in one or more filesets.
043: *
044: * <p>Uses a <copy> task internally, but forbidding the use of
045: * mappers and filter chains. Files of the destination directory not
046: * present in any of the source fileset are removed.</p>
047: *
048: * @since Ant 1.6
049: *
050: * revised by <a href="mailto:daniel.armbrust@mayo.edu">Dan Armbrust</a>
051: * to remove orphaned directories.
052: *
053: * @ant.task category="filesystem"
054: */
055: public class Sync extends Task {
056:
057: // Same as regular <copy> task... see at end-of-file!
058: private MyCopy myCopy;
059:
060: // Similar to a fileset, but doesn't allow dir attribute to be set
061: private SyncTarget syncTarget;
062:
063: // Override Task#init
064: /**
065: * Initialize the sync task.
066: * @throws BuildException if there is a problem.
067: * @see Task#init()
068: */
069: public void init() throws BuildException {
070: // Instantiate it
071: myCopy = new MyCopy();
072: configureTask(myCopy);
073:
074: // Default config of <mycopy> for our purposes.
075: myCopy.setFiltering(false);
076: myCopy.setIncludeEmptyDirs(false);
077: myCopy.setPreserveLastModified(true);
078: }
079:
080: private void configureTask(Task helper) {
081: helper.setProject(getProject());
082: helper.setTaskName(getTaskName());
083: helper.setOwningTarget(getOwningTarget());
084: helper.init();
085: }
086:
087: // Override Task#execute
088: /**
089: * Execute the sync task.
090: * @throws BuildException if there is an error.
091: * @see Task#execute()
092: */
093: public void execute() throws BuildException {
094: // The destination of the files to copy
095: File toDir = myCopy.getToDir();
096:
097: // The complete list of files to copy
098: Set allFiles = myCopy.nonOrphans;
099:
100: // If the destination directory didn't already exist,
101: // or was empty, then no previous file removal is necessary!
102: boolean noRemovalNecessary = !toDir.exists()
103: || toDir.list().length < 1;
104:
105: // Copy all the necessary out-of-date files
106: log("PASS#1: Copying files to " + toDir, Project.MSG_DEBUG);
107: myCopy.execute();
108:
109: // Do we need to perform further processing?
110: if (noRemovalNecessary) {
111: log("NO removing necessary in " + toDir, Project.MSG_DEBUG);
112: return; // nope ;-)
113: }
114:
115: // Get rid of all files not listed in the source filesets.
116: log("PASS#2: Removing orphan files from " + toDir,
117: Project.MSG_DEBUG);
118: int[] removedFileCount = removeOrphanFiles(allFiles, toDir);
119: logRemovedCount(removedFileCount[0], "dangling director", "y",
120: "ies");
121: logRemovedCount(removedFileCount[1], "dangling file", "", "s");
122:
123: // Get rid of empty directories on the destination side
124: if (!myCopy.getIncludeEmptyDirs()) {
125: log("PASS#3: Removing empty directories from " + toDir,
126: Project.MSG_DEBUG);
127: int removedDirCount = removeEmptyDirectories(toDir, false);
128: logRemovedCount(removedDirCount, "empty director", "y",
129: "ies");
130: }
131: }
132:
133: private void logRemovedCount(int count, String prefix,
134: String singularSuffix, String pluralSuffix) {
135: File toDir = myCopy.getToDir();
136:
137: String what = (prefix == null) ? "" : prefix;
138: what += (count < 2) ? singularSuffix : pluralSuffix;
139:
140: if (count > 0) {
141: log("Removed " + count + " " + what + " from " + toDir,
142: Project.MSG_INFO);
143: } else {
144: log("NO " + what + " to remove from " + toDir,
145: Project.MSG_VERBOSE);
146: }
147: }
148:
149: /**
150: * Removes all files and folders not found as keys of a table
151: * (used as a set!).
152: *
153: * <p>If the provided file is a directory, it is recursively
154: * scanned for orphaned files which will be removed as well.</p>
155: *
156: * <p>If the directory is an orphan, it will also be removed.</p>
157: *
158: * @param nonOrphans the table of all non-orphan <code>File</code>s.
159: * @param file the initial file or directory to scan or test.
160: * @return the number of orphaned files and directories actually removed.
161: * Position 0 of the array is the number of orphaned directories.
162: * Position 1 of the array is the number or orphaned files.
163: */
164: private int[] removeOrphanFiles(Set nonOrphans, File toDir) {
165: int[] removedCount = new int[] { 0, 0 };
166: String[] excls = (String[]) nonOrphans
167: .toArray(new String[nonOrphans.size() + 1]);
168: // want to keep toDir itself
169: excls[nonOrphans.size()] = "";
170:
171: DirectoryScanner ds = null;
172: if (syncTarget != null) {
173: FileSet fs = new FileSet();
174: fs.setDir(toDir);
175: fs.setCaseSensitive(syncTarget.isCaseSensitive());
176: fs.setFollowSymlinks(syncTarget.isFollowSymlinks());
177:
178: // preserveInTarget would find all files we want to keep,
179: // but we need to find all that we want to delete - so the
180: // meaning of all patterns and selectors must be inverted
181: PatternSet ps = syncTarget.mergePatterns(getProject());
182: fs.appendExcludes(ps.getIncludePatterns(getProject()));
183: fs.appendIncludes(ps.getExcludePatterns(getProject()));
184: fs.setDefaultexcludes(!syncTarget.getDefaultexcludes());
185:
186: // selectors are implicitly ANDed in DirectoryScanner. To
187: // revert their logic we wrap them into a <none> selector
188: // instead.
189: FileSelector[] s = syncTarget.getSelectors(getProject());
190: if (s.length > 0) {
191: NoneSelector ns = new NoneSelector();
192: for (int i = 0; i < s.length; i++) {
193: ns.appendSelector(s[i]);
194: }
195: fs.appendSelector(ns);
196: }
197: ds = fs.getDirectoryScanner(getProject());
198: } else {
199: ds = new DirectoryScanner();
200: ds.setBasedir(toDir);
201: }
202: ds.addExcludes(excls);
203:
204: ds.scan();
205: String[] files = ds.getIncludedFiles();
206: for (int i = 0; i < files.length; i++) {
207: File f = new File(toDir, files[i]);
208: log("Removing orphan file: " + f, Project.MSG_DEBUG);
209: f.delete();
210: ++removedCount[1];
211: }
212: String[] dirs = ds.getIncludedDirectories();
213: // ds returns the directories in lexicographic order.
214: // iterating through the array backwards means we are deleting
215: // leaves before their parent nodes - thus making sure (well,
216: // more likely) that the directories are empty when we try to
217: // delete them.
218: for (int i = dirs.length - 1; i >= 0; --i) {
219: File f = new File(toDir, dirs[i]);
220: if (f.list().length < 1) {
221: log("Removing orphan directory: " + f,
222: Project.MSG_DEBUG);
223: f.delete();
224: ++removedCount[0];
225: }
226: }
227: return removedCount;
228: }
229:
230: /**
231: * Removes all empty directories from a directory.
232: *
233: * <p><em>Note that a directory that contains only empty
234: * directories, directly or not, will be removed!</em></p>
235: *
236: * <p>Recurses depth-first to find the leaf directories
237: * which are empty and removes them, then unwinds the
238: * recursion stack, removing directories which have
239: * become empty themselves, etc...</p>
240: *
241: * @param dir the root directory to scan for empty directories.
242: * @param removeIfEmpty whether to remove the root directory
243: * itself if it becomes empty.
244: * @return the number of empty directories actually removed.
245: */
246: private int removeEmptyDirectories(File dir, boolean removeIfEmpty) {
247: int removedCount = 0;
248: if (dir.isDirectory()) {
249: File[] children = dir.listFiles();
250: for (int i = 0; i < children.length; ++i) {
251: File file = children[i];
252: // Test here again to avoid method call for non-directories!
253: if (file.isDirectory()) {
254: removedCount += removeEmptyDirectories(file, true);
255: }
256: }
257: if (children.length > 0) {
258: // This directory may have become empty...
259: // We need to re-query its children list!
260: children = dir.listFiles();
261: }
262: if (children.length < 1 && removeIfEmpty) {
263: log("Removing empty directory: " + dir,
264: Project.MSG_DEBUG);
265: dir.delete();
266: ++removedCount;
267: }
268: }
269: return removedCount;
270: }
271:
272: //
273: // Various copy attributes/subelements of <copy> passed thru to <mycopy>
274: //
275:
276: /**
277: * Sets the destination directory.
278: * @param destDir the destination directory
279: */
280: public void setTodir(File destDir) {
281: myCopy.setTodir(destDir);
282: }
283:
284: /**
285: * Used to force listing of all names of copied files.
286: * @param verbose if true force listing of all names of copied files.
287: */
288: public void setVerbose(boolean verbose) {
289: myCopy.setVerbose(verbose);
290: }
291:
292: /**
293: * Overwrite any existing destination file(s).
294: * @param overwrite if true overwrite any existing destination file(s).
295: */
296: public void setOverwrite(boolean overwrite) {
297: myCopy.setOverwrite(overwrite);
298: }
299:
300: /**
301: * Used to copy empty directories.
302: * @param includeEmpty If true copy empty directories.
303: */
304: public void setIncludeEmptyDirs(boolean includeEmpty) {
305: myCopy.setIncludeEmptyDirs(includeEmpty);
306: }
307:
308: /**
309: * If false, note errors to the output but keep going.
310: * @param failonerror true or false
311: */
312: public void setFailOnError(boolean failonerror) {
313: myCopy.setFailOnError(failonerror);
314: }
315:
316: /**
317: * Adds a set of files to copy.
318: * @param set a fileset
319: */
320: public void addFileset(FileSet set) {
321: add(set);
322: }
323:
324: /**
325: * Adds a collection of filesystem resources to copy.
326: * @param rc a resource collection
327: * @since Ant 1.7
328: */
329: public void add(ResourceCollection rc) {
330: myCopy.add(rc);
331: }
332:
333: /**
334: * The number of milliseconds leeway to give before deciding a
335: * target is out of date.
336: *
337: * <p>Default is 0 milliseconds, or 2 seconds on DOS systems.</p>
338: * @param granularity a <code>long</code> value
339: * @since Ant 1.6.2
340: */
341: public void setGranularity(long granularity) {
342: myCopy.setGranularity(granularity);
343: }
344:
345: /**
346: * A container for patterns and selectors that can be used to
347: * specify files that should be kept in the target even if they
348: * are not present in any source directory.
349: *
350: * <p>You must not invoke this method more than once.</p>
351: * @param s a preserveintarget nested element
352: * @since Ant 1.7
353: */
354: public void addPreserveInTarget(SyncTarget s) {
355: if (syncTarget != null) {
356: throw new BuildException("you must not specify multiple "
357: + "preserveintarget elements.");
358: }
359: syncTarget = s;
360: }
361:
362: /**
363: * Subclass Copy in order to access it's file/dir maps.
364: */
365: public static class MyCopy extends Copy {
366:
367: // List of files that must be copied, irrelevant from the
368: // fact that they are newer or not than the destination.
369: private Set nonOrphans = new HashSet();
370:
371: /** Constructor for MyCopy. */
372: public MyCopy() {
373: }
374:
375: /**
376: * @see Copy#scan(File, File, String[], String[])
377: */
378: /** {@inheritDoc} */
379: protected void scan(File fromDir, File toDir, String[] files,
380: String[] dirs) {
381: assertTrue("No mapper", mapperElement == null);
382:
383: super .scan(fromDir, toDir, files, dirs);
384:
385: for (int i = 0; i < files.length; ++i) {
386: nonOrphans.add(files[i]);
387: }
388: for (int i = 0; i < dirs.length; ++i) {
389: nonOrphans.add(dirs[i]);
390: }
391: }
392:
393: /**
394: * @see Copy#scan(Resource[], File)
395: */
396: /** {@inheritDoc} */
397: protected Map scan(Resource[] resources, File toDir) {
398: assertTrue("No mapper", mapperElement == null);
399:
400: Map m = super .scan(resources, toDir);
401:
402: Iterator iter = m.keySet().iterator();
403: while (iter.hasNext()) {
404: nonOrphans.add(((Resource) iter.next()).getName());
405: }
406: return m;
407: }
408:
409: /**
410: * Get the destination directory.
411: * @return the destination directory
412: */
413: public File getToDir() {
414: return destDir;
415: }
416:
417: /**
418: * Get the includeEmptyDirs attribute.
419: * @return true if emptyDirs are to be included
420: */
421: public boolean getIncludeEmptyDirs() {
422: return includeEmpty;
423: }
424:
425: /**
426: * Yes, we can.
427: * @return true always.
428: * @since Ant 1.7
429: */
430: protected boolean supportsNonFileResources() {
431: return true;
432: }
433: }
434:
435: /**
436: * Inner class used to hold exclude patterns and selectors to save
437: * stuff that happens to live in the target directory but should
438: * not get removed.
439: *
440: * @since Ant 1.7
441: */
442: public static class SyncTarget extends AbstractFileSet {
443:
444: /**
445: * Constructor for SyncTarget.
446: * This just changes the default value of "defaultexcludes" from
447: * true to false.
448: */
449: public SyncTarget() {
450: super ();
451: }
452:
453: /**
454: * Override AbstractFileSet#setDir(File) to disallow
455: * setting the directory.
456: * @param dir ignored
457: * @throws BuildException always
458: */
459: public void setDir(File dir) throws BuildException {
460: throw new BuildException(
461: "preserveintarget doesn't support the dir "
462: + "attribute");
463: }
464:
465: }
466:
467: /**
468: * Pseudo-assert method.
469: */
470: private static void assertTrue(String message, boolean condition) {
471: if (!condition) {
472: throw new BuildException("Assertion Error: " + message);
473: }
474: }
475:
476: }
|