/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. * */ package org.apache.tools.ant.types.selectors.modifiedselector; // Java import java.io.File; import java.util.Comparator; import java.util.Iterator; import java.util.Vector; import org.apache.tools.ant.BuildEvent; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildListener; import org.apache.tools.ant.IntrospectionHelper; // Ant import org.apache.tools.ant.Project; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.Parameter; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.types.resources.selectors.ResourceSelector; import org.apache.tools.ant.types.selectors.BaseExtendSelector; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.ResourceUtils; /** *

Selector class that uses Algorithm, Cache and Comparator * for its work. * The Algorithm is used for computing a hashvalue for a file. * The Comparator decides whether to select or not. * The Cache stores the other value for comparison by the Comparator * in a persistent manner.

* *

The ModifiedSelector is implemented as a CoreSelector and uses default * values for all its attributes therefore the simplest example is

 *   <copy todir="dest">
 *       <filelist dir="src">
 *           <modified/>
 *       </filelist>
 *   </copy>
 * 

* *

The same example rewritten as CoreSelector with setting the all values * (same as defaults are) would be

 *   <copy todir="dest">
 *       <filelist dir="src">
 *           <modified update="true"
 *                     cache="propertyfile"
 *                     algorithm="digest"
 *                     comparator="equal">
 *               <param name="cache.cachefile"     value="cache.properties"/>
 *               <param name="algorithm.algorithm" value="MD5"/>
 *           </modified>
 *       </filelist>
 *   </copy>
 * 

* *

And the same rewritten as CustomSelector would be

 *   <copy todir="dest">
 *       <filelist dir="src">
 *           <custom class="org.apache.tools.ant.type.selectors.ModifiedSelector">
 *               <param name="update"     value="true"/>
 *               <param name="cache"      value="propertyfile"/>
 *               <param name="algorithm"  value="digest"/>
 *               <param name="comparator" value="equal"/>
 *               <param name="cache.cachefile"     value="cache.properties"/>
 *               <param name="algorithm.algorithm" value="MD5"/>
 *           </custom>
 *       </filelist>
 *   </copy>
 * 

* *

If you want to provide your own interface implementation you can do * that via the *classname attributes. If the classes are not on Ant's core * classpath, you will have to provide the path via nested <classpath> * element, so that the selector can find the classes.

 *   <modified cacheclassname="com.mycompany.MyCache">
 *       <classpath>
 *           <pathelement location="lib/mycompany-antutil.jar"/>
 *       </classpath>
 *   </modified>
 * 

* *

All these three examples copy the files from src to dest * using the ModifiedSelector. The ModifiedSelector uses the PropertyfileCache * , the DigestAlgorithm and the EqualComparator for its * work. The PropertyfileCache stores key-value-pairs in a simple java * properties file. The filename is cache.properties. The update * flag lets the selector update the values in the cache (and on first call * creates the cache). The DigestAlgorithm computes a hashvalue using the * java.security.MessageDigest class with its MD5-Algorithm and its standard * provider. The new computed hashvalue and the stored one are compared by * the EqualComparator which returns 'true' (more correct a value not * equals zero (1)) if the values are not the same using simple String * comparison.

* *

A useful scenario for this selector is inside a build environment * for homepage generation (e.g. with * Apache Forrest).

 * <target name="generate-and-upload-site">
 *     <echo> generate the site using forrest </echo>
 *     <antcall target="site"/>
 *
 *     <echo> upload the changed files </echo>
 *     <ftp server="${ftp.server}" userid="${ftp.user}" password="${ftp.pwd}">
 *         <fileset dir="htdocs/manual">
 *             <modified/>
 *         </fileset>
 *     </ftp>
 * </target>
 * 
Here all changed files are uploaded to the server. The * ModifiedSelector saves therefore much upload time.

* * *

