From 40ce7de3a68104dc25f6d84f6b17a0d615decd16 Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Tue, 25 Mar 2003 09:30:59 +0000 Subject: [PATCH] New task . PR: 12632 Submitted by: Dominique Devienne Dan Armbrust git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@274313 13f79535-47bb-0310-9956-ffa450edef68 --- WHATSNEW | 2 + .../org/apache/tools/ant/taskdefs/Sync.java | 382 ++++++++++++++++++ .../tools/ant/taskdefs/defaults.properties | 1 + 3 files changed, 385 insertions(+) create mode 100644 src/main/org/apache/tools/ant/taskdefs/Sync.java diff --git a/WHATSNEW b/WHATSNEW index 612d0104d..9ea14f0eb 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -172,6 +172,8 @@ Other changes: * 's basedir attribute is now optional if you specify nested filesets. Bugzilla Report 18046. +* New task that synchronizes two directory trees. + Changes from Ant 1.5.2 to Ant 1.5.3 =================================== diff --git a/src/main/org/apache/tools/ant/taskdefs/Sync.java b/src/main/org/apache/tools/ant/taskdefs/Sync.java new file mode 100644 index 000000000..187696962 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/Sync.java @@ -0,0 +1,382 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "Ant" and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This code is based on code Copyright (c) 2002, Landmark Graphics + * Corp that has been kindly donated to the Apache Software + * Foundation. + */ + +package org.apache.tools.ant.taskdefs; + +import java.io.File; + +import java.util.Hashtable; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.util.FileNameMapper; +import org.apache.tools.ant.util.IdentityMapper; + +/** + * Synchronize a local target directory from the files defined + * in one or more filesets. + * + *

Uses a <copy> task internally, but forbidding the use of + * mappers and filter chains. Files of the destination directory not + * present in any of the source fileset are removed.

