org.apache.tools.ant.IntrospectionHelper Maven / Gradle / Ivy
/*
* 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
*
* https://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.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
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 java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.Supplier;
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 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>> 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> attributeTypes = new Hashtable<>();
/**
* Map from attribute names to attribute setter methods
* (String to AttributeSetter).
*/
private final Map attributeSetters = new Hashtable<>();
/**
* Map from attribute names to nested types
* (String to Class).
*/
private final Map> nestedTypes = new Hashtable<>();
/**
* Map from attribute names to methods to create nested types
* (String to NestedCreator).
*/
private final Map nestedCreators = new Hashtable<>();
/**
* Vector of methods matching add[Configured](Class) pattern.
*/
private final List 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:
*
*
* - If the method is
Task.setLocation(Location)
,
* Task.setTaskType(String)
* or TaskContainer.addTask(Task)
, it is ignored. These
* methods are handled differently elsewhere.
* void addText(String)
is recognised as the method for
* adding PCDATA to a bean.
* void setFoo(Bar)
is recognised as a method for
* setting the value of attribute foo
, so long as
* Bar
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.
* Foo createBar()
is recognised as a method for
* creating a nested element called bar
of type
* Foo
, so long as Foo
is not a primitive or
* array type.
* void addConfiguredFoo(Bar)
is recognised as a
* method for storing a pre-configured element called
* foo
and of type Bar
, so long as
* Bar
is not an array, primitive or String type.
* Bar
must have an accessible constructor taking no
* arguments.
* void addFoo(Bar)
is recognised as a method for storing
* an element called foo
and of type Bar
, so
* long as Bar
is not an array, primitive or String type.
* Bar
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 addFoo(Bar)
should
* do little or nothing with its argument besides storing it for later use.
*
* 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 null
.
*
* @see #getHelper(Class)
*/
private IntrospectionHelper(final Class> bean) {
this.bean = bean;
Method addTextMethod = null;
for (final Method m : bean.getMethods()) {
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 && Void.TYPE.equals(returnType)
&& ("add".equals(name) || "addConfigured".equals(name))) {
insertAddTypeMethod(m);
continue;
}
// not really user settable properties on tasks/project components
if (ProjectComponent.class.isAssignableFrom(bean)
&& args.length == 1 && isHiddenSetMethod(name, args[0])) {
continue;
}
// hide addTask for TaskContainers
if (isContainer() && args.length == 1 && "addTask".equals(name)
&& Task.class.equals(args[0])) {
continue;
}
if ("addText".equals(name) && Void.TYPE.equals(returnType)
&& args.length == 1 && String.class.equals(args[0])) {
addTextMethod = m;
} else if (name.startsWith("set") && 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 (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 (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")
&& Void.TYPE.equals(returnType) && args.length == 1
&& !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")
&& Void.TYPE.equals(returnType) && args.length == 1
&& !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) {
return "setLocation".equals(name) && Location.class.equals(type)
|| "setTaskType".equals(name) && String.class.equals(type);
}
/**
* 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 null
.
*
* @return a helper for the specified class
*/
public static 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 null
.
*
* @return a helper for the specified class
*/
public static IntrospectionHelper getHelper(final Project p, final Class> c) {
if (p == null) {
// #30162: do *not* use cache if there is no project, as we
// cannot guarantee that the cache will be cleared.
return new IntrospectionHelper(c);
}
IntrospectionHelper ih = HELPERS.get(c.getName());
if (ih != null && ih.bean == c) {
return ih;
}
// If a helper cannot be found, or if the helper is for another
// classloader, create a new IH and cache it
// Note: This new instance of IntrospectionHelper is intentionally
// created without holding a lock, to prevent potential deadlocks.
// See bz-65424 for details
ih = new IntrospectionHelper(c);
synchronized (HELPERS) {
IntrospectionHelper cached = HELPERS.get(c.getName());
if (cached != null && cached.bean == c) {
return cached;
}
// cache the recently created one
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 null
.
* @param element The element to set the attribute in. Must not be
* null
.
* @param attributeName The name of the attribute to set. Must not be
* null
.
* @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 = uri.isEmpty() ? 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.contains(":")) {
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 null
.
* @param element The element to set the attribute in. Must not be
* null
.
* @param attributeName The name of the attribute to set. Must not be
* null
.
* @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 null
.
*
* @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
* void addText(String)
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 null
.
* @param element The element to add the text to.
* Must not be null
.
* @param text The text to add.
* Must not be null
.
*
* @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.isEmpty()) {
// 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, 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.isEmpty()) {
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 null
. 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 null
.
* @param elementName Name of the element to create an instance of.
* Must not be null
.
*
* @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 | InstantiationException ie) {
// impossible as getMethods should only return public methods
throw new BuildException(ie);
} 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 true
if the introspected class is dynamic;
* false
otherwise.
* @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 true
if the introspected class is a container;
* false
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.
*
* 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.
*
* @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) {
return isDynamic() || !addTypeMethods.isEmpty()
|| supportsReflectElement(parentUri, elementName);
}
/**
* Indicate if this element supports a nested element of the
* given name.
*
* 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}.
*
* @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) {
return !addTypeMethods.isEmpty()
&& createAddTypeCreator(project, parent, elementName) != null
|| 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.isEmpty()) {
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 null
.
*
* @param parent Parent instance to store the child in.
* Must not be null
.
*
* @param child Child instance to store in the parent.
* Should not be null
.
*
* @param elementName Name of the child element to store.
* May be null
, 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 | InstantiationException ie) {
// impossible as getMethods should only return public methods
throw new BuildException(ie);
} 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 the exception
* @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 null
.
*
* @return the type of the nested element with the specified name.
* This will never be null
.
*
* @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 null
.
*
* @return the type of the attribute with the specified name.
* This will never be null
.
*
* @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 null
.
* @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 null
.
* @return the method on this introspected class that adds or creates this
* nested element. Can be null
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 NestedCreator creator = nestedCreators.get(elementName);
if (creator == null) {
throw new UnsupportedElementException("Class "
+ bean.getName() + " doesn't support the nested \""
+ elementName + "\" element.", elementName);
}
return 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 null
.
* @return the method on this introspected class that sets this attribute.
* This will never be null
.
* @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 AttributeSetter setter = attributeSetters.get(attributeName);
if (setter == null) {
throw new UnsupportedAttributeException("Class "
+ bean.getName() + " doesn't support the \""
+ attributeName + "\" attribute.", attributeName);
}
return 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 getAttributes() {
return Collections.enumeration(attributeSetters.keySet());
}
/**
* Returns a read-only map of attributes supported by the introspected class.
*
* @return an attribute name to attribute Class
* unmodifiable map. Can be empty, but never null
.
* @since Ant 1.6.3
*/
public Map> getAttributeMap() {
return attributeTypes.isEmpty()
? Collections.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 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 Class
* unmodifiable map. Can be empty, but never null
.
* @since Ant 1.6.3
*/
public Map> getNestedElementMap() {
return nestedTypes.isEmpty()
? Collections.emptyMap() : Collections.unmodifiableMap(nestedTypes);
}
/**
* Returns a read-only list of extension points supported
* by the introspected class.
*
* A task/type or nested element with void methods named add()
* or addConfigured()
, taking a single class or interface
* argument, supports extensions point. This method returns the list of
* all these void add[Configured](type) methods.
*
* @return a list of void, single argument add() or addConfigured()
* Method
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 null
.
* @since Ant 1.6.3
*/
public List getExtensionPoints() {
return addTypeMethods.isEmpty()
? Collections.emptyList() : Collections.unmodifiableList(addTypeMethods);
}
/**
* Creates an implementation of AttributeSetter for the given
* attribute type. Conversions (where necessary) are automatically
* made for the following types:
*
* - String (left as it is)
*
- Character/char (first character is used)
*
- Boolean/boolean
* ({@link Project#toBoolean(String) Project.toBoolean(String)} is used)
*
- Class (Class.forName is used)
*
- File (resolved relative to the appropriate project)
*
- Path (resolve relative to the appropriate project)
*
- Resource (resolved as a FileResource relative to the appropriate project)
*
- FileProvider (resolved as a FileResource relative to the appropriate project)
*
- EnumeratedAttribute (uses its own
* {@link EnumeratedAttribute#setValue(String) setValue} method)
*
- Other primitive types (wrapper classes are used with constructors
* taking String)
*
*
* 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 null
.
* @param arg The type of the single argument of the bean's method.
* Must not be null
.
* @param attrName the name of the attribute for which the setter is being
* created.
*
* @return an appropriate AttributeSetter instance, or null
* if no appropriate conversion is available.
*/
private AttributeSetter createAttributeSetter(final Method m,
final Class> arg,
final String attrName) {
if (Optional.class.equals(arg)) {
Type gpt = m.getGenericParameterTypes()[0];
Class> payload = Object.class;
if (gpt instanceof ParameterizedType ) {
Type ata = ((ParameterizedType) gpt).getActualTypeArguments()[0];
if (ata instanceof Class>) {
payload = (Class>) ata;
} else if (ata instanceof ParameterizedType) {
payload = (Class>) ((ParameterizedType) ata).getRawType();
}
}
final AttributeSetter wrapped = createAttributeSetter(m, payload, attrName);
return new AttributeSetter(m, arg, Optional::empty) {
@Override
Optional> toTargetType(Project project, String value)
throws BuildException {
return Optional.ofNullable(wrapped.toTargetType(project, value));
}
};
}
if (OptionalInt.class.equals(arg)) {
final AttributeSetter wrapped = createAttributeSetter(m, Integer.class, attrName);
return new AttributeSetter(m, arg, OptionalInt::empty) {
@Override
OptionalInt toTargetType(Project project, String value)
throws BuildException {
return Optional.ofNullable((Integer) wrapped.toTargetType(project, value))
.map(OptionalInt::of).orElseGet(OptionalInt::empty);
}
};
}
if (OptionalLong.class.equals(arg)) {
final AttributeSetter wrapped = createAttributeSetter(m, Long.class, attrName);
return new AttributeSetter(m, arg, OptionalLong::empty) {
@Override
OptionalLong toTargetType(Project project, String value)
throws BuildException {
return Optional.ofNullable((Long) wrapped.toTargetType(project, value))
.map(OptionalLong::of).orElseGet(OptionalLong::empty);
}
};
}
if (OptionalDouble.class.equals(arg)) {
final AttributeSetter wrapped = createAttributeSetter(m, Double.class, attrName);
return new AttributeSetter(m, arg, OptionalDouble::empty) {
@Override
Object toTargetType(Project project, String value)
throws BuildException {
return Optional.ofNullable((Double) wrapped.toTargetType(project, value))
.map(OptionalDouble::of).orElseGet(OptionalDouble::empty);
}
};
}
// use wrappers for primitive classes, e.g. int and
// Integer are treated identically
final Class> reflectedArg = PRIMITIVE_TYPE_MAP.getOrDefault(arg, arg);
// Object.class - it gets handled differently by AttributeSetter
if (Object.class == reflectedArg) {
return new AttributeSetter(m, arg) {
@Override
Object toTargetType(Project project, String value)
throws BuildException {
throw new BuildException(
"Internal ant problem - this should not get called");
}
};
}
// simplest case - setAttribute expects String
if (String.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public String toTargetType(Project project, String t) {
return t;
}
};
}
// char and Character get special treatment - take the first character
if (Character.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public Character toTargetType(Project project, String value) {
if (value.isEmpty()) {
throw new BuildException("The value \"\" is not a "
+ "legal value for attribute \"" + attrName + "\"");
}
return Character.valueOf(value.charAt(0));
}
};
}
// boolean and Boolean get special treatment because we have a nice method in Project
if (Boolean.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public Boolean toTargetType(Project project, String value) {
return Boolean.valueOf(Project.toBoolean(value));
}
};
}
// Class doesn't have a String constructor but a decent factory method
if (Class.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public Class> toTargetType(Project project, String value) {
try {
return Class.forName(value);
} catch (ClassNotFoundException e) {
throw new BuildException(e);
}
}
};
}
// resolve relative paths through Project
if (File.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
Object toTargetType(Project project, String value)
throws BuildException {
return project.resolveFile(value);
}
};
}
// resolve relative nio paths through Project
if (java.nio.file.Path.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
Object toTargetType(Project project, String value)
throws BuildException {
return project.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
Object toTargetType(Project project, String value)
throws BuildException {
return new FileResource(project.resolveFile(value));
}
};
}
// EnumeratedAttributes have their own helper class
if (EnumeratedAttribute.class.isAssignableFrom(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public EnumeratedAttribute toTargetType(Project project, String value) {
EnumeratedAttribute ea;
try {
ea = (EnumeratedAttribute) reflectedArg.getDeclaredConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
throw BuildException.of(e);
}
ea.setValue(value);
return ea;
}
};
}
final AttributeSetter setter = getEnumSetter(reflectedArg, m, arg);
if (setter != null) {
return setter;
}
if (Long.class.equals(reflectedArg)) {
return new AttributeSetter(m, arg) {
@Override
public Long toTargetType(Project project, String value) {
try {
return Long.valueOf(StringUtils.parseHumanSizes(value));
} catch (final NumberFormatException e) {
throw new BuildException(
String.format("Can't assign non-numeric value '%s' to attribute %s",
value, attrName));
} 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 Object toTargetType(Project project, String value) {
try {
final Object[] args = finalIncludeProject
? new Object[] {project, value} : new Object[] {value};
final Object attribute = finalConstructor.newInstance(args);
if (project != null) {
project.setProjectReference(attribute);
}
return attribute;
} catch (final Exception e) {
Throwable thw = e;
while (true) {
if (thw instanceof IllegalArgumentException) {
throw new BuildException(String.format(
"Can't convert value '%s' to type %s, reason: %s with message '%s'",
value, reflectedArg, thw.getClass(), thw.getMessage()));
}
final Throwable _thw = thw;
Optional next = Optional.of(thw).map(Throwable::getCause).filter(t -> t != _thw);
if (!next.isPresent()) {
break;
}
thw = next.get();
}
throw BuildException.of(e);
}
}
};
}
private AttributeSetter getEnumSetter(
final Class> reflectedArg, final Method m, final Class> arg) {
if (reflectedArg.isEnum()) {
return new AttributeSetter(m, arg) {
@Override
public Enum> toTargetType(Project project, String value) {
try {
@SuppressWarnings({ "unchecked", "rawtypes" })
final Enum> result =
Enum.valueOf((Class extends Enum>) reflectedArg, value);
return result;
} catch (final IllegalArgumentException e) {
// there is a 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());
}
}
};
}
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 null
.
*
* @param element The element to describe. Must not be null
.
*
* @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 null
.
* @param prefix The prefix to remove. Must not be null
.
*
* @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 | 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 | 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);
}
}
/** 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 {
getMethod().invoke(parent, 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;
private final Supplier> supplyWhenNull;
protected AttributeSetter(final Method m, final Class> type) {
this(m, type, () -> null);
}
protected AttributeSetter(final Method method, final Class> type,
final Supplier> supplyWhenNull) {
this.method = method;
this.type = type;
this.supplyWhenNull = supplyWhenNull;
}
final void setObject(final Project p, final Object parent, 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 %s to null on %s",
getPropertyName(method.getName(), "set"), parent);
}
useType = PRIMITIVE_TYPE_MAP.get(type);
}
if (value == null ) {
value = supplyWhenNull.get();
}
if (value == null || useType.isInstance(value)) {
method.invoke(parent, value);
return;
}
}
method.invoke(parent, toTargetType(p, value.toString()));
}
Object toTargetType(Project project, String value) {
throw new UnsupportedOperationException();
}
}
/**
* Clears the static cache of on build finished.
*/
public static 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 if something goes wrong
*/
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 ? topLevel : restricted;
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, 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, 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 Method
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 Class
type to search for.
* @param methods the List
of methods to search.
* @return a matching Method
; null if none found.
*/
private Method findMatchingMethod(final Class> paramClass, final List methods) {
if (paramClass == null) {
return null;
}
Class> matchedClass = null;
Method matchedMethod = null;
for (final Method method : methods) {
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 methods) {
AntTypeDefinition definition = null;
Class> matchedDefinitionClass = null;
final List definitions = helper.getRestrictedDefinitions(componentName);
if (definitions == null) {
return null;
}
synchronized (definitions) {
for (final AntTypeDefinition d : definitions) {
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 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 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);
}
}