1. /* $Id: SetNestedPropertiesRule.java,v 1.8 2004/05/10 06:52:50 skitching Exp $
  2. *
  3. * Copyright 2003-2004 The Apache Software Foundation.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * 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. package org.apache.commons.digester;
  18. import java.util.List;
  19. import java.util.LinkedList;
  20. import java.util.ArrayList;
  21. import java.util.Iterator;
  22. import java.util.ListIterator;
  23. import java.util.HashMap;
  24. import java.beans.PropertyDescriptor;
  25. import org.apache.commons.beanutils.BeanUtils;
  26. import org.apache.commons.beanutils.DynaBean;
  27. import org.apache.commons.beanutils.DynaProperty;
  28. import org.apache.commons.beanutils.PropertyUtils;
  29. import org.xml.sax.Attributes;
  30. import org.apache.commons.logging.Log;
  31. import org.apache.commons.logging.LogFactory;
  32. /**
  33. * <p>Rule implementation that sets properties on the object at the top of the
  34. * stack, based on child elements with names matching properties on that
  35. * object.</p>
  36. *
  37. * <p>Example input that can be processed by this rule:</p>
  38. * <pre>
  39. * [widget]
  40. * [height]7[/height]
  41. * [width]8[/width]
  42. * [label]Hello, world[/label]
  43. * [/widget]
  44. * </pre>
  45. *
  46. * <p>This rule supports custom mapping of attribute names to property names.
  47. * The default mapping for particular attributes can be overridden by using
  48. * {@link #SetNestedPropertiesRule(String[] elementNames,
  49. * String[] propertyNames)}.
  50. * This allows child elements to be mapped to properties with different names.
  51. * Certain elements can also be marked to be ignored.</p>
  52. *
  53. * <p>A very similar effect can be achieved using a combination of the
  54. * <code>BeanPropertySetterRule</code> and the <code>ExtendedBaseRules</code>
  55. * rules manager; this <code>Rule</code>, however, works fine with the default
  56. * <code>RulesBase</code> rules manager.</p>
  57. *
  58. * <p><b>Implementation Notes</b></p>
  59. *
  60. * <p>This class works by creating its own simple Rules implementation. When
  61. * begin is invoked on this rule, the digester's current rules object is
  62. * replaced by a custom one. When end is invoked for this rule, the original
  63. * rules object is restored. The digester rules objects therefore behave in
  64. * a stack-like manner.</p>
  65. *
  66. * <p>For each child element encountered, the custom Rules implementation
  67. * ensures that a special AnyChildRule instance is included in the matches
  68. * returned to the digester, and it is this rule instance that is responsible
  69. * for setting the appropriate property on the target object (if such a property
  70. * exists). The effect is therefore like a "trailing wildcard pattern". The
  71. * custom Rules implementation also returns the matches provided by the
  72. * underlying Rules implementation for the same pattern, so other rules
  73. * are not "disabled" during processing of a SetNestedPropertiesRule.</p>
  74. *
  75. * @since 1.6
  76. */
  77. public class SetNestedPropertiesRule extends Rule {
  78. /**
  79. * Dummy object that can be placed in collections to indicate an
  80. * ignored property when null cannot be used for that purpose.
  81. */
  82. private static final String PROP_IGNORE = "ignore-me";
  83. private Log log = null;
  84. private AnyChildRule anyChildRule = new AnyChildRule();
  85. private AnyChildRules newRules = new AnyChildRules(anyChildRule);
  86. private Rules oldRules = null;
  87. private boolean trimData = true;
  88. private boolean allowUnknownChildElements = false;
  89. private HashMap elementNames = new HashMap();
  90. // ----------------------------------------------------------- Constructors
  91. /**
  92. * Base constructor.
  93. */
  94. public SetNestedPropertiesRule() {
  95. // nothing to set up
  96. }
  97. /**
  98. * <p>Convenience constructor overrides the mapping for just one property.</p>
  99. *
  100. * <p>For details about how this works, see
  101. * {@link #SetNestedPropertiesRule(String[] elementNames,
  102. * String[] propertyNames)}.</p>
  103. *
  104. * @param elementName map the child element to match
  105. * @param propertyName to a property with this name
  106. */
  107. public SetNestedPropertiesRule(String elementName, String propertyName) {
  108. elementNames.put(elementName, propertyName);
  109. }
  110. /**
  111. * <p>Constructor allows element->property mapping to be overriden.</p>
  112. *
  113. * <p>Two arrays are passed in.
  114. * One contains the element names and the other the property names.
  115. * The element name / property name pairs are match by position
  116. * In order words, the first string in the element name list matches
  117. * to the first string in the property name list and so on.</p>
  118. *
  119. * <p>If a property name is null or the element name has no matching
  120. * property name, then this indicates that the element should be ignored.</p>
  121. *
  122. * <h5>Example One</h5>
  123. * <p> The following constructs a rule that maps the <code>alt-city</code>
  124. * element to the <code>city</code> property and the <code>alt-state</code>
  125. * to the <code>state</code> property.
  126. * All other child elements are mapped as usual using exact name matching.
  127. * <code><pre>
  128. * SetNestedPropertiesRule(
  129. * new String[] {"alt-city", "alt-state"},
  130. * new String[] {"city", "state"});
  131. * </pre></code>
  132. *
  133. * <h5>Example Two</h5>
  134. * <p> The following constructs a rule that maps the <code>class</code>
  135. * element to the <code>className</code> property.
  136. * The element <code>ignore-me</code> is not mapped.
  137. * All other elements are mapped as usual using exact name matching.
  138. * <code><pre>
  139. * SetPropertiesRule(
  140. * new String[] {"class", "ignore-me"},
  141. * new String[] {"className"});
  142. * </pre></code>
  143. *
  144. * @param elementNames names of elements to map
  145. * @param propertyNames names of properties mapped to
  146. */
  147. public SetNestedPropertiesRule(String[] elementNames, String[] propertyNames) {
  148. for (int i=0, size=elementNames.length; i<size; i++) {
  149. String propName = null;
  150. if (i < propertyNames.length) {
  151. propName = propertyNames[i];
  152. }
  153. if (propName == null) {
  154. this.elementNames.put(elementNames[i], PROP_IGNORE);
  155. }
  156. else {
  157. this.elementNames.put(elementNames[i], propName);
  158. }
  159. }
  160. }
  161. // --------------------------------------------------------- Public Methods
  162. /** Invoked when rule is added to digester. */
  163. public void setDigester(Digester digester) {
  164. super.setDigester(digester);
  165. log = digester.getLogger();
  166. anyChildRule.setDigester(digester);
  167. }
  168. /**
  169. * When set to true, any text within child elements will have leading
  170. * and trailing whitespace removed before assignment to the target
  171. * object. The default value for this attribute is true.
  172. */
  173. public void setTrimData(boolean trimData) {
  174. this.trimData = trimData;
  175. }
  176. /** See {@link #setTrimData}. */
  177. public boolean getTrimData() {
  178. return trimData;
  179. }
  180. /**
  181. * When set to true, any child element for which there is no
  182. * corresponding object property will cause an error to be reported.
  183. * The default value of this attribute is false (not allowed).
  184. */
  185. public void setAllowUnknownChildElements(boolean allowUnknownChildElements) {
  186. this.allowUnknownChildElements = allowUnknownChildElements;
  187. }
  188. /** See {@link #setAllowUnknownChildElements}. */
  189. public boolean getAllowUnknownChildElements() {
  190. return allowUnknownChildElements;
  191. }
  192. /**
  193. * Process the beginning of this element.
  194. *
  195. * @param namespace is the namespace this attribute is in, or null
  196. * @param name is the name of the current xml element
  197. * @param attributes is the attribute list of this element
  198. */
  199. public void begin(String namespace, String name, Attributes attributes)
  200. throws Exception {
  201. oldRules = digester.getRules();
  202. newRules.init(digester.getMatch()+"/", oldRules);
  203. digester.setRules(newRules);
  204. }
  205. /**
  206. * This is only invoked after all child elements have been processed,
  207. * so we can remove the custom Rules object that does the
  208. * child-element-matching.
  209. */
  210. public void body(String bodyText) throws Exception {
  211. digester.setRules(oldRules);
  212. }
  213. /**
  214. * <p>Add an additional element name to property name mapping.
  215. * This is intended to be used from the xml rules.
  216. */
  217. public void addAlias(String elementName, String propertyName) {
  218. if (propertyName == null) {
  219. elementNames.put(elementName, PROP_IGNORE);
  220. }
  221. else {
  222. elementNames.put(elementName, propertyName);
  223. }
  224. }
  225. /**
  226. * Render a printable version of this Rule.
  227. */
  228. public String toString() {
  229. return ("SetNestedPropertiesRule");
  230. }
  231. //----------------------------------------- local classes
  232. /** Private Rules implementation */
  233. private class AnyChildRules implements Rules {
  234. private String matchPrefix = null;
  235. private Rules decoratedRules = null;
  236. private ArrayList rules = new ArrayList(1);
  237. private AnyChildRule rule;
  238. public AnyChildRules(AnyChildRule rule) {
  239. this.rule = rule;
  240. rules.add(rule);
  241. }
  242. public Digester getDigester() { return null; }
  243. public void setDigester(Digester digester) {}
  244. public String getNamespaceURI() {return null;}
  245. public void setNamespaceURI(String namespaceURI) {}
  246. public void add(String pattern, Rule rule) {}
  247. public void clear() {}
  248. public List match(String matchPath) {
  249. return match(null,matchPath);
  250. }
  251. public List match(String namespaceURI, String matchPath) {
  252. List match = decoratedRules.match(namespaceURI, matchPath);
  253. if ((matchPath.startsWith(matchPrefix)) &&
  254. (matchPath.indexOf('/', matchPrefix.length()) == -1)) {
  255. // The current element is a direct child of the element
  256. // specified in the init method, so include it as the
  257. // first rule in the matches list. The way that
  258. // SetNestedPropertiesRule is used, it is in fact very
  259. // likely to be the only match, so we optimise that
  260. // solution by keeping a list with only the AnyChildRule
  261. // instance in it.
  262. if ((match == null || match.size()==0)) {
  263. return rules;
  264. }
  265. else {
  266. // it might not be safe to modify the returned list,
  267. // so clone it first.
  268. LinkedList newMatch = new LinkedList(match);
  269. //newMatch.addFirst(rule);
  270. newMatch.addLast(rule);
  271. return newMatch;
  272. }
  273. }
  274. else {
  275. return match;
  276. }
  277. }
  278. public List rules() {
  279. // This is not actually expected to be called.
  280. throw new RuntimeException(
  281. "AnyChildRules.rules not implemented.");
  282. }
  283. public void init(String prefix, Rules rules) {
  284. matchPrefix = prefix;
  285. decoratedRules = rules;
  286. }
  287. }
  288. private class AnyChildRule extends Rule {
  289. private String currChildNamespaceURI = null;
  290. private String currChildElementName = null;
  291. public void begin(String namespaceURI, String name,
  292. Attributes attributes) throws Exception {
  293. currChildNamespaceURI = namespaceURI;
  294. currChildElementName = name;
  295. }
  296. public void body(String value) throws Exception {
  297. boolean debug = log.isDebugEnabled();
  298. String propName = (String) elementNames.get(currChildElementName);
  299. if (propName == PROP_IGNORE) {
  300. // note: above deliberately tests for IDENTITY, not EQUALITY
  301. return;
  302. }
  303. if (propName == null) {
  304. propName = currChildElementName;
  305. }
  306. if (digester.log.isDebugEnabled()) {
  307. digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
  308. "} Setting property '" + propName + "' to '" +
  309. value + "'");
  310. }
  311. // Populate the corresponding properties of the top object
  312. Object top = digester.peek();
  313. if (digester.log.isDebugEnabled()) {
  314. if (top != null) {
  315. digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
  316. "} Set " + top.getClass().getName() +
  317. " properties");
  318. } else {
  319. digester.log.debug("[SetPropertiesRule]{" + digester.match +
  320. "} Set NULL properties");
  321. }
  322. }
  323. if (trimData) {
  324. value = value.trim();
  325. }
  326. if (!allowUnknownChildElements) {
  327. // Force an exception if the property does not exist
  328. // (BeanUtils.setProperty() silently returns in this case)
  329. if (top instanceof DynaBean) {
  330. DynaProperty desc =
  331. ((DynaBean) top).getDynaClass().getDynaProperty(propName);
  332. if (desc == null) {
  333. throw new NoSuchMethodException
  334. ("Bean has no property named " + propName);
  335. }
  336. } else /* this is a standard JavaBean */ {
  337. PropertyDescriptor desc =
  338. PropertyUtils.getPropertyDescriptor(top, propName);
  339. if (desc == null) {
  340. throw new NoSuchMethodException
  341. ("Bean has no property named " + propName);
  342. }
  343. }
  344. }
  345. BeanUtils.setProperty(top, propName, value);
  346. }
  347. public void end(String namespace, String name) throws Exception {
  348. currChildElementName = null;
  349. }
  350. }
  351. }