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