blob: 0337768ce26fbcce59fb30b53332ef55694abee6 [file] [log] [blame]
/* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.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;
/**
* <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>For each child element of [widget], a corresponding setter method is
* located on the object on the top of the digester stack, the body text of
* the child element is converted to the type specified for the (sole)
* parameter to the setter method, then the setter method is invoked.</p>
*
* <p>This rule supports custom mapping of xml element names to property names.
* The default mapping for particular elements 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>Note that this rule is designed to be used to set only "primitive"
* bean properties, eg String, int, boolean. If some of the child xml elements
* match ObjectCreateRule rules (ie cause objects to be created) then you must
* use one of the more complex constructors to this rule to explicitly skip
* processing of that xml element, and define a SetNextRule (or equivalent) to
* handle assigning the child object to the appropriate property instead.</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>
*
* <p>TODO: Optimise this class. Currently, each time begin is called,
* new AnyChildRules and AnyChildRule objects are created. It should be
* possible to cache these in normal use (though watch out for when a rule
* instance is invoked re-entrantly!).</p>
*
* @since 1.6
*/
public class SetNestedPropertiesRule extends Rule {
private Log log = null;
private boolean trimData = true;
private boolean allowUnknownChildElements = false;
private HashMap<String, String> elementNames = new HashMap<String, String>();
// ----------------------------------------------------------- Constructors
/**
* Base constructor, which maps every child element into a bean property
* with the same name as the xml element.
*
* <p>It is an error if a child xml element exists but the target java
* bean has no such property (unless setAllowUnknownChildElements has been
* set to true).</p>
*/
public SetNestedPropertiesRule() {
// nothing to set up
}
/**
* <p>Convenience constructor which overrides the default mappings for
* just one property.</p>
*
* <p>For details about how this works, see
* {@link #SetNestedPropertiesRule(String[] elementNames,
* String[] propertyNames)}.</p>
*
* @param elementName is the child xml element to match
* @param propertyName is the java bean property to be assigned the value
* of the specified xml element. This may be null, in which case the
* specified xml element will be ignored.
*/
public SetNestedPropertiesRule(String elementName, String propertyName) {
elementNames.put(elementName, propertyName);
}
/**
* <p>Constructor which allows element->property mapping to be overridden.
* </p>
*
* <p>Two arrays are passed in. One contains xml element names and the
* other java bean property names. The element name / property name pairs
* are matched by position; in order words, the first string in the element
* name array corresponds to the first string in the property name array
* and so on.</p>
*
* <p>If a property name is null or the xml element name has no matching
* property name due to the arrays being of different lengths then this
* indicates that the xml 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>
* </p>
*
* <h5>Example Two</h5>
* <p> The following constructs a rule that maps the <code>class</code>
* xml element to the <code>className</code> property. The xml element
* <code>ignore-me</code> is not mapped, ie is ignored. All other elements
* are mapped as usual using exact name matching.
* <code><pre>
* SetPropertiesRule(
* new String[] {"class", "ignore-me"},
* new String[] {"className"});
* </pre></code>
* </p>
*
* @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];
}
this.elementNames.put(elementNames[i], propName);
}
}
// --------------------------------------------------------- Public Methods
/** Invoked when rule is added to digester. */
@Override
public void setDigester(Digester digester) {
super.setDigester(digester);
log = digester.getLogger();
}
/**
* 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;
}
/**
* Determines whether an error is reported when a nested element is
* encountered for which there is no corresponding property-setter
* method.
* <p>
* When set to false, any child element for which there is no
* corresponding object property will cause an error to be reported.
* <p>
* When set to true, any child element for which there is no
* corresponding object property will simply be ignored.
* <p>
* The default value of this attribute is false (unknown child elements
* are 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
*/
@Override
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
Rules oldRules = digester.getRules();
AnyChildRule anyChildRule = new AnyChildRule();
anyChildRule.setDigester(digester);
AnyChildRules newRules = new AnyChildRules(anyChildRule);
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.
*/
@Override
public void body(String bodyText) throws Exception {
AnyChildRules newRules = (AnyChildRules) digester.getRules();
digester.setRules(newRules.getOldRules());
}
/**
* Add an additional custom xml-element -> property mapping.
* <p>
* This is primarily intended to be used from the xml rules module
* (as it is not possible there to pass the necessary parameters to the
* constructor for this class). However it is valid to use this method
* directly if desired.
*/
public void addAlias(String elementName, String propertyName) {
elementNames.put(elementName, propertyName);
}
/**
* Render a printable version of this Rule.
*/
@Override
public String toString() {
StringBuffer sb = new StringBuffer("SetNestedPropertiesRule[");
sb.append("allowUnknownChildElements=");
sb.append(allowUnknownChildElements);
sb.append(", trimData=");
sb.append(trimData);
sb.append(", elementNames=");
sb.append(elementNames);
sb.append("]");
return sb.toString();
}
//----------------------------------------- local classes
/** Private Rules implementation */
private class AnyChildRules implements Rules {
private String matchPrefix = null;
private Rules decoratedRules = null;
private ArrayList<Rule> rules = new ArrayList<Rule>(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<Rule> match(String matchPath) {
return match(null,matchPath);
}
public List<Rule> match(String namespaceURI, String matchPath) {
List<Rule> 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 we want to ensure that
// the rule passed to this object's constructor is included
// in the returned list of matching rules.
if ((match == null || match.size()==0)) {
// The "real" rules class doesn't have any matches for
// the specified path, so we return a list containing
// just one rule: the one passed to this object's
// constructor.
return rules;
}
else {
// The "real" rules class has rules that match the current
// node, so we return this list *plus* the rule passed to
// this object's constructor.
//
// It might not be safe to modify the returned list,
// so clone it first.
LinkedList<Rule> newMatch = new LinkedList<Rule>(match);
newMatch.addLast(rule);
return newMatch;
}
}
else {
return match;
}
}
public List<Rule> rules() {
// This is not actually expected to be called during normal
// processing.
//
// There is only one known case where this is called; when a rule
// returned from AnyChildRules.match is invoked and throws a
// SAXException then method Digester.endDocument will be called
// without having "uninstalled" the AnyChildRules ionstance. That
// method attempts to invoke the "finish" method for every Rule
// instance - and thus needs to call rules() on its Rules object,
// which is this one. Actually, java 1.5 and 1.6beta2 have a
// bug in their xml implementation such that endDocument is not
// called after a SAXException, but other parsers (eg Aelfred)
// do call endDocument. Here, we therefore need to return the
// rules registered with the underlying Rules object.
log.debug("AnyChildRules.rules invoked.");
return decoratedRules.rules();
}
public void init(String prefix, Rules rules) {
matchPrefix = prefix;
decoratedRules = rules;
}
public Rules getOldRules() {
return decoratedRules;
}
}
private class AnyChildRule extends Rule {
private String currChildNamespaceURI = null;
private String currChildElementName = null;
@Override
public void begin(String namespaceURI, String name,
Attributes attributes) throws Exception {
currChildNamespaceURI = namespaceURI;
currChildElementName = name;
}
@Override
public void body(String value) throws Exception {
String propName = currChildElementName;
if (elementNames.containsKey(currChildElementName)) {
// overide propName
propName = elementNames.get(currChildElementName);
if (propName == null) {
// user wants us to ignore this element
return;
}
}
boolean debug = log.isDebugEnabled();
if (debug) {
log.debug("[SetNestedPropertiesRule]{" + digester.match +
"} Setting property '" + propName + "' to '" +
value + "'");
}
// Populate the corresponding properties of the top object
Object top = digester.peek();
if (debug) {
if (top != null) {
log.debug("[SetNestedPropertiesRule]{" + digester.match +
"} Set " + top.getClass().getName() +
" properties");
} else {
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);
}
}
}
try
{
BeanUtils.setProperty(top, propName, value);
}
catch(NullPointerException e) {
log.error("NullPointerException: "
+ "top=" + top + ",propName=" + propName + ",value=" + value + "!");
throw e;
}
}
@Override
public void end(String namespace, String name) throws Exception {
currChildElementName = null;
}
}
}