This selector uses reflection for setting the values of its three interfaces * (using org.apache.tools.ant.IntrospectionHelper) therefore no special * 'configuration interfaces' has to be implemented by new caches, algorithms or * comparators. All present setXX methods can be used. E.g. the DigestAlgorithm * can use a specified provider for computing its value. For selecting this * there is a setProvider(String providername) method. So you can use * a nested <param name="algorithm.provider" value="MyProvider"/>. * * * @since Ant 1.6 */ public class ModifiedSelector extends BaseExtendSelector implements BuildListener, ResourceSelector { private static final String CACHE_PREFIX = "cache."; private static final String ALGORITHM_PREFIX = "algorithm."; private static final String COMPARATOR_PREFIX = "comparator."; // ----- attributes ----- /** Cache name for later instantiation. */ private CacheName cacheName = null; /** User specified classname for Cache. */ private String cacheClass; /** Algorithm name for later instantiation. */ private AlgorithmName algoName = null; /** User specified classname for Algorithm. */ private String algorithmClass; /** Comparator name for later instantiation. */ private ComparatorName compName = null; /** User specified classname for Comparator. */ private String comparatorClass; /** Should the cache be updated? */ private boolean update = true; /** Are directories selected? */ private boolean selectDirectories = true; /** * Should Resources whithout an InputStream, and * therefore without checking, be selected? */ private boolean selectResourcesWithoutInputStream = true; /** Delay the writing of the cache file */ private boolean delayUpdate = true; // ----- internal member variables ----- /** How should the cached value and the new one compared? */ private Comparator comparator = null; /** Algorithm for computing new values and updating the cache. */ private Algorithm algorithm = null; /** The Cache containing the old values. */ private Cache cache = null; /** Count of modified properties */ private int modified = 0; /** Flag whether this object is configured. Configuration is only done once. */ private boolean isConfigured = false; /** * Parameter vector with parameters for later initialization. * @see #configure */ private Vector configParameter = new Vector(); /** * Parameter vector with special parameters for later initialization. * The names have the pattern '*.*', e.g. 'cache.cachefile'. * These parameters are used after the parameters with the pattern '*'. * @see #configure */ private Vector specialParameter = new Vector(); /** The classloader of this class. */ private ClassLoader myClassLoader = null; /** provided classpath for the classloader */ private Path classpath = null; // ----- constructors ----- /** Bean-Constructor. */ public ModifiedSelector() { } // ----- configuration ----- /** Overrides BaseSelector.verifySettings(). */ public void verifySettings() { configure(); if (cache == null) { setError("Cache must be set."); } else if (algorithm == null) { setError("Algorithm must be set."); } else if (!cache.isValid()) { setError("Cache must be proper configured."); } else if (!algorithm.isValid()) { setError("Algorithm must be proper configured."); } } /** * Configures this Selector. * Does this work only once per Selector object. *

Because some problems while configuring from Selector * the configuration is done in the following order:

    *
  1. collect the configuration data
  2. *
  3. wait for the first isSelected() call
  4. *
  5. set the default values
  6. *
  7. set values for name pattern '*': update, cache, algorithm, comparator
  8. *
  9. set values for name pattern '*.*: cache.cachefile, ...
  10. *

*

This configuration algorithm is needed because you don't know * the order of arriving config-data. E.g. if you first set the * cache.cachefilename and after that the cache itself, * the default value for cachefilename is used, because setting the * cache implies creating a new Cache instance - with its defaults.

*/ public void configure() { // // ----- The "Singleton" ----- // if (isConfigured) { return; } isConfigured = true; // // ----- Set default values ----- // Project p = getProject(); String filename = "cache.properties"; File cachefile = null; if (p != null) { // normal use inside Ant cachefile = new File(p.getBaseDir(), filename); // set self as a BuildListener to delay cachefile saves getProject().addBuildListener(this); } else { // no reference to project - e.g. during normal JUnit tests cachefile = new File(filename); setDelayUpdate(false); } Cache defaultCache = new PropertiesfileCache(cachefile); Algorithm defaultAlgorithm = new DigestAlgorithm(); Comparator defaultComparator = new EqualComparator(); // // ----- Set the main attributes, pattern '*' ----- // for (Parameter parameter : configParameter) { if (parameter.getName().indexOf(".") > 0) { // this is a *.* parameter for later use specialParameter.add(parameter); } else { useParameter(parameter); } } configParameter = new Vector(); // specify the algorithm classname if (algoName != null) { // use Algorithm defined via name if ("hashvalue".equals(algoName.getValue())) { algorithm = new HashvalueAlgorithm(); } else if ("digest".equals(algoName.getValue())) { algorithm = new DigestAlgorithm(); } else if ("checksum".equals(algoName.getValue())) { algorithm = new ChecksumAlgorithm(); } } else { if (algorithmClass != null) { // use Algorithm specified by classname algorithm = loadClass( algorithmClass, "is not an Algorithm.", Algorithm.class); } else { // nothing specified - use default algorithm = defaultAlgorithm; } } // specify the cache classname if (cacheName != null) { // use Cache defined via name if ("propertyfile".equals(cacheName.getValue())) { cache = new PropertiesfileCache(); } } else { if (cacheClass != null) { // use Cache specified by classname cache = loadClass(cacheClass, "is not a Cache.", Cache.class); } else { // nothing specified - use default cache = defaultCache; } } // specify the comparator classname if (compName != null) { // use Algorithm defined via name if ("equal".equals(compName.getValue())) { comparator = new EqualComparator(); } else if ("rule".equals(compName.getValue())) { // TODO there is a problem with the constructor for the RBC. // you have to provide the rules in the constructors - no setters // available. throw new BuildException("RuleBasedCollator not yet supported."); // Have to think about lazy initialization here... JHM // comparator = new java.text.RuleBasedCollator(); } } else { if (comparatorClass != null) { // use Algorithm specified by classname @SuppressWarnings("unchecked") Comparator localComparator = loadClass(comparatorClass, "is not a Comparator.", Comparator.class); comparator = localComparator; } else { // nothing specified - use default comparator = defaultComparator; } } // // ----- Set the special attributes, pattern '*.*' ----- // for (Iterator itSpecial = specialParameter.iterator(); itSpecial.hasNext();) { Parameter par = itSpecial.next(); useParameter(par); } specialParameter = new Vector(); } /** * Loads the specified class and initializes an object of that class. * Throws a BuildException using the given message if an error occurs during * loading/instantiation or if the object is not from the given type. * @param classname the classname * @param msg the message-part for the BuildException * @param type the type to check against * @return a castable object */ protected T loadClass(String classname, String msg, Class type) { try { // load the specified class ClassLoader cl = getClassLoader(); Class clazz = null; if (cl != null) { clazz = cl.loadClass(classname); } else { clazz = Class.forName(classname); } Object rv = clazz.newInstance(); if (!type.isInstance(rv)) { throw new BuildException("Specified class (" + classname + ") " + msg); } return (T) rv; } catch (ClassNotFoundException e) { throw new BuildException("Specified class (" + classname + ") not found."); } catch (Exception e) { throw new BuildException(e); } } // ----- the selection work ----- /** * Implementation of ResourceSelector.isSelected(). * * @param resource The resource to check * @return whether the resource is selected * @see ResourceSelector#isSelected(Resource) */ public boolean isSelected(Resource resource) { if (resource.isFilesystemOnly()) { // We have a 'resourced' file, so reconvert it and use // the 'old' implementation. FileResource fileResource = (FileResource) resource; File file = fileResource.getFile(); String filename = fileResource.getName(); File basedir = fileResource.getBaseDir(); return isSelected(basedir, filename, file); } else { try { // How to handle non-file-Resources? I copy temporarily the // resource to a file and use the file-implementation. FileUtils fu = FileUtils.getFileUtils(); File tmpFile = fu.createTempFile("modified-", ".tmp", null, true, false); Resource tmpResource = new FileResource(tmpFile); ResourceUtils.copyResource(resource, tmpResource); boolean isSelected = isSelected(tmpFile.getParentFile(), tmpFile.getName(), resource.toLongString()); tmpFile.delete(); return isSelected; } catch (UnsupportedOperationException uoe) { log("The resource '" + resource.getName() + "' does not provide an InputStream, so it is not checked. " + "Akkording to 'selres' attribute value it is " + ((selectResourcesWithoutInputStream) ? "" : " not") + "selected.", Project.MSG_INFO); return selectResourcesWithoutInputStream; } catch (Exception e) { throw new BuildException(e); } } } /** * Implementation of BaseExtendSelector.isSelected(). * * @param basedir as described in BaseExtendSelector * @param filename as described in BaseExtendSelector * @param file as described in BaseExtendSelector * @return as described in BaseExtendSelector */ public boolean isSelected(File basedir, String filename, File file) { return isSelected(basedir, filename, file.getAbsolutePath()); } /** * The business logic of this selector for use as ResourceSelector of * FileSelector. * * @param basedir as described in BaseExtendSelector * @param filename as described in BaseExtendSelector * @param cacheKey the name for the key for storing the hashvalue * @return true if the file is selected otherwise false */ private boolean isSelected(File basedir, String filename, String cacheKey) { validate(); File f = new File(basedir, filename); // You can not compute a value for a directory if (f.isDirectory()) { return selectDirectories; } // Get the values and do the comparison String cachedValue = String.valueOf(cache.get(f.getAbsolutePath())); String newValue = algorithm.getValue(f); boolean rv = (comparator.compare(cachedValue, newValue) != 0); // Maybe update the cache if (update && rv) { cache.put(f.getAbsolutePath(), newValue); setModified(getModified() + 1); if (!getDelayUpdate()) { saveCache(); } } return rv; } /** * save the cache file */ protected void saveCache() { if (getModified() > 0) { cache.save(); setModified(0); } } // ----- attribute and nested element support ----- /** * Setter for algorithmClass. * @param classname new value */ public void setAlgorithmClass(String classname) { algorithmClass = classname; } /** * Setter for comparatorClass. * @param classname new value */ public void setComparatorClass(String classname) { comparatorClass = classname; } /** * Setter for cacheClass. * @param classname new value */ public void setCacheClass(String classname) { cacheClass = classname; } /** * Support for update attribute. * @param update new value */ public void setUpdate(boolean update) { this.update = update; } /** * Support for seldirs attribute. * @param seldirs new value */ public void setSeldirs(boolean seldirs) { selectDirectories = seldirs; } /** * Support for selres attribute. * @param newValue the new value */ public void setSelres(boolean newValue) { this.selectResourcesWithoutInputStream = newValue; } /** * Getter for the modified count * @return modified count */ public int getModified() { return modified; } /** * Setter for the modified count * @param modified count */ public void setModified(int modified) { this.modified = modified; } /** * Getter for the delay update * @return true if we should delay for performance */ public boolean getDelayUpdate() { return delayUpdate; } /** * Setter for the delay update * @param delayUpdate true if we should delay for performance */ public void setDelayUpdate(boolean delayUpdate) { this.delayUpdate = delayUpdate; } /** * Add the classpath. * @param path the classpath */ public void addClasspath(Path path) { if (classpath != null) { throw new BuildException(" can be set only once."); } classpath = path; } /** * Returns and initializes the classloader for this class. * @return the classloader */ public ClassLoader getClassLoader() { if (myClassLoader == null) { myClassLoader = (classpath == null) // the usual classloader ? getClass().getClassLoader() // additional use the provided classpath // Memory leak in line below : getProject().createClassLoader(classpath); } return myClassLoader; } /** * Set the used ClassLoader. * If you invoke this selector by API (e.g. inside some testcases) the selector * will use a different classloader for loading the interface implementations than * the caller. Therefore you will get a ClassCastException if you get the * implementations from the selector and cast them. * @param loader the ClassLoader to use */ public void setClassLoader(ClassLoader loader) { myClassLoader = loader; } /** * Support for nested <param> tags. * @param key the key of the parameter * @param value the value of the parameter */ public void addParam(String key, Object value) { Parameter par = new Parameter(); par.setName(key); par.setValue(String.valueOf(value)); configParameter.add(par); } /** * Support for nested <param> tags. * @param parameter the parameter object */ public void addParam(Parameter parameter) { configParameter.add(parameter); } /** * Defined in org.apache.tools.ant.types.Parameterizable. * Overwrite implementation in superclass because only special * parameters are valid. * @see #addParam(String,Object). * @param parameters the parameters to set. */ public void setParameters(Parameter[] parameters) { if (parameters != null) { for (int i = 0; i < parameters.length; i++) { configParameter.add(parameters[i]); } } } /** * Support for nested tags. * Parameter named cache, algorithm, * comparator or update are mapped to * the respective set-Method. * Parameter which names starts with cache. or * algorithm. or comparator. are tried * to set on the appropriate object via its set-methods. * Other parameters are invalid and an BuildException will * be thrown. * * @param parameter Key and value as parameter object */ public void useParameter(Parameter parameter) { String key = parameter.getName(); String value = parameter.getValue(); if ("cache".equals(key)) { CacheName cn = new CacheName(); cn.setValue(value); setCache(cn); } else if ("algorithm".equals(key)) { AlgorithmName an = new AlgorithmName(); an.setValue(value); setAlgorithm(an); } else if ("comparator".equals(key)) { ComparatorName cn = new ComparatorName(); cn.setValue(value); setComparator(cn); } else if ("update".equals(key)) { boolean updateValue = ("true".equalsIgnoreCase(value)) ? true : false; setUpdate(updateValue); } else if ("delayupdate".equals(key)) { boolean updateValue = ("true".equalsIgnoreCase(value)) ? true : false; setDelayUpdate(updateValue); } else if ("seldirs".equals(key)) { boolean sdValue = ("true".equalsIgnoreCase(value)) ? true : false; setSeldirs(sdValue); } else if (key.startsWith(CACHE_PREFIX)) { String name = key.substring(CACHE_PREFIX.length()); tryToSetAParameter(cache, name, value); } else if (key.startsWith(ALGORITHM_PREFIX)) { String name = key.substring(ALGORITHM_PREFIX.length()); tryToSetAParameter(algorithm, name, value); } else if (key.startsWith(COMPARATOR_PREFIX)) { String name = key.substring(COMPARATOR_PREFIX.length()); tryToSetAParameter(comparator, name, value); } else { setError("Invalid parameter " + key); } } /** * Try to set a value on an object using reflection. * Helper method for easier access to IntrospectionHelper.setAttribute(). * @param obj the object on which the attribute should be set * @param name the attributename * @param value the new value */ protected void tryToSetAParameter(Object obj, String name, String value) { Project prj = (getProject() != null) ? getProject() : new Project(); IntrospectionHelper iHelper = IntrospectionHelper.getHelper(prj, obj.getClass()); try { iHelper.setAttribute(prj, obj, name, value); } catch (org.apache.tools.ant.BuildException e) { // no-op } } // ----- 'beautiful' output ----- /** * Override Object.toString(). * @return information about this selector */ public String toString() { StringBuffer buf = new StringBuffer("{modifiedselector"); buf.append(" update=").append(update); buf.append(" seldirs=").append(selectDirectories); buf.append(" cache=").append(cache); buf.append(" algorithm=").append(algorithm); buf.append(" comparator=").append(comparator); buf.append("}"); return buf.toString(); } // ----- BuildListener interface methods ----- /** * Signals that the last target has finished. * @param event received BuildEvent */ public void buildFinished(BuildEvent event) { if (getDelayUpdate()) { saveCache(); } } /** * Signals that a target has finished. * @param event received BuildEvent */ public void targetFinished(BuildEvent event) { if (getDelayUpdate()) { saveCache(); } } /** * Signals that a task has finished. * @param event received BuildEvent */ public void taskFinished(BuildEvent event) { if (getDelayUpdate()) { saveCache(); } } /** * Signals that a build has started. * @param event received BuildEvent */ public void buildStarted(BuildEvent event) { // no-op } /** * Signals that a target is starting. * @param event received BuildEvent */ public void targetStarted(BuildEvent event) { // no-op } /** * Signals that a task is starting. * @param event received BuildEvent */ public void taskStarted(BuildEvent event) { // no-op } /** * Signals a message logging event. * @param event received BuildEvent */ public void messageLogged(BuildEvent event) { // no-op } // The EnumeratedAttributes for the three interface implementations. // Name-Classname mapping is done in the configure() method. /** * Get the cache type to use. * @return the enumerated cache type */ public Cache getCache() { return cache; } /** * Set the cache type to use. * @param name an enumerated cache type. */ public void setCache(CacheName name) { cacheName = name; } /** * The enumerated type for cache. * The values are "propertyfile". */ public static class CacheName extends EnumeratedAttribute { /** * {@inheritDoc} * @see EnumeratedAttribute#getValues() */ public String[] getValues() { return new String[] {"propertyfile" }; } } /** * Get the algorithm type to use. * @return the enumerated algorithm type */ public Algorithm getAlgorithm() { return algorithm; } /** * Set the algorithm type to use. * @param name an enumerated algorithm type. */ public void setAlgorithm(AlgorithmName name) { algoName = name; } /** * The enumerated type for algorithm. * The values are "hashValue", "digest" and "checksum". */ public static class AlgorithmName extends EnumeratedAttribute { /** * {@inheritDoc} * @see EnumeratedAttribute#getValues() */ public String[] getValues() { return new String[] {"hashvalue", "digest", "checksum" }; } } /** * Get the comparator type to use. * @return the enumerated comparator type */ public Comparator getComparator() { return comparator; } /** * Set the comparator type to use. * @param name an enumerated comparator type. */ public void setComparator(ComparatorName name) { compName = name; } /** * The enumerated type for algorithm. * The values are "equal" and "rule". */ public static class ComparatorName extends EnumeratedAttribute { /** * {@inheritDoc} * @see EnumeratedAttribute#getValues() */ public String[] getValues() { return new String[] {"equal", "rule" }; } } }