| /* $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; |
| } |
| } |
| } |