1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18 package org.apache.tools.ant;
19
20 import java.lang.reflect.Constructor;
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Enumeration;
26 import java.util.Hashtable;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Map;
31 import org.apache.tools.ant.types.EnumeratedAttribute;
32 import org.apache.tools.ant.taskdefs.PreSetDef;
33 import org.apache.tools.ant.util.StringUtils;
34
35 /**
36 * Helper class that collects the methods a task or nested element
37 * holds to set attributes, create nested elements or hold PCDATA
38 * elements.
39 *
40 * It contains hashtables containing classes that use introspection
41 * to handle all the invocation of the project-component specific methods.
42 *
43 * This class is somewhat complex, as it implements the O/X mapping between
44 * Ant XML and Java class instances. This is not the best place for someone new
45 * to Ant to start contributing to the codebase, as a change here can break the
46 * entire system in interesting ways. Always run a full test of Ant before checking
47 * in/submitting changes to this file.
48 *
49 * The class is final and has a private constructor.
50 * To get an instance for a specific (class,project) combination, use {@link #getHelper(Project,Class)}.
51 * This may return an existing version, or a new one
52 * ...do not make any assumptions about its uniqueness, or its validity after the Project
53 * instance has finished its build.
54 *
55 */
56 public final class IntrospectionHelper {
57
58 /**
59 * Helper instances we've already created (Class.getName() to IntrospectionHelper).
60 */
61 private static final Map HELPERS = new Hashtable();
62
63 /**
64 * Map from primitive types to wrapper classes for use in
65 * createAttributeSetter (Class to Class). Note that char
66 * and boolean are in here even though they get special treatment
67 * - this way we only need to test for the wrapper class.
68 */
69 private static final Map PRIMITIVE_TYPE_MAP = new HashMap(8);
70
71 // Set up PRIMITIVE_TYPE_MAP
72 static {
73 Class[] primitives = {Boolean.TYPE, Byte.TYPE, Character.TYPE, Short.TYPE,
74 Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE};
75 Class[] wrappers = {Boolean.class, Byte.class, Character.class, Short.class,
76 Integer.class, Long.class, Float.class, Double.class};
77 for (int i = 0; i < primitives.length; i++) {
78 PRIMITIVE_TYPE_MAP.put (primitives[i], wrappers[i]);
79 }
80 }
81
82 private static final int MAX_REPORT_NESTED_TEXT = 20;
83 private static final String ELLIPSIS = "...";
84
85 /**
86 * Map from attribute names to attribute types
87 * (String to Class).
88 */
89 private Hashtable attributeTypes = new Hashtable();
90
91 /**
92 * Map from attribute names to attribute setter methods
93 * (String to AttributeSetter).
94 */
95 private Hashtable attributeSetters = new Hashtable();
96
97 /**
98 * Map from attribute names to nested types
99 * (String to Class).
100 */
101 private Hashtable nestedTypes = new Hashtable();
102
103 /**
104 * Map from attribute names to methods to create nested types
105 * (String to NestedCreator).
106 */
107 private Hashtable nestedCreators = new Hashtable();
108
109 /**
110 * Vector of methods matching add[Configured](Class) pattern.
111 */
112 private List addTypeMethods = new ArrayList();
113
114 /**
115 * The method to invoke to add PCDATA.
116 */
117 private Method addText = null;
118
119 /**
120 * The class introspected by this instance.
121 */
122 private Class bean;
123
124 /**
125 * Sole constructor, which is private to ensure that all
126 * IntrospectionHelpers are created via {@link #getHelper(Class) getHelper}.
127 * Introspects the given class for bean-like methods.
128 * Each method is examined in turn, and the following rules are applied:
129 * <p>
130 * <ul>
131 * <li>If the method is <code>Task.setLocation(Location)</code>,
132 * <code>Task.setTaskType(String)</code>
133 * or <code>TaskContainer.addTask(Task)</code>, it is ignored. These
134 * methods are handled differently elsewhere.
135 * <li><code>void addText(String)</code> is recognised as the method for
136 * adding PCDATA to a bean.
137 * <li><code>void setFoo(Bar)</code> is recognised as a method for
138 * setting the value of attribute <code>foo</code>, so long as
139 * <code>Bar</code> is non-void and is not an array type. Non-String
140 * parameter types always overload String parameter types, but that is
141 * the only guarantee made in terms of priority.
142 * <li><code>Foo createBar()</code> is recognised as a method for
143 * creating a nested element called <code>bar</code> of type
144 * <code>Foo</code>, so long as <code>Foo</code> is not a primitive or
145 * array type.
146 * <li><code>void addConfiguredFoo(Bar)</code> is recognised as a
147 * method for storing a pre-configured element called
148 * <code>foo</code> and of type <code>Bar</code>, so long as
149 * <code>Bar</code> is not an array, primitive or String type.
150 * <code>Bar</code> must have an accessible constructor taking no
151 * arguments.
152 * <li><code>void addFoo(Bar)</code> is recognised as a method for storing
153 * an element called <code>foo</code> and of type <code>Bar</code>, so
154 * long as <code>Bar</code> is not an array, primitive or String type.
155 * <code>Bar</code> must have an accessible constructor taking no
156 * arguments. This is distinct from the 'addConfigured' idiom in that
157 * the nested element is added to the parent immediately after it is
158 * constructed; in practice this means that <code>addFoo(Bar)</code> should
159 * do little or nothing with its argument besides storing it for later use.
160 * </ul>
161 * Note that only one method is retained to create/set/addConfigured/add
162 * any element or attribute.
163 *
164 * @param bean The bean type to introspect.
165 * Must not be <code>null</code>.
166 *
167 * @see #getHelper(Class)
168 */
169 private IntrospectionHelper(final Class bean) {
170 this.bean = bean;
171 Method[] methods = bean.getMethods();
172 for (int i = 0; i < methods.length; i++) {
173 final Method m = methods[i];
174 final String name = m.getName();
175 Class returnType = m.getReturnType();
176 Class[] args = m.getParameterTypes();
177
178 // check of add[Configured](Class) pattern
179 if (args.length == 1 && java.lang.Void.TYPE.equals(returnType)
180 && ("add".equals(name) || "addConfigured".equals(name))) {
181 insertAddTypeMethod(m);
182 continue;
183 }
184 // not really user settable properties on tasks/project components
185 if (org.apache.tools.ant.ProjectComponent.class.isAssignableFrom(bean)
186 && args.length == 1 && isHiddenSetMethod(name, args[0])) {
187 continue;
188 }
189 // hide addTask for TaskContainers
190 if (isContainer() && args.length == 1 && "addTask".equals(name)
191 && org.apache.tools.ant.Task.class.equals(args[0])) {
192 continue;
193 }
194 if ("addText".equals(name) && java.lang.Void.TYPE.equals(returnType)
195 && args.length == 1 && java.lang.String.class.equals(args[0])) {
196 addText = methods[i];
197 } else if (name.startsWith("set") && java.lang.Void.TYPE.equals(returnType)
198 && args.length == 1 && !args[0].isArray()) {
199 String propName = getPropertyName(name, "set");
200 if (attributeSetters.get(propName) != null) {
201 if (java.lang.String.class.equals(args[0])) {
202 /*
203 Ignore method m, as there is an overloaded
204 form of this method that takes in a
205 non-string argument, which gains higher
206 priority.
207 */
208 continue;
209 }
210 /*
211 If the argument is not a String and if there
212 is an overloaded form of this method already defined,
213 we just override that with the new one.
214 This mechanism does not guarantee any specific order
215 in which the methods will be selected: so any code
216 that depends on the order in which "set" methods have
217 been defined, is not guaranteed to be selected in any
218 particular order.
219 */
220 }
221 AttributeSetter as = createAttributeSetter(m, args[0], propName);
222 if (as != null) {
223 attributeTypes.put(propName, args[0]);
224 attributeSetters.put(propName, as);
225 }
226 } else if (name.startsWith("create") && !returnType.isArray()
227 && !returnType.isPrimitive() && args.length == 0) {
228
229 String propName = getPropertyName(name, "create");
230 // Check if a create of this property is already present
231 // add takes preference over create for CB purposes
232 if (nestedCreators.get(propName) == null) {
233 nestedTypes.put(propName, returnType);
234 nestedCreators.put(propName, new CreateNestedCreator(m));
235 }
236 } else if (name.startsWith("addConfigured")
237 && java.lang.Void.TYPE.equals(returnType) && args.length == 1
238 && !java.lang.String.class.equals(args[0])
239 && !args[0].isArray() && !args[0].isPrimitive()) {
240 try {
241 Constructor constructor = null;
242 try {
243 constructor = args[0].getConstructor(new Class[] {});
244 } catch (NoSuchMethodException ex) {
245 constructor = args[0].getConstructor(new Class[] {Project.class});
246 }
247 String propName = getPropertyName(name, "addConfigured");
248 nestedTypes.put(propName, args[0]);
249 nestedCreators.put(propName, new AddNestedCreator(m,
250 constructor, AddNestedCreator.ADD_CONFIGURED));
251 } catch (NoSuchMethodException nse) {
252 // ignore
253 }
254 } else if (name.startsWith("add")
255 && java.lang.Void.TYPE.equals(returnType) && args.length == 1
256 && !java.lang.String.class.equals(args[0])
257 && !args[0].isArray() && !args[0].isPrimitive()) {
258 try {
259 Constructor constructor = null;
260 try {
261 constructor = args[0].getConstructor(new Class[] {});
262 } catch (NoSuchMethodException ex) {
263 constructor = args[0].getConstructor(new Class[] {Project.class});
264 }
265 String propName = getPropertyName(name, "add");
266 if (nestedTypes.get(propName) != null) {
267 /*
268 * Ignore this method as there is an addConfigured
269 * form of this method that has a higher
270 * priority
271 */
272 continue;
273 }
274 nestedTypes.put(propName, args[0]);
275 nestedCreators.put(propName, new AddNestedCreator(m,
276 constructor, AddNestedCreator.ADD));
277 } catch (NoSuchMethodException nse) {
278 // ignore
279 }
280 }
281 }
282 }
283
284 /**
285 * Certain set methods are part of the Ant core interface to tasks and
286 * therefore not to be considered for introspection
287 *
288 * @param name the name of the set method
289 * @param type the type of the set method's parameter
290 * @return true if the given set method is to be hidden.
291 */
292 private boolean isHiddenSetMethod(String name, Class type) {
293 if ("setLocation".equals(name) && org.apache.tools.ant.Location.class.equals(type)) {
294 return true;
295 }
296 if ("setTaskType".equals(name) && java.lang.String.class.equals(type)) {
297 return true;
298 }
299 return false;
300 }
301
302 /**
303 * Returns a helper for the given class, either from the cache
304 * or by creating a new instance.
305 *
306 * @param c The class for which a helper is required.
307 * Must not be <code>null</code>.
308 *
309 * @return a helper for the specified class
310 */
311 public static synchronized IntrospectionHelper getHelper(Class c) {
312 return getHelper(null, c);
313 }
314
315 /**
316 * Returns a helper for the given class, either from the cache
317 * or by creating a new instance.
318 *
319 * The method will make sure the helper will be cleaned up at the end of
320 * the project, and only one instance will be created for each class.
321 *
322 * @param p the project instance. Can be null, in which case the helper is not cached.
323 * @param c The class for which a helper is required.
324 * Must not be <code>null</code>.
325 *
326 * @return a helper for the specified class
327 */
328 public static IntrospectionHelper getHelper(Project p, Class c) {
329 IntrospectionHelper ih = (IntrospectionHelper) HELPERS.get(c.getName());
330 // If a helper cannot be found, or if the helper is for another
331 // classloader, create a new IH
332 if (ih == null || ih.bean != c) {
333 ih = new IntrospectionHelper(c);
334 if (p != null) {
335 // #30162: do *not* cache this if there is no project, as we
336 // cannot guarantee that the cache will be cleared.
337 HELPERS.put(c.getName(), ih);
338 }
339 }
340 return ih;
341 }
342
343 /**
344 * Sets the named attribute in the given element, which is part of the
345 * given project.
346 *
347 * @param p The project containing the element. This is used when files
348 * need to be resolved. Must not be <code>null</code>.
349 * @param element The element to set the attribute in. Must not be
350 * <code>null</code>.
351 * @param attributeName The name of the attribute to set. Must not be
352 * <code>null</code>.
353 * @param value The value to set the attribute to. This may be interpreted
354 * or converted to the necessary type if the setter method
355 * doesn't just take a string. Must not be <code>null</code>.
356 *
357 * @exception BuildException if the introspected class doesn't support
358 * the given attribute, or if the setting
359 * method fails.
360 */
361 public void setAttribute(Project p, Object element, String attributeName,
362 String value) throws BuildException {
363 AttributeSetter as = (AttributeSetter) attributeSetters.get(
364 attributeName.toLowerCase(Locale.US));
365 if (as == null) {
366 if (element instanceof DynamicAttributeNS) {
367 DynamicAttributeNS dc = (DynamicAttributeNS) element;
368 String uriPlusPrefix = ProjectHelper.extractUriFromComponentName(attributeName);
369 String uri = ProjectHelper.extractUriFromComponentName(uriPlusPrefix);
370 String localName = ProjectHelper.extractNameFromComponentName(attributeName);
371 String qName = "".equals(uri) ? localName : uri + ":" + localName;
372 dc.setDynamicAttribute(uri, localName, qName, value);
373 return;
374 }
375 if (element instanceof DynamicAttribute) {
376 DynamicAttribute dc = (DynamicAttribute) element;
377 dc.setDynamicAttribute(attributeName.toLowerCase(Locale.US), value);
378 return;
379 }
380 if (attributeName.indexOf(':') != -1) {
381 return; // Ignore attribute from unknown uri's
382 }
383 String msg = getElementName(p, element)
384 + " doesn't support the \"" + attributeName + "\" attribute.";
385 throw new UnsupportedAttributeException(msg, attributeName);
386 }
387 try {
388 as.set(p, element, value);
389 } catch (IllegalAccessException ie) {
390 // impossible as getMethods should only return public methods
391 throw new BuildException(ie);
392 } catch (InvocationTargetException ite) {
393 throw extractBuildException(ite);
394 }
395 }
396
397 /**
398 * Adds PCDATA to an element, using the element's
399 * <code>void addText(String)</code> method, if it has one. If no
400 * such method is present, a BuildException is thrown if the
401 * given text contains non-whitespace.
402 *
403 * @param project The project which the element is part of.
404 * Must not be <code>null</code>.
405 * @param element The element to add the text to.
406 * Must not be <code>null</code>.
407 * @param text The text to add.
408 * Must not be <code>null</code>.
409 *
410 * @exception BuildException if non-whitespace text is provided and no
411 * method is available to handle it, or if
412 * the handling method fails.
413 */
414 public void addText(Project project, Object element, String text)
415 throws BuildException {
416 if (addText == null) {
417 text = text.trim();
418 // Element doesn't handle text content
419 if (text.length() == 0) {
420 // Only whitespace - ignore
421 return;
422 }
423 // Not whitespace - fail
424 throw new BuildException(project.getElementName(element)
425 + " doesn't support nested text data (\"" + condenseText(text) + "\").");
426 }
427 try {
428 addText.invoke(element, new Object[] {text});
429 } catch (IllegalAccessException ie) {
430 // impossible as getMethods should only return public methods
431 throw new BuildException(ie);
432 } catch (InvocationTargetException ite) {
433 throw extractBuildException(ite);
434 }
435 }
436
437 /**
438 * Utility method to throw a NotSupported exception
439 *
440 * @param project the Project instance.
441 * @param parent the object which doesn't support a requested element
442 * @param elementName the name of the Element which is trying to be created.
443 */
444 public void throwNotSupported(Project project, Object parent, String elementName) {
445 String msg = project.getElementName(parent)
446 + " doesn't support the nested \"" + elementName + "\" element.";
447 throw new UnsupportedElementException(msg, elementName);
448 }
449
450 /**
451 * Get the specific NestedCreator for a given project/parent/element combination
452 * @param project ant project
453 * @param parentUri URI of the parent.
454 * @param parent the parent class
455 * @param elementName element to work with. This can contain
456 * a URI,localname tuple of of the form uri:localname
457 * @param child the bit of XML to work with
458 * @return a nested creator that can handle the child elements.
459 * @throws BuildException if the parent does not support child elements of that name
460 */
461 private NestedCreator getNestedCreator(
462 Project project, String parentUri, Object parent,
463 String elementName, UnknownElement child) throws BuildException {
464
465 String uri = ProjectHelper.extractUriFromComponentName(elementName);
466 String name = ProjectHelper.extractNameFromComponentName(elementName);
467
468 if (uri.equals(ProjectHelper.ANT_CORE_URI)) {
469 uri = "";
470 }
471 if (parentUri.equals(ProjectHelper.ANT_CORE_URI)) {
472 parentUri = "";
473 }
474 NestedCreator nc = null;
475 if (uri.equals(parentUri) || uri.length() == 0) {
476 nc = (NestedCreator) nestedCreators.get(name.toLowerCase(Locale.US));
477 }
478 if (nc == null) {
479 nc = createAddTypeCreator(project, parent, elementName);
480 }
481 if (nc == null && parent instanceof DynamicElementNS) {
482 DynamicElementNS dc = (DynamicElementNS) parent;
483 String qName = child == null ? name : child.getQName();
484 final Object nestedElement = dc.createDynamicElement(
485 child == null ? "" : child.getNamespace(), name, qName);
486 if (nestedElement != null) {
487 nc = new NestedCreator(null) {
488 Object create(Project project, Object parent, Object ignore) {
489 return nestedElement;
490 }
491 };
492 }
493 }
494 if (nc == null && parent instanceof DynamicElement) {
495 DynamicElement dc = (DynamicElement) parent;
496 final Object nestedElement = dc.createDynamicElement(name.toLowerCase(Locale.US));
497 if (nestedElement != null) {
498 nc = new NestedCreator(null) {
499 Object create(Project project, Object parent, Object ignore) {
500 return nestedElement;
501 }
502 };
503 }
504 }
505 if (nc == null) {
506 throwNotSupported(project, parent, elementName);
507 }
508 return nc;
509 }
510
511 /**
512 * Creates a named nested element. Depending on the results of the
513 * initial introspection, either a method in the given parent instance
514 * or a simple no-arg constructor is used to create an instance of the
515 * specified element type.
516 *
517 * @param project Project to which the parent object belongs.
518 * Must not be <code>null</code>. If the resulting
519 * object is an instance of ProjectComponent, its
520 * Project reference is set to this parameter value.
521 * @param parent Parent object used to create the instance.
522 * Must not be <code>null</code>.
523 * @param elementName Name of the element to create an instance of.
524 * Must not be <code>null</code>.
525 *
526 * @return an instance of the specified element type
527 * @deprecated since 1.6.x.
528 * This is not a namespace aware method.
529 *
530 * @exception BuildException if no method is available to create the
531 * element instance, or if the creating method fails.
532 */
533 public Object createElement(Project project, Object parent, String elementName)
534 throws BuildException {
535 NestedCreator nc = getNestedCreator(project, "", parent, elementName, null);
536 try {
537 Object nestedElement = nc.create(project, parent, null);
538 if (project != null) {
539 project.setProjectReference(nestedElement);
540 }
541 return nestedElement;
542 } catch (IllegalAccessException ie) {
543 // impossible as getMethods should only return public methods
544 throw new BuildException(ie);
545 } catch (InstantiationException ine) {
546 // impossible as getMethods should only return public methods
547 throw new BuildException(ine);
548 } catch (InvocationTargetException ite) {
549 throw extractBuildException(ite);
550 }
551 }
552
553 /**
554 * returns an object that creates and stores an object
555 * for an element of a parent.
556 *
557 * @param project Project to which the parent object belongs.
558 * @param parentUri The namespace uri of the parent object.
559 * @param parent Parent object used to create the creator object to
560 * create and store and instance of a subelement.
561 * @param elementName Name of the element to create an instance of.
562 * @param ue The unknown element associated with the element.
563 * @return a creator object to create and store the element instance.
564 */
565 public Creator getElementCreator(
566 Project project, String parentUri, Object parent, String elementName, UnknownElement ue) {
567 NestedCreator nc = getNestedCreator(project, parentUri, parent, elementName, ue);
568 return new Creator(project, parent, nc);
569 }
570
571 /**
572 * Indicates whether the introspected class is a dynamic one,
573 * supporting arbitrary nested elements and/or attributes.
574 *
575 * @return <code>true<code> if the introspected class is dynamic;
576 * <code>false<code> otherwise.
577 * @since Ant 1.6.3
578 *
579 * @see DynamicElement
580 * @see DynamicElementNS
581 */
582 public boolean isDynamic() {
583 return DynamicElement.class.isAssignableFrom(bean)
584 || DynamicElementNS.class.isAssignableFrom(bean);
585 }
586
587 /**
588 * Indicates whether the introspected class is a task container,
589 * supporting arbitrary nested tasks/types.
590 *
591 * @return <code>true<code> if the introspected class is a container;
592 * <code>false<code> otherwise.
593 * @since Ant 1.6.3
594 *
595 * @see TaskContainer
596 */
597 public boolean isContainer() {
598 return TaskContainer.class.isAssignableFrom(bean);
599 }
600
601 /**
602 * Indicates if this element supports a nested element of the
603 * given name.
604 *
605 * @param elementName the name of the nested element being checked
606 *
607 * @return true if the given nested element is supported
608 */
609 public boolean supportsNestedElement(String elementName) {
610 return supportsNestedElement("", elementName);
611 }
612
613 /**
614 * Indicate if this element supports a nested element of the
615 * given name.
616 *
617 * @param parentUri the uri of the parent
618 * @param elementName the name of the nested element being checked
619 *
620 * @return true if the given nested element is supported
621 */
622 public boolean supportsNestedElement(String parentUri, String elementName) {
623 if (isDynamic() || addTypeMethods.size() > 0) {
624 return true;
625 }
626 String name = ProjectHelper.extractNameFromComponentName(elementName);
627 if (!nestedCreators.containsKey(name.toLowerCase(Locale.US))) {
628 return false;
629 }
630 String uri = ProjectHelper.extractUriFromComponentName(elementName);
631 if (uri.equals(ProjectHelper.ANT_CORE_URI)) {
632 uri = "";
633 }
634 if ("".equals(uri)) {
635 return true;
636 }
637 if (parentUri.equals(ProjectHelper.ANT_CORE_URI)) {
638 parentUri = "";
639 }
640 return uri.equals(parentUri);
641 }
642
643 /**
644 * Stores a named nested element using a storage method determined
645 * by the initial introspection. If no appropriate storage method
646 * is available, this method returns immediately.
647 *
648 * @param project Ignored in this implementation.
649 * May be <code>null</code>.
650 *
651 * @param parent Parent instance to store the child in.
652 * Must not be <code>null</code>.
653 *
654 * @param child Child instance to store in the parent.
655 * Should not be <code>null</code>.
656 *
657 * @param elementName Name of the child element to store.
658 * May be <code>null</code>, in which case
659 * this method returns immediately.
660 *
661 * @exception BuildException if the storage method fails.
662 */
663 public void storeElement(Project project, Object parent, Object child,
664 String elementName) throws BuildException {
665 if (elementName == null) {
666 return;
667 }
668 NestedCreator ns = (NestedCreator) nestedCreators.get(elementName.toLowerCase(Locale.US));
669 if (ns == null) {
670 return;
671 }
672 try {
673 ns.store(parent, child);
674 } catch (IllegalAccessException ie) {
675 // impossible as getMethods should only return public methods
676 throw new BuildException(ie);
677 } catch (InstantiationException ine) {
678 // impossible as getMethods should only return public methods
679 throw new BuildException(ine);
680 } catch (InvocationTargetException ite) {
681 throw extractBuildException(ite);
682 }
683 }
684
685 /**
686 * Helper method to extract the inner fault from an {@link InvocationTargetException}, and turn
687 * it into a BuildException. If it is already a BuildException, it is type cast and returned; if
688 * not a new BuildException is created containing the child as nested text.
689 * @param ite
690 * @return the nested exception
691 */
692 private static BuildException extractBuildException(InvocationTargetException ite) {
693 Throwable t = ite.getTargetException();
694 if (t instanceof BuildException) {
695 return (BuildException) t;
696 }
697 return new BuildException(t);
698 }
699
700 /**
701 * Returns the type of a named nested element.
702 *
703 * @param elementName The name of the element to find the type of.
704 * Must not be <code>null</code>.
705 *
706 * @return the type of the nested element with the specified name.
707 * This will never be <code>null</code>.
708 *
709 * @exception BuildException if the introspected class does not
710 * support the named nested element.
711 */
712 public Class getElementType(String elementName) throws BuildException {
713 Class nt = (Class) nestedTypes.get(elementName);
714 if (nt == null) {
715 throw new UnsupportedElementException("Class "
716 + bean.getName() + " doesn't support the nested \""
717 + elementName + "\" element.", elementName);
718 }
719 return nt;
720 }
721
722 /**
723 * Returns the type of a named attribute.
724 *
725 * @param attributeName The name of the attribute to find the type of.
726 * Must not be <code>null</code>.
727 *
728 * @return the type of the attribute with the specified name.
729 * This will never be <code>null</code>.
730 *
731 * @exception BuildException if the introspected class does not
732 * support the named attribute.
733 */
734 public Class getAttributeType(String attributeName) throws BuildException {
735 Class at = (Class) attributeTypes.get(attributeName);
736 if (at == null) {
737 throw new UnsupportedAttributeException("Class "
738 + bean.getName() + " doesn't support the \""
739 + attributeName + "\" attribute.", attributeName);
740 }
741 return at;
742 }
743
744 /**
745 * Returns the addText method when the introspected
746 * class supports nested text.
747 *
748 * @return the method on this introspected class that adds nested text.
749 * Cannot be <code>null</code>.
750 * @throws BuildException if the introspected class does not
751 * support the nested text.
752 * @since Ant 1.6.3
753 */
754 public Method getAddTextMethod() throws BuildException {
755 if (!supportsCharacters()) {
756 throw new BuildException("Class " + bean.getName()
757 + " doesn't support nested text data.");
758 }
759 return addText;
760 }
761
762 /**
763 * Returns the adder or creator method of a named nested element.
764 *
765 * @param elementName The name of the attribute to find the setter
766 * method of. Must not be <code>null</code>.
767 * @return the method on this introspected class that adds or creates this
768 * nested element. Can be <code>null</code> when the introspected
769 * class is a dynamic configurator!
770 * @throws BuildException if the introspected class does not
771 * support the named nested element.
772 * @since Ant 1.6.3
773 */
774 public Method getElementMethod(String elementName) throws BuildException {
775 Object creator = nestedCreators.get(elementName);
776 if (creator == null) {
777 throw new UnsupportedElementException("Class "
778 + bean.getName() + " doesn't support the nested \""
779 + elementName + "\" element.", elementName);
780 }
781 return ((NestedCreator) creator).method;
782 }
783
784 /**
785 * Returns the setter method of a named attribute.
786 *
787 * @param attributeName The name of the attribute to find the setter
788 * method of. Must not be <code>null</code>.
789 * @return the method on this introspected class that sets this attribute.
790 * This will never be <code>null</code>.
791 * @throws BuildException if the introspected class does not
792 * support the named attribute.
793 * @since Ant 1.6.3
794 */
795 public Method getAttributeMethod(String attributeName) throws BuildException {
796 Object setter = attributeSetters.get(attributeName);
797 if (setter == null) {
798 throw new UnsupportedAttributeException("Class "
799 + bean.getName() + " doesn't support the \""
800 + attributeName + "\" attribute.", attributeName);
801 }
802 return ((AttributeSetter) setter).method;
803 }
804
805 /**
806 * Returns whether or not the introspected class supports PCDATA.
807 *
808 * @return whether or not the introspected class supports PCDATA.
809 */
810 public boolean supportsCharacters() {
811 return addText != null;
812 }
813
814 /**
815 * Returns an enumeration of the names of the attributes supported by the introspected class.
816 *
817 * @return an enumeration of the names of the attributes supported by the introspected class.
818 * @see #getAttributeMap
819 */
820 public Enumeration getAttributes() {
821 return attributeSetters.keys();
822 }
823
824 /**
825 * Returns a read-only map of attributes supported by the introspected class.
826 *
827 * @return an attribute name to attribute <code>Class</code>
828 * unmodifiable map. Can be empty, but never <code>null</code>.
829 * @since Ant 1.6.3
830 */
831 public Map getAttributeMap() {
832 return attributeTypes.isEmpty()
833 ? Collections.EMPTY_MAP : Collections.unmodifiableMap(attributeTypes);
834 }
835
836 /**
837 * Returns an enumeration of the names of the nested elements supported
838 * by the introspected class.
839 *
840 * @return an enumeration of the names of the nested elements supported
841 * by the introspected class.
842 * @see #getNestedElementMap
843 */
844 public Enumeration getNestedElements() {
845 return nestedTypes.keys();
846 }
847
848 /**
849 * Returns a read-only map of nested elements supported
850 * by the introspected class.
851 *
852 * @return a nested-element name to nested-element <code>Class</code>
853 * unmodifiable map. Can be empty, but never <code>null</code>.
854 * @since Ant 1.6.3
855 */
856 public Map getNestedElementMap() {
857 return nestedTypes.isEmpty()
858 ? Collections.EMPTY_MAP : Collections.unmodifiableMap(nestedTypes);
859 }
860
861 /**
862 * Returns a read-only list of extension points supported
863 * by the introspected class.
864 * <p>
865 * A task/type or nested element with void methods named <code>add()<code>
866 * or <code>addConfigured()</code>, taking a single class or interface
867 * argument, supports extensions point. This method returns the list of
868 * all these <em>void add[Configured](type)</em> methods.
869 *
870 * @return a list of void, single argument add() or addConfigured()
871 * <code>Method<code>s of all supported extension points.
872 * These methods are sorted such that if the argument type of a
873 * method derives from another type also an argument of a method
874 * of this list, the method with the most derived argument will
875 * always appear first. Can be empty, but never <code>null</code>.
876 * @since Ant 1.6.3
877 */
878 public List getExtensionPoints() {
879 return addTypeMethods.isEmpty()
880 ? Collections.EMPTY_LIST : Collections.unmodifiableList(addTypeMethods);
881 }
882
883 /**
884 * Creates an implementation of AttributeSetter for the given
885 * attribute type. Conversions (where necessary) are automatically
886 * made for the following types:
887 * <ul>
888 * <li>String (left as it is)
889 * <li>Character/char (first character is used)
890 * <li>Boolean/boolean
891 * ({@link Project#toBoolean(String) Project.toBoolean(String)} is used)
892 * <li>Class (Class.forName is used)
893 * <li>File (resolved relative to the appropriate project)
894 * <li>Path (resolve relative to the appropriate project)
895 * <li>EnumeratedAttribute (uses its own
896 * {@link EnumeratedAttribute#setValue(String) setValue} method)
897 * <li>Other primitive types (wrapper classes are used with constructors
898 * taking String)
899 * </ul>
900 *
901 * If none of the above covers the given parameters, a constructor for the
902 * appropriate class taking a String parameter is used if it is available.
903 *
904 * @param m The method to invoke on the bean when the setter is invoked.
905 * Must not be <code>null</code>.
906 * @param arg The type of the single argument of the bean's method.
907 * Must not be <code>null</code>.
908 * @param attrName the name of the attribute for which the setter is being
909 * created.
910 *
911 * @return an appropriate AttributeSetter instance, or <code>null</code>
912 * if no appropriate conversion is available.
913 */
914 private AttributeSetter createAttributeSetter(final Method m,
915 Class arg,
916 final String attrName) {
917 // use wrappers for primitive classes, e.g. int and
918 // Integer are treated identically
919 final Class reflectedArg = PRIMITIVE_TYPE_MAP.containsKey(arg)
920 ? (Class) PRIMITIVE_TYPE_MAP.get(arg) : arg;
921
922 // simplest case - setAttribute expects String
923 if (java.lang.String.class.equals(reflectedArg)) {
924 return new AttributeSetter(m) {
925 public void set(Project p, Object parent, String value)
926 throws InvocationTargetException, IllegalAccessException {
927 m.invoke(parent, (Object[]) new String[] {value});
928 }
929 };
930 }
931 // char and Character get special treatment - take the first character
932 if (java.lang.Character.class.equals(reflectedArg)) {
933 return new AttributeSetter(m) {
934 public void set(Project p, Object parent, String value)
935 throws InvocationTargetException, IllegalAccessException {
936 if (value.length() == 0) {
937 throw new BuildException("The value \"\" is not a "
938 + "legal value for attribute \"" + attrName + "\"");
939 }
940 m.invoke(parent, (Object[]) new Character[] {new Character(value.charAt(0))});
941 }
942 };
943 }
944 // boolean and Boolean get special treatment because we have a nice method in Project
945 if (java.lang.Boolean.class.equals(reflectedArg)) {
946 return new AttributeSetter(m) {
947 public void set(Project p, Object parent, String value)
948 throws InvocationTargetException, IllegalAccessException {
949 m.invoke(parent, (Object[]) new Boolean[] {
950 Project.toBoolean(value) ? Boolean.TRUE : Boolean.FALSE });
951 }
952 };
953 }
954 // Class doesn't have a String constructor but a decent factory method
955 if (java.lang.Class.class.equals(reflectedArg)) {
956 return new AttributeSetter(m) {
957 public void set(Project p, Object parent, String value)
958 throws InvocationTargetException, IllegalAccessException, BuildException {
959 try {
960 m.invoke(parent, new Object[] {Class.forName(value)});
961 } catch (ClassNotFoundException ce) {
962 throw new BuildException(ce);
963 }
964 }
965 };
966 }
967 // resolve relative paths through Project
968 if (java.io.File.class.equals(reflectedArg)) {
969 return new AttributeSetter(m) {
970 public void set(Project p, Object parent, String value)
971 throws InvocationTargetException, IllegalAccessException {
972 m.invoke(parent, new Object[] {p.resolveFile(value)});
973 }
974 };
975 }
976 // EnumeratedAttributes have their own helper class
977 if (EnumeratedAttribute.class.isAssignableFrom(reflectedArg)) {
978 return new AttributeSetter(m) {
979 public void set(Project p, Object parent, String value)
980 throws InvocationTargetException, IllegalAccessException, BuildException {
981 try {
982 EnumeratedAttribute ea = (EnumeratedAttribute) reflectedArg.newInstance();
983 ea.setValue(value);
984 m.invoke(parent, new Object[] {ea});
985 } catch (InstantiationException ie) {
986 throw new BuildException(ie);
987 }
988 }
989 };
990 }
991 Class enumClass = null;
992 try {
993 enumClass = Class.forName("java.lang.Enum");
994 } catch (ClassNotFoundException e) {
995 //ignore
996 }
997 if (enumClass != null && enumClass.isAssignableFrom(reflectedArg)) {
998 return new AttributeSetter(m) {
999 public void set(Project p, Object parent, String value)
1000 throws InvocationTargetException, IllegalAccessException, BuildException {
1001 try {
1002 m.invoke(parent, new Object[] {
1003 reflectedArg.getMethod("valueOf", new Class[] {String.class}).
1004 invoke(null, new Object[] {value})});
1005 } catch (InvocationTargetException x) {
1006 //there is specific logic here for the value being out of the allowed
1007 //set of enumerations.
1008 if (x.getTargetException() instanceof IllegalArgumentException) {
1009 throw new BuildException("'" + value + "' is not a permitted value for "
1010 + reflectedArg.getName());
1011 }
1012 //only if the exception is not an IllegalArgument do we request the
1013 //BuildException via extractBuildException():
1014 throw extractBuildException(x);
1015 } catch (Exception x) {
1016 //any other failure of invoke() to work.
1017 throw new BuildException(x);
1018 }
1019 }
1020 };
1021 }
1022 if (java.lang.Long.class.equals(reflectedArg)) {
1023 return new AttributeSetter(m) {
1024 public void set(Project p, Object parent, String value)
1025 throws InvocationTargetException, IllegalAccessException, BuildException {
1026 try {
1027 m.invoke(parent, new Object[] {
1028 new Long(StringUtils.parseHumanSizes(value)) });
1029 } catch (InvocationTargetException e) {
1030 throw e;
1031 } catch (IllegalAccessException e) {
1032 throw e;
1033 } catch (Exception e) {
1034 throw new BuildException(e);
1035 }
1036 }
1037 };
1038 }
1039 // worst case. look for a public String constructor and use it
1040 // also supports new Whatever(Project, String) as for Path or Reference
1041 // This is used (deliberately) for all primitives/wrappers other than
1042 // char, boolean, and long.
1043 boolean includeProject;
1044 Constructor c;
1045 try {
1046 // First try with Project.
1047 c = reflectedArg.getConstructor(new Class[] {Project.class, String.class});
1048 includeProject = true;
1049 } catch (NoSuchMethodException nme) {
1050 // OK, try without.
1051 try {
1052 c = reflectedArg.getConstructor(new Class[] {String.class});
1053 includeProject = false;
1054 } catch (NoSuchMethodException nme2) {
1055 // Well, no matching constructor.
1056 return null;
1057 }
1058 }
1059 final boolean finalIncludeProject = includeProject;
1060 final Constructor finalConstructor = c;
1061
1062 return new AttributeSetter(m) {
1063 public void set(Project p, Object parent, String value)
1064 throws InvocationTargetException, IllegalAccessException, BuildException {
1065 try {
1066 Object[] args = finalIncludeProject
1067 ? new Object[] {p, value} : new Object[] {value};
1068
1069 Object attribute = finalConstructor.newInstance(args);
1070 if (p != null) {
1071 p.setProjectReference(attribute);
1072 }
1073 m.invoke(parent, new Object[] {attribute});
1074 } catch (InstantiationException ie) {
1075 throw new BuildException(ie);
1076 }
1077 }
1078 };
1079 }
1080
1081 /**
1082 * Returns a description of the type of the given element in
1083 * relation to a given project. This is used for logging purposes
1084 * when the element is asked to cope with some data it has no way of handling.
1085 *
1086 * @param project The project the element is defined in. Must not be <code>null</code>.
1087 *
1088 * @param element The element to describe. Must not be <code>null</code>.
1089 *
1090 * @return a description of the element type
1091 */
1092 private String getElementName(Project project, Object element) {
1093 return project.getElementName(element);
1094 }
1095
1096 /**
1097 * Extracts the name of a property from a method name by subtracting
1098 * a given prefix and converting into lower case. It is up to calling
1099 * code to make sure the method name does actually begin with the
1100 * specified prefix - no checking is done in this method.
1101 *
1102 * @param methodName The name of the method in question. Must not be <code>null</code>.
1103 * @param prefix The prefix to remove. Must not be <code>null</code>.
1104 *
1105 * @return the lower-cased method name with the prefix removed.
1106 */
1107 private static String getPropertyName(String methodName, String prefix) {
1108 return methodName.substring(prefix.length()).toLowerCase(Locale.US);
1109 }
1110
1111 /**
1112 * creator - allows use of create/store external
1113 * to IntrospectionHelper.
1114 * The class is final as it has a private constructor.
1115 */
1116 public static final class Creator {
1117 private NestedCreator nestedCreator;
1118 private Object parent;
1119 private Project project;
1120 private Object nestedObject;
1121 private String polyType;
1122
1123 /**
1124 * Creates a new Creator instance.
1125 * This object is given to the UnknownElement to create
1126 * objects for sub-elements. UnknownElement calls
1127 * create to create an object, the object then gets
1128 * configured and then UnknownElement calls store.
1129 * SetPolyType may be used to override the type used
1130 * to create the object with. SetPolyType gets called before create.
1131 *
1132 * @param project the current project
1133 * @param parent the parent object to create the object in
1134 * @param nestedCreator the nested creator object to use
1135 */
1136 private Creator(Project project, Object parent, NestedCreator nestedCreator) {
1137 this.project = project;
1138 this.parent = parent;
1139 this.nestedCreator = nestedCreator;
1140 }
1141
1142 /**
1143 * Used to override the class used to create the object.
1144 *
1145 * @param polyType a ant component type name
1146 */
1147 public void setPolyType(String polyType) {
1148 this.polyType = polyType;
1149 }
1150
1151 /**
1152 * Create an object using this creator, which is determined by introspection.
1153 *
1154 * @return the created object
1155 */
1156 public Object create() {
1157 if (polyType != null) {
1158 if (!nestedCreator.isPolyMorphic()) {
1159 throw new BuildException(
1160 "Not allowed to use the polymorphic form for this element");
1161 }
1162 ComponentHelper helper = ComponentHelper.getComponentHelper(project);
1163 nestedObject = helper.createComponent(polyType);
1164 if (nestedObject == null) {
1165 throw new BuildException("Unable to create object of type " + polyType);
1166 }
1167 }
1168 try {
1169 nestedObject = nestedCreator.create(project, parent, nestedObject);
1170 if (project != null) {
1171 project.setProjectReference(nestedObject);
1172 }
1173 return nestedObject;
1174 } catch (IllegalAccessException ex) {
1175 throw new BuildException(ex);
1176 } catch (InstantiationException ex) {
1177 throw new BuildException(ex);
1178 } catch (IllegalArgumentException ex) {
1179 if (polyType == null) {
1180 throw ex;
1181 }
1182 throw new BuildException("Invalid type used " + polyType);
1183 } catch (InvocationTargetException ex) {
1184 throw extractBuildException(ex);
1185 }
1186 }
1187
1188 /**
1189 * @return the real object (used currently only for presetdef).
1190 */
1191 public Object getRealObject() {
1192 return nestedCreator.getRealObject();
1193 }
1194
1195 /**
1196 * Stores the nested element object using a storage method determined by introspection.
1197 *
1198 */
1199 public void store() {
1200 try {
1201 nestedCreator.store(parent, nestedObject);
1202 } catch (IllegalAccessException ex) {
1203 throw new BuildException(ex);
1204 } catch (InstantiationException ex) {
1205 throw new BuildException(ex);
1206 } catch (IllegalArgumentException ex) {
1207 if (polyType == null) {
1208 throw ex;
1209 }
1210 throw new BuildException("Invalid type used " + polyType);
1211 } catch (InvocationTargetException ex) {
1212 throw extractBuildException(ex);
1213 }
1214 }
1215 }
1216
1217 /**
1218 * Internal interface used to create nested elements. Not documented
1219 * in detail for reasons of source code readability.
1220 */
1221 private abstract static class NestedCreator {
1222 private Method method; // the method called to add/create the nested element
1223
1224 protected NestedCreator(Method m) {
1225 method = m;
1226 }
1227 Method getMethod() {
1228 return method;
1229 }
1230 boolean isPolyMorphic() {
1231 return false;
1232 }
1233 Object getRealObject() {
1234 return null;
1235 }
1236 abstract Object create(Project project, Object parent, Object child)
1237 throws InvocationTargetException, IllegalAccessException, InstantiationException;
1238
1239 void store(Object parent, Object child)
1240 throws InvocationTargetException, IllegalAccessException, InstantiationException {
1241 // DO NOTHING
1242 }
1243 }
1244
1245 private static class CreateNestedCreator extends NestedCreator {
1246 CreateNestedCreator(Method m) {
1247 super(m);
1248 }
1249
1250 Object create(Project project, Object parent, Object ignore)
1251 throws InvocationTargetException, IllegalAccessException {
1252 return getMethod().invoke(parent, new Object[] {});
1253 }
1254 }
1255
1256 /** Version to use for addXXX and addConfiguredXXX */
1257 private static class AddNestedCreator extends NestedCreator {
1258
1259 static final int ADD = 1;
1260 static final int ADD_CONFIGURED = 2;
1261
1262 private Constructor constructor;
1263 private int behavior; // ADD or ADD_CONFIGURED
1264
1265 AddNestedCreator(Method m, Constructor c, int behavior) {
1266 super(m);
1267 this.constructor = c;
1268 this.behavior = behavior;
1269 }
1270
1271 boolean isPolyMorphic() {
1272 return true;
1273 }
1274
1275 Object create(Project project, Object parent, Object child)
1276 throws InvocationTargetException, IllegalAccessException, InstantiationException {
1277 if (child == null) {
1278 child = constructor.newInstance(
1279 constructor.getParameterTypes().length == 0
1280 ? new Object[] {} : new Object[] {project});
1281 }
1282 if (child instanceof PreSetDef.PreSetDefinition) {
1283 child = ((PreSetDef.PreSetDefinition) child).createObject(project);
1284 }
1285 if (behavior == ADD) {
1286 istore(parent, child);
1287 }
1288 return child;
1289 }
1290
1291 void store(Object parent, Object child)
1292 throws InvocationTargetException, IllegalAccessException, InstantiationException {
1293 if (behavior == ADD_CONFIGURED) {
1294 istore(parent, child);
1295 }
1296 }
1297
1298 private void istore(Object parent, Object child)
1299 throws InvocationTargetException, IllegalAccessException, InstantiationException {
1300 getMethod().invoke(parent, new Object[] {child});
1301 }
1302 }
1303
1304 /**
1305 * Internal interface used to setting element attributes. Not documented
1306 * in detail for reasons of source code readability.
1307 */
1308 private abstract static class AttributeSetter {
1309 private Method method; // the method called to set the attribute
1310
1311 protected AttributeSetter(Method m) {
1312 method = m;
1313 }
1314 abstract void set(Project p, Object parent, String value)
1315 throws InvocationTargetException, IllegalAccessException, BuildException;
1316 }
1317
1318 /**
1319 * Clears the static cache of on build finished.
1320 */
1321 public static void clearCache() {
1322 HELPERS.clear();
1323 }
1324
1325 /**
1326 * Create a NestedCreator for the given element.
1327 * @param project owning project
1328 * @param parent Parent object used to create the instance.
1329 * @param elementName name of the element
1330 * @return a nested creator, or null if there is no component of the given name, or it
1331 * has no matching add type methods
1332 * @throws BuildException
1333 */
1334 private NestedCreator createAddTypeCreator(
1335 Project project, Object parent, String elementName) throws BuildException {
1336 if (addTypeMethods.size() == 0) {
1337 return null;
1338 }
1339 ComponentHelper helper = ComponentHelper.getComponentHelper(project);
1340
1341 Object addedObject = null;
1342 Method addMethod = null;
1343 Class clazz = helper.getComponentClass(elementName);
1344 if (clazz == null) {
1345 return null;
1346 }
1347 addMethod = findMatchingMethod(clazz, addTypeMethods);
1348 if (addMethod == null) {
1349 return null;
1350 }
1351 addedObject = helper.createComponent(elementName);
1352 if (addedObject == null) {
1353 return null;
1354 }
1355 Object rObject = addedObject;
1356 if (addedObject instanceof PreSetDef.PreSetDefinition) {
1357 rObject = ((PreSetDef.PreSetDefinition) addedObject).createObject(project);
1358 }
1359 final Object nestedObject = addedObject;
1360 final Object realObject = rObject;
1361
1362 return new NestedCreator(addMethod) {
1363 Object create(Project project, Object parent, Object ignore)
1364 throws InvocationTargetException, IllegalAccessException {
1365 if (!getMethod().getName().endsWith("Configured")) {
1366 getMethod().invoke(parent, new Object[] {realObject});
1367 }
1368 return nestedObject;
1369 }
1370
1371 Object getRealObject() {
1372 return realObject;
1373 }
1374
1375 void store(Object parent, Object child) throws InvocationTargetException,
1376 IllegalAccessException, InstantiationException {
1377 if (getMethod().getName().endsWith("Configured")) {
1378 getMethod().invoke(parent, new Object[] {realObject});
1379 }
1380 }
1381 };
1382 }
1383
1384 /**
1385 * Inserts an add or addConfigured method into
1386 * the addTypeMethods array. The array is
1387 * ordered so that the more derived classes are first.
1388 * If both add and addConfigured are present, the addConfigured will take priority.
1389 * @param method the <code>Method</code> to insert.
1390 */
1391 private void insertAddTypeMethod(Method method) {
1392 Class argClass = method.getParameterTypes()[0];
1393 for (int c = 0; c < addTypeMethods.size(); ++c) {
1394 Method current = (Method) addTypeMethods.get(c);
1395 if (current.getParameterTypes()[0].equals(argClass)) {
1396 if (method.getName().equals("addConfigured")) {
1397 // add configured replaces the add method
1398 addTypeMethods.set(c, method);
1399 }
1400 return; // Already present
1401 }
1402 if (current.getParameterTypes()[0].isAssignableFrom(argClass)) {
1403 addTypeMethods.add(c, method);
1404 return; // higher derived
1405 }
1406 }
1407 addTypeMethods.add(method);
1408 }
1409
1410 /**
1411 * Search the list of methods to find the first method
1412 * that has a parameter that accepts the nested element object.
1413 * @param paramClass the <code>Class</code> type to search for.
1414 * @param methods the <code>List</code> of methods to search.
1415 * @return a matching <code>Method</code>; null if none found.
1416 */
1417 private Method findMatchingMethod(Class paramClass, List methods) {
1418 Class matchedClass = null;
1419 Method matchedMethod = null;
1420
1421 for (int i = 0; i < methods.size(); ++i) {
1422 Method method = (Method) methods.get(i);
1423 Class methodClass = method.getParameterTypes()[0];
1424 if (methodClass.isAssignableFrom(paramClass)) {
1425 if (matchedClass == null) {
1426 matchedClass = methodClass;
1427 matchedMethod = method;
1428 } else if (!methodClass.isAssignableFrom(matchedClass)) {
1429 throw new BuildException("ambiguous: types " + matchedClass.getName() + " and "
1430 + methodClass.getName() + " match " + paramClass.getName());
1431 }
1432 }
1433 }
1434 return matchedMethod;
1435 }
1436
1437 private String condenseText(final String text) {
1438 if (text.length() <= MAX_REPORT_NESTED_TEXT) {
1439 return text;
1440 }
1441 int ends = (MAX_REPORT_NESTED_TEXT - ELLIPSIS.length()) / 2;
1442 return new StringBuffer(text).replace(ends, text.length() - ends, ELLIPSIS).toString();
1443 }
1444 }