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