- /* $Id: SetNestedPropertiesRule.java,v 1.8 2004/05/10 06:52:50 skitching Exp $
- *
- * Copyright 2003-2004 The Apache Software Foundation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
- package org.apache.commons.digester;
-
-
- import java.util.List;
- import java.util.LinkedList;
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.ListIterator;
- import java.util.HashMap;
- import java.beans.PropertyDescriptor;
-
- import org.apache.commons.beanutils.BeanUtils;
- import org.apache.commons.beanutils.DynaBean;
- import org.apache.commons.beanutils.DynaProperty;
- import org.apache.commons.beanutils.PropertyUtils;
-
- import org.xml.sax.Attributes;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
-
-
- /**
- * <p>Rule implementation that sets properties on the object at the top of the
- * stack, based on child elements with names matching properties on that
- * object.</p>
- *
- * <p>Example input that can be processed by this rule:</p>
- * <pre>
- * [widget]
- * [height]7[/height]
- * [width]8[/width]
- * [label]Hello, world[/label]
- * [/widget]
- * </pre>
- *
- * <p>This rule supports custom mapping of attribute names to property names.
- * The default mapping for particular attributes can be overridden by using
- * {@link #SetNestedPropertiesRule(String[] elementNames,
- * String[] propertyNames)}.
- * This allows child elements to be mapped to properties with different names.
- * Certain elements can also be marked to be ignored.</p>
- *
- * <p>A very similar effect can be achieved using a combination of the
- * <code>BeanPropertySetterRule</code> and the <code>ExtendedBaseRules</code>
- * rules manager; this <code>Rule</code>, however, works fine with the default
- * <code>RulesBase</code> rules manager.</p>
- *
- * <p><b>Implementation Notes</b></p>
- *
- * <p>This class works by creating its own simple Rules implementation. When
- * begin is invoked on this rule, the digester's current rules object is
- * replaced by a custom one. When end is invoked for this rule, the original
- * rules object is restored. The digester rules objects therefore behave in
- * a stack-like manner.</p>
- *
- * <p>For each child element encountered, the custom Rules implementation
- * ensures that a special AnyChildRule instance is included in the matches
- * returned to the digester, and it is this rule instance that is responsible
- * for setting the appropriate property on the target object (if such a property
- * exists). The effect is therefore like a "trailing wildcard pattern". The
- * custom Rules implementation also returns the matches provided by the
- * underlying Rules implementation for the same pattern, so other rules
- * are not "disabled" during processing of a SetNestedPropertiesRule.</p>
- *
- * @since 1.6
- */
-
- public class SetNestedPropertiesRule extends Rule {
-
- /**
- * Dummy object that can be placed in collections to indicate an
- * ignored property when null cannot be used for that purpose.
- */
- private static final String PROP_IGNORE = "ignore-me";
-
- private Log log = null;
-
- private AnyChildRule anyChildRule = new AnyChildRule();
- private AnyChildRules newRules = new AnyChildRules(anyChildRule);
- private Rules oldRules = null;
-
- private boolean trimData = true;
- private boolean allowUnknownChildElements = false;
-
- private HashMap elementNames = new HashMap();
-
- // ----------------------------------------------------------- Constructors
-
- /**
- * Base constructor.
- */
- public SetNestedPropertiesRule() {
- // nothing to set up
- }
-
- /**
- * <p>Convenience constructor overrides the mapping for just one property.</p>
- *
- * <p>For details about how this works, see
- * {@link #SetNestedPropertiesRule(String[] elementNames,
- * String[] propertyNames)}.</p>
- *
- * @param elementName map the child element to match
- * @param propertyName to a property with this name
- */
- public SetNestedPropertiesRule(String elementName, String propertyName) {
- elementNames.put(elementName, propertyName);
- }
-
- /**
- * <p>Constructor allows element->property mapping to be overriden.</p>
- *
- * <p>Two arrays are passed in.
- * One contains the element names and the other the property names.
- * The element name / property name pairs are match by position
- * In order words, the first string in the element name list matches
- * to the first string in the property name list and so on.</p>
- *
- * <p>If a property name is null or the element name has no matching
- * property name, then this indicates that the element should be ignored.</p>
- *
- * <h5>Example One</h5>
- * <p> The following constructs a rule that maps the <code>alt-city</code>
- * element to the <code>city</code> property and the <code>alt-state</code>
- * to the <code>state</code> property.
- * All other child elements are mapped as usual using exact name matching.
- * <code><pre>
- * SetNestedPropertiesRule(
- * new String[] {"alt-city", "alt-state"},
- * new String[] {"city", "state"});
- * </pre></code>
- *
- * <h5>Example Two</h5>
- * <p> The following constructs a rule that maps the <code>class</code>
- * element to the <code>className</code> property.
- * The element <code>ignore-me</code> is not mapped.
- * All other elements are mapped as usual using exact name matching.
- * <code><pre>
- * SetPropertiesRule(
- * new String[] {"class", "ignore-me"},
- * new String[] {"className"});
- * </pre></code>
- *
- * @param elementNames names of elements to map
- * @param propertyNames names of properties mapped to
- */
- public SetNestedPropertiesRule(String[] elementNames, String[] propertyNames) {
- for (int i=0, size=elementNames.length; i<size; i++) {
- String propName = null;
- if (i < propertyNames.length) {
- propName = propertyNames[i];
- }
-
- if (propName == null) {
- this.elementNames.put(elementNames[i], PROP_IGNORE);
- }
- else {
- this.elementNames.put(elementNames[i], propName);
- }
- }
- }
-
- // --------------------------------------------------------- Public Methods
-
- /** Invoked when rule is added to digester. */
- public void setDigester(Digester digester) {
- super.setDigester(digester);
- log = digester.getLogger();
- anyChildRule.setDigester(digester);
- }
-
- /**
- * When set to true, any text within child elements will have leading
- * and trailing whitespace removed before assignment to the target
- * object. The default value for this attribute is true.
- */
- public void setTrimData(boolean trimData) {
- this.trimData = trimData;
- }
-
- /** See {@link #setTrimData}. */
- public boolean getTrimData() {
- return trimData;
- }
-
- /**
- * When set to true, any child element for which there is no
- * corresponding object property will cause an error to be reported.
- * The default value of this attribute is false (not allowed).
- */
- public void setAllowUnknownChildElements(boolean allowUnknownChildElements) {
- this.allowUnknownChildElements = allowUnknownChildElements;
- }
-
- /** See {@link #setAllowUnknownChildElements}. */
- public boolean getAllowUnknownChildElements() {
- return allowUnknownChildElements;
- }
-
- /**
- * Process the beginning of this element.
- *
- * @param namespace is the namespace this attribute is in, or null
- * @param name is the name of the current xml element
- * @param attributes is the attribute list of this element
- */
- public void begin(String namespace, String name, Attributes attributes)
- throws Exception {
- oldRules = digester.getRules();
- newRules.init(digester.getMatch()+"/", oldRules);
- digester.setRules(newRules);
- }
-
- /**
- * This is only invoked after all child elements have been processed,
- * so we can remove the custom Rules object that does the
- * child-element-matching.
- */
- public void body(String bodyText) throws Exception {
- digester.setRules(oldRules);
- }
-
- /**
- * <p>Add an additional element name to property name mapping.
- * This is intended to be used from the xml rules.
- */
- public void addAlias(String elementName, String propertyName) {
- if (propertyName == null) {
- elementNames.put(elementName, PROP_IGNORE);
- }
- else {
- elementNames.put(elementName, propertyName);
- }
- }
-
- /**
- * Render a printable version of this Rule.
- */
- public String toString() {
-
- return ("SetNestedPropertiesRule");
- }
-
- //----------------------------------------- local classes
-
- /** Private Rules implementation */
- private class AnyChildRules implements Rules {
- private String matchPrefix = null;
- private Rules decoratedRules = null;
-
- private ArrayList rules = new ArrayList(1);
- private AnyChildRule rule;
-
- public AnyChildRules(AnyChildRule rule) {
- this.rule = rule;
- rules.add(rule);
- }
-
- public Digester getDigester() { return null; }
- public void setDigester(Digester digester) {}
- public String getNamespaceURI() {return null;}
- public void setNamespaceURI(String namespaceURI) {}
- public void add(String pattern, Rule rule) {}
- public void clear() {}
-
- public List match(String matchPath) {
- return match(null,matchPath);
- }
-
- public List match(String namespaceURI, String matchPath) {
- List match = decoratedRules.match(namespaceURI, matchPath);
-
- if ((matchPath.startsWith(matchPrefix)) &&
- (matchPath.indexOf('/', matchPrefix.length()) == -1)) {
-
- // The current element is a direct child of the element
- // specified in the init method, so include it as the
- // first rule in the matches list. The way that
- // SetNestedPropertiesRule is used, it is in fact very
- // likely to be the only match, so we optimise that
- // solution by keeping a list with only the AnyChildRule
- // instance in it.
-
- if ((match == null || match.size()==0)) {
- return rules;
- }
- else {
- // it might not be safe to modify the returned list,
- // so clone it first.
- LinkedList newMatch = new LinkedList(match);
- //newMatch.addFirst(rule);
- newMatch.addLast(rule);
- return newMatch;
- }
- }
- else {
- return match;
- }
- }
-
- public List rules() {
- // This is not actually expected to be called.
- throw new RuntimeException(
- "AnyChildRules.rules not implemented.");
- }
-
- public void init(String prefix, Rules rules) {
- matchPrefix = prefix;
- decoratedRules = rules;
- }
- }
-
- private class AnyChildRule extends Rule {
- private String currChildNamespaceURI = null;
- private String currChildElementName = null;
-
- public void begin(String namespaceURI, String name,
- Attributes attributes) throws Exception {
-
- currChildNamespaceURI = namespaceURI;
- currChildElementName = name;
- }
-
- public void body(String value) throws Exception {
- boolean debug = log.isDebugEnabled();
-
- String propName = (String) elementNames.get(currChildElementName);
- if (propName == PROP_IGNORE) {
- // note: above deliberately tests for IDENTITY, not EQUALITY
- return;
- }
- if (propName == null) {
- propName = currChildElementName;
- }
-
- if (digester.log.isDebugEnabled()) {
- digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
- "} Setting property '" + propName + "' to '" +
- value + "'");
- }
-
- // Populate the corresponding properties of the top object
- Object top = digester.peek();
- if (digester.log.isDebugEnabled()) {
- if (top != null) {
- digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
- "} Set " + top.getClass().getName() +
- " properties");
- } else {
- digester.log.debug("[SetPropertiesRule]{" + digester.match +
- "} Set NULL properties");
- }
- }
-
- if (trimData) {
- value = value.trim();
- }
-
- if (!allowUnknownChildElements) {
- // Force an exception if the property does not exist
- // (BeanUtils.setProperty() silently returns in this case)
- if (top instanceof DynaBean) {
- DynaProperty desc =
- ((DynaBean) top).getDynaClass().getDynaProperty(propName);
- if (desc == null) {
- throw new NoSuchMethodException
- ("Bean has no property named " + propName);
- }
- } else /* this is a standard JavaBean */ {
- PropertyDescriptor desc =
- PropertyUtils.getPropertyDescriptor(top, propName);
- if (desc == null) {
- throw new NoSuchMethodException
- ("Bean has no property named " + propName);
- }
- }
- }
-
- BeanUtils.setProperty(top, propName, value);
- }
-
- public void end(String namespace, String name) throws Exception {
- currChildElementName = null;
- }
- }
- }