/* * 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; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.tools.ant.taskdefs.PreSetDef; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.resources.FileProvider; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.util.StringUtils; /** * Helper class that collects the methods a task or nested element * holds to set attributes, create nested elements or hold PCDATA * elements. * * It contains hashtables containing classes that use introspection * to handle all the invocation of the project-component specific methods. * * This class is somewhat complex, as it implements the O/X mapping between * Ant XML and Java class instances. This is not the best place for someone new * to Ant to start contributing to the codebase, as a change here can break the * entire system in interesting ways. Always run a full test of Ant before checking * in/submitting changes to this file. * * The class is final and has a private constructor. * To get an instance for a specific (class,project) combination, * use {@link #getHelper(Project,Class)}. * This may return an existing version, or a new one * ...do not make any assumptions about its uniqueness, or its validity after the Project * instance has finished its build. * */ public final class IntrospectionHelper { /** * Helper instances we've already created (Class.getName() to IntrospectionHelper). */ private static final Map<String, IntrospectionHelper> HELPERS = new Hashtable<>(); /** * Map from primitive types to wrapper classes for use in * createAttributeSetter (Class to Class). Note that char * and boolean are in here even though they get special treatment * - this way we only need to test for the wrapper class. */ private static final Map<Class<?>, Class<?>> PRIMITIVE_TYPE_MAP = new HashMap<>(8); // Set up PRIMITIVE_TYPE_MAP static { final Class<?>[] primitives = {Boolean.TYPE, Byte.TYPE, Character.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE}; final Class<?>[] wrappers = {Boolean.class, Byte.class, Character.class, Short.class, Integer.class, Long.class, Float.class, Double.class}; for (int i = 0; i < primitives.length; i++) { PRIMITIVE_TYPE_MAP.put (primitives[i], wrappers[i]); } } private static final int MAX_REPORT_NESTED_TEXT = 20; private static final String ELLIPSIS = "..."; /** * Map from attribute names to attribute types * (String to Class). */ private final Map<String, Class<?>> attributeTypes = new Hashtable<>(); /** * Map from attribute names to attribute setter methods * (String to AttributeSetter). */ private final Map<String, AttributeSetter> attributeSetters = new Hashtable<>(); /** * Map from attribute names to nested types * (String to Class). */ private final Map<String, Class<?>> nestedTypes = new Hashtable<>(); /** * Map from attribute names to methods to create nested types * (String to NestedCreator). */ private final Map<String, NestedCreator> nestedCreators = new Hashtable<>(); /** * Vector of methods matching add[Configured](Class) pattern. */ private final List<Method> addTypeMethods = new ArrayList<>(); /** * The method to invoke to add PCDATA. */ private final Method addText; /** * The class introspected by this instance. */ private final Class<?> bean; /** * Sole constructor, which is private to ensure that all * IntrospectionHelpers are created via {@link #getHelper(Class) getHelper}. * Introspects the given class for bean-like methods. * Each method is examined in turn, and the following rules are applied: * <p> * <ul> * <li>If the method is <code>Task.setLocation(Location)</code>, * <code>Task.setTaskType(String)</code> * or <code>TaskContainer.addTask(Task)</code>, it is ignored. These * methods are handled differently elsewhere. * <li><code>void addText(String)</code> is recognised as the method for * adding PCDATA to a bean. * <li><code>void setFoo(Bar)</code> is recognised as a method for * setting the value of attribute <code>foo</code>, so long as * <code>Bar</code> is non-void and is not an array type. * As of Ant 1.8, a Resource or FileProvider parameter overrides a java.io.File parameter; * in practice the only effect of this is to allow objects rendered from * the 1.8 PropertyHelper implementation to be used as Resource parameters, * since Resources set from Strings are resolved as project-relative files * to preserve backward compatibility. Beyond this, non-String * parameter types always overload String parameter types; these are * the only guarantees made in terms of priority. * <li><code>Foo createBar()</code> is recognised as a method for * creating a nested element called <code>bar</code> of type * <code>Foo</code>, so long as <code>Foo</code> is not a primitive or * array type. * <li><code>void addConfiguredFoo(Bar)</code> is recognised as a * method for storing a pre-configured element called * <code>foo</code> and of type <code>Bar</code>, so long as * <code>Bar</code> is not an array, primitive or String type. * <code>Bar</code> must have an accessible constructor taking no * arguments. * <li><code>void addFoo(Bar)</code> is recognised as a method for storing * an element called <code>foo</code> and of type <code>Bar</code>, so * long as <code>Bar</code> is not an array, primitive or String type. * <code>Bar</code> must have an accessible constructor taking no * arguments. This is distinct from the 'addConfigured' idiom in that * the nested element is added to the parent immediately after it is * constructed; in practice this means that <code>addFoo(Bar)</code> should * do little or nothing with its argument besides storing it for later use. * </ul> * Note that only one method is retained to create/set/addConfigured/add * any element or attribute. * * @param bean The bean type to introspect. * Must not be <code>null</code>. * * @see #getHelper(Class) */ private IntrospectionHelper(final Class<?> bean) { this.bean = bean; final Method[] methods = bean.getMethods(); Method addTextMethod = null; for (int i = 0; i < methods.length; i++) { final Method m = methods[i]; final String name = m.getName(); final Class<?> returnType = m.getReturnType(); final Class<?>[] args = m.getParameterTypes(); // check of add[Configured](Class) pattern if (args.length == 1 && java.lang.Void.TYPE.equals(returnType) && ("add".equals(name) || "addConfigured".equals(name))) { insertAddTypeMethod(m); continue; } // not really user settable properties on tasks/project components if (org.apache.tools.ant.ProjectComponent.class.isAssignableFrom(bean) && args.length == 1 && isHiddenSetMethod(name, args[0])) { continue; } // hide addTask for TaskContainers if (isContainer() && args.length == 1 && "addTask".equals(name) && org.apache.tools.ant.Task.class.equals(args[0])) { continue; } if ("addText".equals(name) && java.lang.Void.TYPE.equals(returnType) && args.length == 1 && java.lang.String.class.equals(args[0])) { addTextMethod = methods[i]; } else if (name.startsWith("set") && java.lang.Void.TYPE.equals(returnType) && args.length == 1 && !args[0].isArray()) { final String propName = getPropertyName(name, "set"); AttributeSetter as = attributeSetters.get(propName); if (as != null) { if (java.lang.String.class.equals(args[0])) { /* Ignore method m, as there is an overloaded form of this method that takes in a non-string argument, which gains higher priority. */ continue; } if (java.io.File.class.equals(args[0])) { // Ant Resources/FileProviders override java.io.File if (Resource.class.equals(as.type) || FileProvider.class.equals(as.type)) { continue; } } /* In cases other than those just explicitly covered, we just override that with the new one. This mechanism does not guarantee any specific order in which the methods will be selected: so any code that depends on the order in which "set" methods have been defined, is not guaranteed to be selected in any particular order. */ } as = createAttributeSetter(m, args[0], propName); if (as != null) { attributeTypes.put(propName, args[0]); attributeSetters.put(propName, as); } } else if (name.startsWith("create") && !returnType.isArray() && !returnType.isPrimitive() && args.length == 0) { final String propName = getPropertyName(name, "create"); // Check if a create of this property is already present // add takes preference over create for CB purposes if (nestedCreators.get(propName) == null) { nestedTypes.put(propName, returnType); nestedCreators.put(propName, new CreateNestedCreator(m)); } } else if (name.startsWith("addConfigured") && java.lang.Void.TYPE.equals(returnType) && args.length == 1 && !java.lang.String.class.equals(args[0]) && !args[0].isArray() && !args[0].isPrimitive()) { try { Constructor<?> constructor = null; try { constructor = args[0].getConstructor(); } catch (final NoSuchMethodException ex) { constructor = args[0].getConstructor(Project.class); } final String propName = getPropertyName(name, "addConfigured"); nestedTypes.put(propName, args[0]); nestedCreators.put(propName, new AddNestedCreator(m, constructor, AddNestedCreator.ADD_CONFIGURED)); } catch (final NoSuchMethodException nse) { // ignore } } else if (name.startsWith("add") && java.lang.Void.TYPE.equals(returnType) && args.length == 1 && !java.lang.String.class.equals(args[0]) && !args[0].isArray() && !args[0].isPrimitive()) { try { Constructor<?> constructor = null; try { constructor = args[0].getConstructor(); } catch (final NoSuchMethodException ex) { constructor = args[0].getConstructor(Project.class); } final String propName = getPropertyName(name, "add"); if (nestedTypes.get(propName) != null) { /* * Ignore this method as there is an addConfigured * form of this method that has a higher * priority */ continue; } nestedTypes.put(propName, args[0]); nestedCreators.put(propName, new AddNestedCreator(m, constructor, AddNestedCreator.ADD)); } catch (final NoSuchMethodException nse) { // ignore } } } addText = addTextMethod; } /** * Certain set methods are part of the Ant core interface to tasks and * therefore not to be considered for introspection * * @param name the name of the set method * @param type the type of the set method's parameter * @return true if the given set method is to be hidden. */ private boolean isHiddenSetMethod(final String name, final Class<?> type) { if ("setLocation".equals(name) && org.apache.tools.ant.Location.class.equals(type)) { return true; } if ("setTaskType".equals(name) && java.lang.String.class.equals(type)) { return true; } return false; } /** * Returns a helper for the given class, either from the cache * or by creating a new instance. * * @param c The class for which a helper is required. * Must not be <code>null</code>. * * @return a helper for the specified class */ public static synchronized IntrospectionHelper getHelper(final Class<?> c) { return getHelper(null, c); } /** * Returns a helper for the given class, either from the cache * or by creating a new instance. * * The method will make sure the helper will be cleaned up at the end of * the project, and only one instance will be created for each class. * * @param p the project instance. Can be null, in which case the helper is not cached. * @param c The class for which a helper is required. * Must not be <code>null</code>. * * @return a helper for the specified class */ public static synchronized IntrospectionHelper getHelper(final Project p, final Class<?> c) { IntrospectionHelper ih = HELPERS.get(c.getName()); // If a helper cannot be found, or if the helper is for another // classloader, create a new IH if (ih == null || ih.bean != c) { ih = new IntrospectionHelper(c); if (p != null) { // #30162: do *not* cache this if there is no project, as we // cannot guarantee that the cache will be cleared. HELPERS.put(c.getName(), ih); } } return ih; } /** * Sets the named attribute in the given element, which is part of the * given project. * * @param p The project containing the element. This is used when files * need to be resolved. Must not be <code>null</code>. * @param element The element to set the attribute in. Must not be * <code>null</code>. * @param attributeName The name of the attribute to set. Must not be * <code>null</code>. * @param value The value to set the attribute to. This may be interpreted * or converted to the necessary type if the setter method * doesn't accept an object of the supplied type. * * @exception BuildException if the introspected class doesn't support * the given attribute, or if the setting * method fails. */ public void setAttribute(final Project p, final Object element, final String attributeName, final Object value) throws BuildException { final AttributeSetter as = attributeSetters.get( attributeName.toLowerCase(Locale.ENGLISH)); if (as == null && value != null) { if (element instanceof DynamicAttributeNS) { final DynamicAttributeNS dc = (DynamicAttributeNS) element; final String uriPlusPrefix = ProjectHelper.extractUriFromComponentName(attributeName); final String uri = ProjectHelper.extractUriFromComponentName(uriPlusPrefix); final String localName = ProjectHelper.extractNameFromComponentName(attributeName); final String qName = "".equals(uri) ? localName : uri + ":" + localName; dc.setDynamicAttribute(uri, localName, qName, value.toString()); return; } if (element instanceof DynamicObjectAttribute) { final DynamicObjectAttribute dc = (DynamicObjectAttribute) element; dc.setDynamicAttribute(attributeName.toLowerCase(Locale.ENGLISH), value); return; } if (element instanceof DynamicAttribute) { final DynamicAttribute dc = (DynamicAttribute) element; dc.setDynamicAttribute(attributeName.toLowerCase(Locale.ENGLISH), value.toString()); return; } if (attributeName.indexOf(':') >= 0) { return; // Ignore attribute from unknown uri's } final String msg = getElementName(p, element) + " doesn't support the \"" + attributeName + "\" attribute."; throw new UnsupportedAttributeException(msg, attributeName); } if (as != null) { // possible if value == null try { as.setObject(p, element, value); } catch (final IllegalAccessException ie) { // impossible as getMethods should only return public methods throw new BuildException(ie); } catch (final InvocationTargetException ite) { throw extractBuildException(ite); } } } /** * Sets the named attribute in the given element, which is part of the * given project. * * @param p The project containing the element. This is used when files * need to be resolved. Must not be <code>null</code>. * @param element The element to set the attribute in. Must not be * <code>null</code>. * @param attributeName The name of the attribute to set. Must not be * <code>null</code>. * @param value The value to set the attribute to. This may be interpreted * or converted to the necessary type if the setter method * doesn't just take a string. Must not be <code>null</code>. * * @exception BuildException if the introspected class doesn't support * the given attribute, or if the setting * method fails. */ public void setAttribute(final Project p, final Object element, final String attributeName, final String value) throws BuildException { setAttribute(p, element, attributeName, (Object) value); } /** * Adds PCDATA to an element, using the element's * <code>void addText(String)</code> method, if it has one. If no * such method is present, a BuildException is thrown if the * given text contains non-whitespace. * * @param project The project which the element is part of. * Must not be <code>null</code>. * @param element The element to add the text to. * Must not be <code>null</code>. * @param text The text to add. * Must not be <code>null</code>. * * @exception BuildException if non-whitespace text is provided and no * method is available to handle it, or if * the handling method fails. */ public void addText(final Project project, final Object element, String text) throws BuildException { if (addText == null) { text = text.trim(); // Element doesn't handle text content if (text.length() == 0) { // Only whitespace - ignore return; } // Not whitespace - fail throw new BuildException(project.getElementName(element) + " doesn't support nested text data (\"" + condenseText(text) + "\")."); } try { addText.invoke(element, new Object[] {text}); } catch (final IllegalAccessException ie) { // impossible as getMethods should only return public methods throw new BuildException(ie); } catch (final InvocationTargetException ite) { throw extractBuildException(ite); } } /** * part of the error message created by {@link #throwNotSupported * throwNotSupported}. * @since Ant 1.8.0 */ protected static final String NOT_SUPPORTED_CHILD_PREFIX = " doesn't support the nested \""; /** * part of the error message created by {@link #throwNotSupported * throwNotSupported}. * @since Ant 1.8.0 */ protected static final String NOT_SUPPORTED_CHILD_POSTFIX = "\" element."; /** * Utility method to throw a NotSupported exception * * @param project the Project instance. * @param parent the object which doesn't support a requested element * @param elementName the name of the Element which is trying to be created. */ public void throwNotSupported(final Project project, final Object parent, final String elementName) { final String msg = project.getElementName(parent) + NOT_SUPPORTED_CHILD_PREFIX + elementName + NOT_SUPPORTED_CHILD_POSTFIX; throw new UnsupportedElementException(msg, elementName); } /** * Get the specific NestedCreator for a given project/parent/element combination * @param project ant project * @param parentUri URI of the parent. * @param parent the parent class * @param elementName element to work with. This can contain * a URI,localname tuple of of the form uri:localname * @param child the bit of XML to work with * @return a nested creator that can handle the child elements. * @throws BuildException if the parent does not support child elements of that name */ private NestedCreator getNestedCreator( final Project project, String parentUri, final Object parent, final String elementName, final UnknownElement child) throws BuildException { String uri = ProjectHelper.extractUriFromComponentName(elementName); final String name = ProjectHelper.extractNameFromComponentName(elementName); if (uri.equals(ProjectHelper.ANT_CORE_URI)) { uri = ""; } if (parentUri.equals(ProjectHelper.ANT_CORE_URI)) { parentUri = ""; } NestedCreator nc = null; if (uri.equals(parentUri) || uri.length() == 0) { nc = nestedCreators.get(name.toLowerCase(Locale.ENGLISH)); } if (nc == null) { nc = createAddTypeCreator(project, parent, elementName); } if (nc == null && (parent instanceof DynamicElementNS || parent instanceof DynamicElement) ) { final String qName = child == null ? name : child.getQName(); final Object nestedElement = createDynamicElement(parent, child == null ? "" : child.getNamespace(), name, qName); if (nestedElement != null) { nc = new NestedCreator(null) { @Override Object create(final Project project, final Object parent, final Object ignore) { return nestedElement; } }; } } if (nc == null) { throwNotSupported(project, parent, elementName); } return nc; } /** * Invokes the "correct" createDynamicElement method on parent in * order to obtain a child element by name. * * @since Ant 1.8.0. */ private Object createDynamicElement(final Object parent, final String ns, final String localName, final String qName) { Object nestedElement = null; if (parent instanceof DynamicElementNS) { final DynamicElementNS dc = (DynamicElementNS) parent; nestedElement = dc.createDynamicElement(ns, localName, qName); } if (nestedElement == null && parent instanceof DynamicElement) { final DynamicElement dc = (DynamicElement) parent; nestedElement = dc.createDynamicElement(localName.toLowerCase(Locale.ENGLISH)); } return nestedElement; } /** * Creates a named nested element. Depending on the results of the * initial introspection, either a method in the given parent instance * or a simple no-arg constructor is used to create an instance of the * specified element type. * * @param project Project to which the parent object belongs. * Must not be <code>null</code>. If the resulting * object is an instance of ProjectComponent, its * Project reference is set to this parameter value. * @param parent Parent object used to create the instance. * Must not be <code>null</code>. * @param elementName Name of the element to create an instance of. * Must not be <code>null</code>. * * @return an instance of the specified element type * @deprecated since 1.6.x. * This is not a namespace aware method. * * @exception BuildException if no method is available to create the * element instance, or if the creating method fails. */ @Deprecated public Object createElement(final Project project, final Object parent, final String elementName) throws BuildException { final NestedCreator nc = getNestedCreator(project, "", parent, elementName, null); try { final Object nestedElement = nc.create(project, parent, null); if (project != null) { project.setProjectReference(nestedElement); } return nestedElement; } catch (final IllegalAccessException ie) { // impossible as getMethods should only return public methods throw new BuildException(ie); } catch (final InstantiationException ine) { // impossible as getMethods should only return public methods throw new BuildException(ine); } catch (final InvocationTargetException ite) { throw extractBuildException(ite); } } /** * returns an object that creates and stores an object * for an element of a parent. * * @param project Project to which the parent object belongs. * @param parentUri The namespace uri of the parent object. * @param parent Parent object used to create the creator object to * create and store and instance of a subelement. * @param elementName Name of the element to create an instance of. * @param ue The unknown element associated with the element. * @return a creator object to create and store the element instance. */ public Creator getElementCreator( final Project project, final String parentUri, final Object parent, final String elementName, final UnknownElement ue) { final NestedCreator nc = getNestedCreator(project, parentUri, parent, elementName, ue); return new Creator(project, parent, nc); } /** * Indicates whether the introspected class is a dynamic one, * supporting arbitrary nested elements and/or attributes. * * @return <div><code>true</code> if the introspected class is dynamic; * <code>false</code> otherwise.</div> * @since Ant 1.6.3 * * @see DynamicElement * @see DynamicElementNS */ public boolean isDynamic() { return DynamicElement.class.isAssignableFrom(bean) || DynamicElementNS.class.isAssignableFrom(bean); } /** * Indicates whether the introspected class is a task container, * supporting arbitrary nested tasks/types. * * @return <code>true</code> if the introspected class is a container; * <code>false</code> otherwise. * @since Ant 1.6.3 * * @see TaskContainer */ public boolean isContainer() { return TaskContainer.class.isAssignableFrom(bean); } /** * Indicates if this element supports a nested element of the * given name. * * @param elementName the name of the nested element being checked * * @return true if the given nested element is supported */ public boolean supportsNestedElement(final String elementName) { return supportsNestedElement("", elementName); } /** * Indicate if this element supports a nested element of the * given name. * * <p>Note that this method will always return true if the * introspected class is {@link #isDynamic dynamic} or contains a * method named "add" with void return type and a single argument. * To ge a more thorough answer, use the four-arg version of this * method instead.</p> * * @param parentUri the uri of the parent * @param elementName the name of the nested element being checked * * @return true if the given nested element is supported */ public boolean supportsNestedElement(final String parentUri, final String elementName) { if (isDynamic() || !addTypeMethods.isEmpty()) { return true; } return supportsReflectElement(parentUri, elementName); } /** * Indicate if this element supports a nested element of the * given name. * * <p>Note that this method will always return true if the * introspected class is {@link #isDynamic dynamic}, so be * prepared to catch an exception about unsupported children when * calling {@link #getElementCreator getElementCreator}.</p> * * @param parentUri the uri of the parent * @param elementName the name of the nested element being checked * @param project currently executing project instance * @param parent the parent element * * @return true if the given nested element is supported * @since Ant 1.8.0. */ public boolean supportsNestedElement(final String parentUri, final String elementName, final Project project, final Object parent) { if (!addTypeMethods.isEmpty() && createAddTypeCreator(project, parent, elementName) != null) { return true; } return isDynamic() || supportsReflectElement(parentUri, elementName); } /** * Check if this element supports a nested element from reflection. * * @param parentUri the uri of the parent * @param elementName the name of the nested element being checked * * @return true if the given nested element is supported * @since Ant 1.8.0 */ public boolean supportsReflectElement( String parentUri, final String elementName) { final String name = ProjectHelper.extractNameFromComponentName(elementName); if (!nestedCreators.containsKey(name.toLowerCase(Locale.ENGLISH))) { return false; } String uri = ProjectHelper.extractUriFromComponentName(elementName); if (uri.equals(ProjectHelper.ANT_CORE_URI)) { uri = ""; } if ("".equals(uri)) { return true; } if (parentUri.equals(ProjectHelper.ANT_CORE_URI)) { parentUri = ""; } return uri.equals(parentUri); } /** * Stores a named nested element using a storage method determined * by the initial introspection. If no appropriate storage method * is available, this method returns immediately. * * @param project Ignored in this implementation. * May be <code>null</code>. * * @param parent Parent instance to store the child in. * Must not be <code>null</code>. * * @param child Child instance to store in the parent. * Should not be <code>null</code>. * * @param elementName Name of the child element to store. * May be <code>null</code>, in which case * this method returns immediately. * * @exception BuildException if the storage method fails. */ public void storeElement(final Project project, final Object parent, final Object child, final String elementName) throws BuildException { if (elementName == null) { return; } final NestedCreator ns = nestedCreators.get(elementName.toLowerCase(Locale.ENGLISH)); if (ns == null) { return; } try { ns.store(parent, child); } catch (final IllegalAccessException ie) { // impossible as getMethods should only return public methods throw new BuildException(ie); } catch (final InstantiationException ine) { // impossible as getMethods should only return public methods throw new BuildException(ine); } catch (final InvocationTargetException ite) { throw extractBuildException(ite); } } /** * Helper method to extract the inner fault from an {@link InvocationTargetException}, and turn * it into a BuildException. If it is already a BuildException, it is type cast and returned; if * not a new BuildException is created containing the child as nested text. * @param ite * @return the nested exception */ private static BuildException extractBuildException(final InvocationTargetException ite) { final Throwable t = ite.getTargetException(); if (t instanceof BuildException) { return (BuildException) t; } return new BuildException(t); } /** * Returns the type of a named nested element. * * @param elementName The name of the element to find the type of. * Must not be <code>null</code>. * * @return the type of the nested element with the specified name. * This will never be <code>null</code>. * * @exception BuildException if the introspected class does not * support the named nested element. */ public Class<?> getElementType(final String elementName) throws BuildException { final Class<?> nt = nestedTypes.get(elementName); if (nt == null) { throw new UnsupportedElementException("Class " + bean.getName() + " doesn't support the nested \"" + elementName + "\" element.", elementName); } return nt; } /** * Returns the type of a named attribute. * * @param attributeName The name of the attribute to find the type of. * Must not be <code>null</code>. * * @return the type of the attribute with the specified name. * This will never be <code>null</code>. * * @exception BuildException if the introspected class does not * support the named attribute. */ public Class<?> getAttributeType(final String attributeName) throws BuildException { final Class<?> at = attributeTypes.get(attributeName); if (at == null) { throw new UnsupportedAttributeException("Class " + bean.getName() + " doesn't support the \"" + attributeName + "\" attribute.", attributeName); } return at; } /** * Returns the addText method when the introspected * class supports nested text. * * @return the method on this introspected class that adds nested text. * Cannot be <code>null</code>. * @throws BuildException if the introspected class does not * support the nested text. * @since Ant 1.6.3 */ public Method getAddTextMethod() throws BuildException { if (!supportsCharacters()) { throw new BuildException("Class " + bean.getName() + " doesn't support nested text data."); } return addText; } /** * Returns the adder or creator method of a named nested element. * * @param elementName The name of the attribute to find the setter * method of. Must not be <code>null</code>. * @return the method on this introspected class that adds or creates this * nested element. Can be <code>null</code> when the introspected * class is a dynamic configurator! * @throws BuildException if the introspected class does not * support the named nested element. * @since Ant 1.6.3 */ public Method getElementMethod(final String elementName) throws BuildException { final Object creator = nestedCreators.get(elementName); if (creator == null) { throw new UnsupportedElementException("Class " + bean.getName() + " doesn't support the nested \"" + elementName + "\" element.", elementName); } return ((NestedCreator) creator).method; } /** * Returns the setter method of a named attribute. * * @param attributeName The name of the attribute to find the setter * method of. Must not be <code>null</code>. * @return the method on this introspected class that sets this attribute. * This will never be <code>null</code>. * @throws BuildException if the introspected class does not * support the named attribute. * @since Ant 1.6.3 */ public Method getAttributeMethod(final String attributeName) throws BuildException { final Object setter = attributeSetters.get(attributeName); if (setter == null) { throw new UnsupportedAttributeException("Class " + bean.getName() + " doesn't support the \"" + attributeName + "\" attribute.", attributeName); } return ((AttributeSetter) setter).method; } /** * Returns whether or not the introspected class supports PCDATA. * * @return whether or not the introspected class supports PCDATA. */ public boolean supportsCharacters() { return addText != null; } /** * Returns an enumeration of the names of the attributes supported by the introspected class. * * @return an enumeration of the names of the attributes supported by the introspected class. * @see #getAttributeMap */ public Enumeration<String> getAttributes() { return Collections.enumeration(attributeSetters.keySet()); } /** * Returns a read-only map of attributes supported by the introspected class. * * @return an attribute name to attribute <code>Class</code> * unmodifiable map. Can be empty, but never <code>null</code>. * @since Ant 1.6.3 */ public Map<String, Class<?>> getAttributeMap() { return attributeTypes.isEmpty() ? Collections.<String, Class<?>> emptyMap() : Collections.unmodifiableMap(attributeTypes); } /** * Returns an enumeration of the names of the nested elements supported * by the introspected class. * * @return an enumeration of the names of the nested elements supported * by the introspected class. * @see #getNestedElementMap */ public Enumeration<String> getNestedElements() { return Collections.enumeration(nestedTypes.keySet()); } /** * Returns a read-only map of nested elements supported * by the introspected class. * * @return a nested-element name to nested-element <code>Class</code> * unmodifiable map. Can be empty, but never <code>null</code>. * @since Ant 1.6.3 */ public Map<String, Class<?>> getNestedElementMap() { return nestedTypes.isEmpty() ? Collections.<String, Class<?>> emptyMap() : Collections.unmodifiableMap(nestedTypes); } /** * Returns a read-only list of extension points supported * by the introspected class. * <p> * A task/type or nested element with void methods named <code>add()</code> * or <code>addConfigured()</code>, taking a single class or interface * argument, supports extensions point. This method returns the list of * all these <em>void add[Configured](type)</em> methods. * * @return a list of void, single argument add() or addConfigured() * <code>Method</code>s of all supported extension points. * These methods are sorted such that if the argument type of a * method derives from another type also an argument of a method * of this list, the method with the most derived argument will * always appear first. Can be empty, but never <code>null</code>. * @since Ant 1.6.3 */ public List<Method> getExtensionPoints() { return addTypeMethods.isEmpty() ? Collections.<Method> emptyList() : Collections.unmodifiableList(addTypeMethods); } /** * Creates an implementation of AttributeSetter for the given * attribute type. Conversions (where necessary) are automatically * made for the following types: * <ul> * <li>String (left as it is) * <li>Character/char (first character is used) * <li>Boolean/boolean * ({@link Project#toBoolean(String) Project.toBoolean(String)} is used) * <li>Class (Class.forName is used) * <li>File (resolved relative to the appropriate project) * <li>Path (resolve relative to the appropriate project) * <li>Resource (resolved as a FileResource relative to the appropriate project) * <li>FileProvider (resolved as a FileResource relative to the appropriate project) * <li>EnumeratedAttribute (uses its own * {@link EnumeratedAttribute#setValue(String) setValue} method) * <li>Other primitive types (wrapper classes are used with constructors * taking String) * </ul> * * If none of the above covers the given parameters, a constructor for the * appropriate class taking a String parameter is used if it is available. * * @param m The method to invoke on the bean when the setter is invoked. * Must not be <code>null</code>. * @param arg The type of the single argument of the bean's method. * Must not be <code>null</code>. * @param attrName the name of the attribute for which the setter is being * created. * * @return an appropriate AttributeSetter instance, or <code>null</code> * if no appropriate conversion is available. */ private AttributeSetter createAttributeSetter(final Method m, final Class<?> arg, final String attrName) { // use wrappers for primitive classes, e.g. int and // Integer are treated identically final Class<?> reflectedArg = PRIMITIVE_TYPE_MAP.containsKey(arg) ? PRIMITIVE_TYPE_MAP.get(arg) : arg; // Object.class - it gets handled differently by AttributeSetter if (java.lang.Object.class == reflectedArg) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { throw new BuildException( "Internal ant problem - this should not get called"); } }; } // simplest case - setAttribute expects String if (java.lang.String.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { m.invoke(parent, (Object[]) new String[] {value}); } }; } // char and Character get special treatment - take the first character if (java.lang.Character.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { if (value.length() == 0) { throw new BuildException("The value \"\" is not a " + "legal value for attribute \"" + attrName + "\""); } m.invoke(parent, (Object[]) new Character[] {new Character(value.charAt(0))}); } }; } // boolean and Boolean get special treatment because we have a nice method in Project if (java.lang.Boolean.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { m.invoke(parent, (Object[]) new Boolean[] { Project.toBoolean(value) ? Boolean.TRUE : Boolean.FALSE }); } }; } // Class doesn't have a String constructor but a decent factory method if (java.lang.Class.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { try { m.invoke(parent, new Object[] {Class.forName(value)}); } catch (final ClassNotFoundException ce) { throw new BuildException(ce); } } }; } // resolve relative paths through Project if (java.io.File.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { m.invoke(parent, new Object[] {p.resolveFile(value)}); } }; } // resolve relative nio paths through Project if (java.nio.file.Path.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException { m.invoke(parent, new Object[] { p.resolveFile(value).toPath() }); } }; } // resolve Resources/FileProviders as FileResources relative to Project: if (Resource.class.equals(reflectedArg) || FileProvider.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { m.invoke(parent, new Object[] {new FileResource(p, p.resolveFile(value))}); }; }; } // EnumeratedAttributes have their own helper class if (EnumeratedAttribute.class.isAssignableFrom(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { try { final EnumeratedAttribute ea = (EnumeratedAttribute) reflectedArg.newInstance(); ea.setValue(value); m.invoke(parent, new Object[] {ea}); } catch (final InstantiationException ie) { throw new BuildException(ie); } } }; } final AttributeSetter setter = getEnumSetter(reflectedArg, m, arg); if (setter != null) { return setter; } if (java.lang.Long.class.equals(reflectedArg)) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { try { m.invoke(parent, new Object[] { new Long(StringUtils.parseHumanSizes(value)) }); } catch (final NumberFormatException e) { throw new BuildException("Can't assign non-numeric" + " value '" + value + "' to" + " attribute " + attrName); } catch (final InvocationTargetException e) { throw e; } catch (final IllegalAccessException e) { throw e; } catch (final Exception e) { throw new BuildException(e); } } }; } // worst case. look for a public String constructor and use it // also supports new Whatever(Project, String) as for Path or Reference // This is used (deliberately) for all primitives/wrappers other than // char, boolean, and long. boolean includeProject; Constructor<?> c; try { // First try with Project. c = reflectedArg.getConstructor(Project.class, String.class); includeProject = true; } catch (final NoSuchMethodException nme) { // OK, try without. try { c = reflectedArg.getConstructor(String.class); includeProject = false; } catch (final NoSuchMethodException nme2) { // Well, no matching constructor. return null; } } final boolean finalIncludeProject = includeProject; final Constructor<?> finalConstructor = c; return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { try { final Object[] args = finalIncludeProject ? new Object[] {p, value} : new Object[] {value}; final Object attribute = finalConstructor.newInstance(args); if (p != null) { p.setProjectReference(attribute); } m.invoke(parent, new Object[] {attribute}); } catch (final InvocationTargetException e) { final Throwable cause = e.getCause(); if (cause instanceof IllegalArgumentException) { throw new BuildException("Can't assign value '" + value + "' to attribute " + attrName + ", reason: " + cause.getClass() + " with message '" + cause.getMessage() + "'"); } throw e; } catch (final InstantiationException ie) { throw new BuildException(ie); } } }; } private AttributeSetter getEnumSetter( final Class<?> reflectedArg, final Method m, final Class<?> arg) { if (reflectedArg.isEnum()) { return new AttributeSetter(m, arg) { @Override public void set(final Project p, final Object parent, final String value) throws InvocationTargetException, IllegalAccessException, BuildException { Enum<?> setValue; try { @SuppressWarnings({ "unchecked", "rawtypes" }) final Enum<?> enumValue = Enum.valueOf((Class<? extends Enum>) reflectedArg, value); setValue = enumValue; } catch (final IllegalArgumentException e) { //there is specific logic here for the value // being out of the allowed set of enumerations. throw new BuildException("'" + value + "' is not a permitted value for " + reflectedArg.getName()); } m.invoke(parent, setValue); } }; } return null; } /** * Returns a description of the type of the given element in * relation to a given project. This is used for logging purposes * when the element is asked to cope with some data it has no way of handling. * * @param project The project the element is defined in. Must not be <code>null</code>. * * @param element The element to describe. Must not be <code>null</code>. * * @return a description of the element type */ private String getElementName(final Project project, final Object element) { return project.getElementName(element); } /** * Extracts the name of a property from a method name by subtracting * a given prefix and converting into lower case. It is up to calling * code to make sure the method name does actually begin with the * specified prefix - no checking is done in this method. * * @param methodName The name of the method in question. Must not be <code>null</code>. * @param prefix The prefix to remove. Must not be <code>null</code>. * * @return the lower-cased method name with the prefix removed. */ private static String getPropertyName(final String methodName, final String prefix) { return methodName.substring(prefix.length()).toLowerCase(Locale.ENGLISH); } /** * creator - allows use of create/store external * to IntrospectionHelper. * The class is final as it has a private constructor. */ public static final class Creator { private final NestedCreator nestedCreator; private final Object parent; private final Project project; private Object nestedObject; private String polyType; /** * Creates a new Creator instance. * This object is given to the UnknownElement to create * objects for sub-elements. UnknownElement calls * create to create an object, the object then gets * configured and then UnknownElement calls store. * SetPolyType may be used to override the type used * to create the object with. SetPolyType gets called before create. * * @param project the current project * @param parent the parent object to create the object in * @param nestedCreator the nested creator object to use */ private Creator(final Project project, final Object parent, final NestedCreator nestedCreator) { this.project = project; this.parent = parent; this.nestedCreator = nestedCreator; } /** * Used to override the class used to create the object. * * @param polyType a ant component type name */ public void setPolyType(final String polyType) { this.polyType = polyType; } /** * Create an object using this creator, which is determined by introspection. * * @return the created object */ public Object create() { if (polyType != null) { if (!nestedCreator.isPolyMorphic()) { throw new BuildException( "Not allowed to use the polymorphic form for this element"); } final ComponentHelper helper = ComponentHelper.getComponentHelper(project); nestedObject = helper.createComponent(polyType); if (nestedObject == null) { throw new BuildException("Unable to create object of type " + polyType); } } try { nestedObject = nestedCreator.create(project, parent, nestedObject); if (project != null) { project.setProjectReference(nestedObject); } return nestedObject; } catch (final IllegalAccessException ex) { throw new BuildException(ex); } catch (final InstantiationException ex) { throw new BuildException(ex); } catch (final IllegalArgumentException ex) { if (polyType == null) { throw ex; } throw new BuildException("Invalid type used " + polyType); } catch (final InvocationTargetException ex) { throw extractBuildException(ex); } } /** * @return the real object (used currently only for presetdef). */ public Object getRealObject() { return nestedCreator.getRealObject(); } /** * Stores the nested element object using a storage method determined by introspection. * */ public void store() { try { nestedCreator.store(parent, nestedObject); } catch (final IllegalAccessException ex) { throw new BuildException(ex); } catch (final InstantiationException ex) { throw new BuildException(ex); } catch (final IllegalArgumentException ex) { if (polyType == null) { throw ex; } throw new BuildException("Invalid type used " + polyType); } catch (final InvocationTargetException ex) { throw extractBuildException(ex); } } } /** * Internal interface used to create nested elements. Not documented * in detail for reasons of source code readability. */ private abstract static class NestedCreator { private final Method method; // the method called to add/create the nested element protected NestedCreator(final Method m) { method = m; } Method getMethod() { return method; } boolean isPolyMorphic() { return false; } Object getRealObject() { return null; } abstract Object create(Project project, Object parent, Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException; void store(final Object parent, final Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException { // DO NOTHING } } private static class CreateNestedCreator extends NestedCreator { CreateNestedCreator(final Method m) { super(m); } @Override Object create(final Project project, final Object parent, final Object ignore) throws InvocationTargetException, IllegalAccessException { return getMethod().invoke(parent, new Object[] {}); } } /** Version to use for addXXX and addConfiguredXXX */ private static class AddNestedCreator extends NestedCreator { static final int ADD = 1; static final int ADD_CONFIGURED = 2; private final Constructor<?> constructor; private final int behavior; // ADD or ADD_CONFIGURED AddNestedCreator(final Method m, final Constructor<?> c, final int behavior) { super(m); this.constructor = c; this.behavior = behavior; } @Override boolean isPolyMorphic() { return true; } @Override Object create(final Project project, final Object parent, Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException { if (child == null) { child = constructor.newInstance( constructor.getParameterTypes().length == 0 ? new Object[] {} : new Object[] {project}); } if (child instanceof PreSetDef.PreSetDefinition) { child = ((PreSetDef.PreSetDefinition) child).createObject(project); } if (behavior == ADD) { istore(parent, child); } return child; } @Override void store(final Object parent, final Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException { if (behavior == ADD_CONFIGURED) { istore(parent, child); } } private void istore(final Object parent, final Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException { getMethod().invoke(parent, new Object[] {child}); } } /** * Internal interface used to setting element attributes. Not documented * in detail for reasons of source code readability. */ private abstract static class AttributeSetter { private final Method method; // the method called to set the attribute private final Class<?> type; protected AttributeSetter(final Method m, final Class<?> type) { method = m; this.type = type; } void setObject(final Project p, final Object parent, final Object value) throws InvocationTargetException, IllegalAccessException, BuildException { if (type != null) { Class<?> useType = type; if (type.isPrimitive()) { if (value == null) { throw new BuildException( "Attempt to set primitive " + getPropertyName(method.getName(), "set") + " to null on " + parent); } useType = PRIMITIVE_TYPE_MAP.get(type); } if (value == null || useType.isInstance(value)) { method.invoke(parent, new Object[] {value}); return; } } set(p, parent, value.toString()); } abstract void set(Project p, Object parent, String value) throws InvocationTargetException, IllegalAccessException, BuildException; } /** * Clears the static cache of on build finished. */ public static synchronized void clearCache() { HELPERS.clear(); } /** * Create a NestedCreator for the given element. * @param project owning project * @param parent Parent object used to create the instance. * @param elementName name of the element * @return a nested creator, or null if there is no component of the given name, or it * has no matching add type methods * @throws BuildException */ private NestedCreator createAddTypeCreator( final Project project, final Object parent, final String elementName) throws BuildException { if (addTypeMethods.isEmpty()) { return null; } final ComponentHelper helper = ComponentHelper.getComponentHelper(project); final MethodAndObject restricted = createRestricted( helper, elementName, addTypeMethods); final MethodAndObject topLevel = createTopLevel( helper, elementName, addTypeMethods); if (restricted == null && topLevel == null) { return null; } if (restricted != null && topLevel != null) { throw new BuildException( "ambiguous: type and component definitions for " + elementName); } final MethodAndObject methodAndObject = restricted != null ? restricted : topLevel; Object rObject = methodAndObject.object; if (methodAndObject.object instanceof PreSetDef.PreSetDefinition) { rObject = ((PreSetDef.PreSetDefinition) methodAndObject.object) .createObject(project); } final Object nestedObject = methodAndObject.object; final Object realObject = rObject; return new NestedCreator(methodAndObject.method) { @Override Object create(final Project project, final Object parent, final Object ignore) throws InvocationTargetException, IllegalAccessException { if (!getMethod().getName().endsWith("Configured")) { getMethod().invoke(parent, new Object[] {realObject}); } return nestedObject; } @Override Object getRealObject() { return realObject; } @Override void store(final Object parent, final Object child) throws InvocationTargetException, IllegalAccessException, InstantiationException { if (getMethod().getName().endsWith("Configured")) { getMethod().invoke(parent, new Object[] {realObject}); } } }; } /** * Inserts an add or addConfigured method into * the addTypeMethods array. The array is * ordered so that the more derived classes are first. * If both add and addConfigured are present, the addConfigured will take priority. * @param method the <code>Method</code> to insert. */ private void insertAddTypeMethod(final Method method) { final Class<?> argClass = method.getParameterTypes()[0]; final int size = addTypeMethods.size(); for (int c = 0; c < size; ++c) { final Method current = addTypeMethods.get(c); if (current.getParameterTypes()[0].equals(argClass)) { if ("addConfigured".equals(method.getName())) { // add configured replaces the add method addTypeMethods.set(c, method); } return; // Already present } if (current.getParameterTypes()[0].isAssignableFrom(argClass)) { addTypeMethods.add(c, method); return; // higher derived } } addTypeMethods.add(method); } /** * Search the list of methods to find the first method * that has a parameter that accepts the nested element object. * @param paramClass the <code>Class</code> type to search for. * @param methods the <code>List</code> of methods to search. * @return a matching <code>Method</code>; null if none found. */ private Method findMatchingMethod(final Class<?> paramClass, final List<Method> methods) { if (paramClass == null) { return null; } Class<?> matchedClass = null; Method matchedMethod = null; final int size = methods.size(); for (int i = 0; i < size; ++i) { final Method method = methods.get(i); final Class<?> methodClass = method.getParameterTypes()[0]; if (methodClass.isAssignableFrom(paramClass)) { if (matchedClass == null) { matchedClass = methodClass; matchedMethod = method; } else if (!methodClass.isAssignableFrom(matchedClass)) { throw new BuildException("ambiguous: types " + matchedClass.getName() + " and " + methodClass.getName() + " match " + paramClass.getName()); } } } return matchedMethod; } private String condenseText(final String text) { if (text.length() <= MAX_REPORT_NESTED_TEXT) { return text; } final int ends = (MAX_REPORT_NESTED_TEXT - ELLIPSIS.length()) / 2; return new StringBuffer(text).replace(ends, text.length() - ends, ELLIPSIS).toString(); } private static class MethodAndObject { private final Method method; private final Object object; public MethodAndObject(final Method method, final Object object) { this.method = method; this.object = object; } } /** * */ private AntTypeDefinition findRestrictedDefinition( final ComponentHelper helper, final String componentName, final List<Method> methods) { AntTypeDefinition definition = null; Class<?> matchedDefinitionClass = null; final List<AntTypeDefinition> definitions = helper.getRestrictedDefinitions(componentName); if (definitions == null) { return null; } synchronized (definitions) { final int size = definitions.size(); for (int i = 0; i < size; ++i) { final AntTypeDefinition d = definitions.get(i); final Class<?> exposedClass = d.getExposedClass(helper.getProject()); if (exposedClass == null) { continue; } final Method method = findMatchingMethod(exposedClass, methods); if (method == null) { continue; } if (matchedDefinitionClass != null) { throw new BuildException( "ambiguous: restricted definitions for " + componentName + " " + matchedDefinitionClass + " and " + exposedClass); } matchedDefinitionClass = exposedClass; definition = d; } } return definition; } private MethodAndObject createRestricted( final ComponentHelper helper, final String elementName, final List<Method> addTypeMethods) { final Project project = helper.getProject(); final AntTypeDefinition restrictedDefinition = findRestrictedDefinition(helper, elementName, addTypeMethods); if (restrictedDefinition == null) { return null; } final Method addMethod = findMatchingMethod( restrictedDefinition.getExposedClass(project), addTypeMethods); if (addMethod == null) { throw new BuildException( "Ant Internal Error - contract mismatch for " + elementName); } final Object addedObject = restrictedDefinition.create(project); if (addedObject == null) { throw new BuildException( "Failed to create object " + elementName + " of type " + restrictedDefinition.getTypeClass(project)); } return new MethodAndObject(addMethod, addedObject); } private MethodAndObject createTopLevel( final ComponentHelper helper, final String elementName, final List<Method> methods) { final Class<?> clazz = helper.getComponentClass(elementName); if (clazz == null) { return null; } final Method addMethod = findMatchingMethod(clazz, addTypeMethods); if (addMethod == null) { return null; } final Object addedObject = helper.createComponent(elementName); return new MethodAndObject(addMethod, addedObject); } }