+ * + * @author Dominique Devienne + * @version $Revision$ + * @since Ant 1.6 + * + * revised by Dan Armbrust + * to remove orphaned directories. + * + * @ant.task category="filesystem" + */ +public class Sync extends Task { + + // Same as regular task... see at end-of-file! + private MyCopy _copy; + + // Override Task#init + public void init() + throws BuildException { + // Instantiate it + _copy = new MyCopy(); + configureTask(_copy); + + // Default config of for our purposes. + _copy.setFiltering(false); + _copy.setIncludeEmptyDirs(false); + _copy.setPreserveLastModified(true); + } + + private void configureTask(Task helper) { + helper.setProject(getProject()); + helper.setTaskName(getTaskName()); + helper.setOwningTarget(getOwningTarget()); + helper.init(); + } + + // Override Task#execute + public void execute() + throws BuildException { + // The destination of the files to copy + File toDir = _copy.getToDir(); + + // The complete list of files to copy + Hashtable allFiles = _copy._dest2src; + + // If the destination directory didn't already exist, + // or was empty, then no previous file removal is necessary! + boolean noRemovalNecessary = !toDir.exists() || + toDir.list().length < 1; + + // Copy all the necessary out-of-date files + log("PASS#1: Copying files to " + toDir, Project.MSG_DEBUG); + _copy.execute(); + + // Do we need to perform further processing? + if (noRemovalNecessary) { + log("NO removing necessary in " + toDir, Project.MSG_DEBUG); + return; // nope ;-) + } + + // Get rid of all files not listed in the source filesets. + log("PASS#2: Removing orphan files from " + toDir, Project.MSG_DEBUG); + int[] removedFileCount = removeOrphanFiles(allFiles, toDir); + logRemovedCount(removedFileCount[0], "dangling director", "y", "ies"); + logRemovedCount(removedFileCount[1], "dangling file", "", "s"); + + // Get rid of empty directories on the destination side + if (!_copy.getIncludeEmptyDirs()) { + log("PASS#3: Removing empty directories from " + toDir, + Project.MSG_DEBUG); + int removedDirCount = removeEmptyDirectories(toDir, false); + logRemovedCount(removedDirCount, "empty director", "y", "ies"); + } + } + + private void logRemovedCount(int count, String prefix, + String singularSuffix, String pluralSuffix) { + File toDir = _copy.getToDir(); + + String what = (prefix == null) ? "" : prefix; + what += (count < 2) ? singularSuffix : pluralSuffix; + + if (count > 0) { + log("Removed " + count + " " + what + " from " + toDir, + Project.MSG_INFO); + } else { + log("NO " + what + " to remove from " + toDir, + Project.MSG_VERBOSE); + } + } + + /** + * Removes all files and folders not found as keyes of a table + * (used as a set!). + * + *

If the provided file is a directory, it is recursively + * scanned for orphaned files which will be removed as well.

+ * + *

If the directory is an orphan, it will also be removed.

+ * + * @param nonOrphans the table of all non-orphan Files. + * @param file the initial file or directory to scan or test. + * @return the number of orphaned files and directories actually removed. + * Position 0 of the array is the number of orphaned directories. + * Position 1 of the array is the number or orphaned files. + * Position 2 is meaningless. + */ + private int[] removeOrphanFiles(Hashtable nonOrphans, File file) { + int[] removedCount = new int[] {0, 0, 0}; + if (file.isDirectory()) { + File[] children = file.listFiles(); + for (int i = 0; i < children.length; ++i) { + int[] temp = removeOrphanFiles(nonOrphans, children[i]); + removedCount[0] += temp[0]; + removedCount[1] += temp[1]; + removedCount[2] += temp[2]; + } + + if (nonOrphans.get(file) == null && removedCount[2] == 0) { + log("Removing orphan directory: " + file, Project.MSG_DEBUG); + file.delete(); + ++removedCount[0]; + } else { + /* + Contrary to what is said above, position 2 is not + meaningless inside the recursion. + Position 2 is used to carry information back up the + recursion about whether or not a directory contains + a directory or file at any depth that is not an + orphan + This has to be done, because if you have the + following directory structure: c:\src\a\file and + your mapper src files were constructed like so: + + The folder 'a' will not be in the hashtable of + nonorphans. So, before deleting it as an orphan, we + have to know whether or not any of its children at + any level are orphans. + If no, then this folder is also an orphan, and may + be deleted. I do this by changing position 2 to a + '1'. + */ + removedCount[2] = 1; + } + + } else { + if (nonOrphans.get(file) == null) { + log("Removing orphan file: " + file, Project.MSG_DEBUG); + file.delete(); + ++removedCount[1]; + } else { + removedCount[2] = 1; + } + } + return removedCount; + } + + /** + * Removes all empty directories from a directory. + * + *

Note that a directory that contains only empty + * directories, directly or not, will be removed!

+ * + *

Recurses depth-first to find the leaf directories + * which are empty and removes them, then unwinds the + * recursion stack, removing directories which have + * become empty themselves, etc...

+ * + * @param dir the root directory to scan for empty directories. + * @param removeIfEmpty whether to remove the root directory + * itself if it becomes empty. + * @return the number of empty directories actually removed. + */ + private int removeEmptyDirectories(File dir, boolean removeIfEmpty) { + int removedCount = 0; + if (dir.isDirectory()) { + File[] children = dir.listFiles(); + for (int i = 0; i < children.length; ++i) { + File file = children[i]; + // Test here again to avoid method call for non-directories! + if (file.isDirectory()) { + removedCount += removeEmptyDirectories(file, true); + } + } + if (children.length > 0) { + // This directory may have become empty... + // We need to re-query its children list! + children = dir.listFiles(); + } + if (children.length < 1 && removeIfEmpty) { + log("Removing empty directory: " + dir, Project.MSG_DEBUG); + dir.delete(); + ++removedCount; + } + } + return removedCount; + } + + + // + // Various copy attributes/subelements of passed thru to + // + + /** + * Sets the destination directory. + */ + public void setTodir(File destDir) { + _copy.setTodir(destDir); + } + + /** + * Used to force listing of all names of copied files. + */ + public void setVerbose(boolean verbose) { + _copy.setVerbose(verbose); + } + + /** + * Overwrite any existing destination file(s). + */ + public void setOverwrite(boolean overwrite) { + _copy.setOverwrite(overwrite); + } + + /** + * Used to copy empty directories. + */ + public void setIncludeEmptyDirs(boolean includeEmpty) { + _copy.setIncludeEmptyDirs(includeEmpty); + } + + /** + * If false, note errors to the output but keep going. + * @param failonerror true or false + */ + public void setFailOnError(boolean failonerror) { + _copy.setFailOnError(failonerror); + } + + /** + * Adds a set of files to copy. + */ + public void addFileset(FileSet set) { + _copy.addFileset(set); + } + + /** + * Sets the character encoding + */ + public void setEncoding(String encoding) { + _copy.setEncoding(encoding); + } + + + /** + * Subclass Copy in order to access it's file/dir maps. + */ + public static class MyCopy + extends Copy { + + // List of files that must be copied, irrelevant from the + // fact that they are newer or not than the destination. + private Hashtable _dest2src = new Hashtable(); + + public MyCopy() {} + + protected void buildMap(File fromDir, File toDir, String[] names, + FileNameMapper mapper, Hashtable map) { + assertTrue("No mapper", mapper instanceof IdentityMapper); + + super.buildMap(fromDir, toDir, names, mapper, map); + + for (int i = 0; i < names.length; ++i) { + String name = names[i]; + File dest = new File(toDir, name); + // No need to instantiate the src file, as we use the + // table as a set (to remain Java 1.1 compatible!!!). + //File src = new File(fromDir, name); + //_dest2src.put(dest, src); + _dest2src.put(dest, fromDir); + } + } + + public File getToDir() { + return destDir; + } + + public boolean getIncludeEmptyDirs() { + return includeEmpty; + } + + } + + /** + * Pseudo-assert method. + */ + private static void assertTrue(String message, boolean condition) { + if (!condition) { + throw new BuildException("Assertion Error: " + message); + } + } + +} diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties index b637cfea8..7e384bebb 100644 --- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties +++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties @@ -74,6 +74,7 @@ classloader=org.apache.tools.ant.taskdefs.Classloader import=org.apache.tools.ant.taskdefs.ImportTask whichresource=org.apache.tools.ant.taskdefs.WhichResource subant=org.apache.tools.ant.taskdefs.SubAnt +sync=org.apache.tools.ant.taskdefs.Sync # optional tasks image=org.apache.tools.ant.taskdefs.optional.image.Image