All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.feilong.lib.digester3.SetNestedPropertiesRule Maven / Gradle / Ivy

Go to download

feilong is a suite of core and expanded libraries that include utility classes, http, excel,cvs, io classes, and much much more.

There is a newer version: 4.3.0
Show newest version
package com.feilong.lib.digester3;

import static java.lang.String.format;

/*
 * 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.
 */

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.DynaBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;

import com.feilong.lib.beanutils.BeanUtils;
import com.feilong.lib.beanutils.DynaProperty;
import com.feilong.lib.beanutils.PropertyUtils;

/**
 * 

* 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. *

*

* Example input that can be processed by this rule: *

* *
 *   [widget]
 *    [height]7[/height]
 *    [width]8[/width]
 *    [label]Hello, world[/label]
 *   [/widget]
 * 
*

* 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. *

*

* 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. *

*

* A very similar effect can be achieved using a combination of the BeanPropertySetterRule and the * ExtendedBaseRules rules manager; this Rule, however, works fine with the default * RulesBase rules manager. *

*

* 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. *

*

* Implementation Notes *

*

* 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. *

*

* 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. *

*

* 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!). *

* * @since 1.6 */ public class SetNestedPropertiesRule extends Rule{ /** The Constant log. */ private static final Logger LOGGER = LoggerFactory.getLogger(SetNestedPropertiesRule.class); //--------------------------------------------------------------- private boolean trimData = true; private boolean allowUnknownChildElements = false; private final HashMap elementNames = new HashMap<>(); // ----------------------------------------------------------- Constructors /** * Base constructor, which maps every child element into a bean property with the same name as the xml element. *

* It is an error if a child xml element exists but the target java bean has no such property (unless * {@link #setAllowUnknownChildElements(boolean)} has been set to true). *

*/ public SetNestedPropertiesRule(){ // nothing to set up } /** *

* Convenience constructor which overrides the default mappings for just one property. *

*

* For details about how this works, see * {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}. *

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

* Constructor which allows element->property mapping to be overridden. *

*

* 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. *

*

* 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. *

*
Example One
*

* The following constructs a rule that maps the alt-city element to the city property and * the alt-state to the state property. All other child elements are mapped as usual using * exact name matching.

     *      SetNestedPropertiesRule(
     *                new String[] {"alt-city", "alt-state"}, 
     *                new String[] {"city", "state"});
     * 
*

*
Example Two
*

* The following constructs a rule that maps the class xml element to the className * property. The xml element ignore-me is not mapped, ie is ignored. All other elements are mapped as * usual using exact name matching.

     *      SetPropertiesRule(
     *                new String[] {"class", "ignore-me"}, 
     *                new String[] {"className"});
     * 
*

* * @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); } } /** * Constructor which allows element->property mapping to be overridden. * * @param elementNames * names of elements->properties to map * @since 3.0 */ public SetNestedPropertiesRule(Map elementNames){ if (elementNames != null && !elementNames.isEmpty()){ this.elementNames.putAll(elementNames); } } // --------------------------------------------------------- Public Methods /** * {@inheritDoc} */ @Override public void setDigester(Digester digester){ super.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. * * @param trimData * flag to have leading and trailing whitespace removed */ public void setTrimData(boolean trimData){ this.trimData = trimData; } /** * Return the flag to have leading and trailing whitespace removed. * * @see #setTrimData(boolean) * @return flag to have leading and trailing whitespace removed */ 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. *

* When set to false, any child element for which there is no corresponding object property will cause an error to * be reported. *

* When set to true, any child element for which there is no corresponding object property will simply be ignored. *

* The default value of this attribute is false (unknown child elements are not allowed). * * @param allowUnknownChildElements * flag to ignore any child element for which there is no corresponding * object property */ public void setAllowUnknownChildElements(boolean allowUnknownChildElements){ this.allowUnknownChildElements = allowUnknownChildElements; } /** * Return the flag to ignore any child element for which there is no corresponding object property * * @return flag to ignore any child element for which there is no corresponding object property * @see #setAllowUnknownChildElements(boolean) */ public boolean getAllowUnknownChildElements(){ return allowUnknownChildElements; } /** * {@inheritDoc} */ @Override public void begin(String namespace,String name,Attributes attributes) throws Exception{ Rules oldRules = getDigester().getRules(); AnyChildRule anyChildRule = new AnyChildRule(); anyChildRule.setDigester(getDigester()); AnyChildRules newRules = new AnyChildRules(anyChildRule); newRules.init(getDigester().getMatch() + "/", oldRules); getDigester().setRules(newRules); } /** * {@inheritDoc} */ @Override public void body(String namespace,String name,String text) throws Exception{ AnyChildRules newRules = (AnyChildRules) getDigester().getRules(); getDigester().setRules(newRules.getOldRules()); } /** * Add an additional custom xml-element -> property mapping. *

* 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. * * @param elementName * the xml-element has to be mapped * @param propertyName * the property name target */ public void addAlias(String elementName,String propertyName){ elementNames.put(elementName, propertyName); } /** * {@inheritDoc} */ @Override public String toString(){ return format( "SetNestedPropertiesRule[allowUnknownChildElements=%s, trimData=%s, elementNames=%s]", allowUnknownChildElements, trimData, elementNames); } // ----------------------------------------- local classes /** Private Rules implementation */ private class AnyChildRules implements Rules{ private String matchPrefix = null; private Rules decoratedRules = null; private final ArrayList rules = new ArrayList<>(1); private final AnyChildRule rule; public AnyChildRules(AnyChildRule rule){ this.rule = rule; rules.add(rule); } @Override public Digester getDigester(){ return null; } @Override public void setDigester(Digester digester){ } @Override public String getNamespaceURI(){ return null; } @Override public void setNamespaceURI(String namespaceURI){ } @Override public void add(String pattern,Rule rule){ } @Override public void clear(){ } @Override public List match(String namespaceURI,String matchPath,String name,Attributes attributes){ List match = decoratedRules.match(namespaceURI, matchPath, name, attributes); 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; } // 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 newMatch = new LinkedList<>(match); newMatch.addLast(rule); return newMatch; } return match; } @Override public List 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. LOGGER.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 currChildElementName = null; @Override public void begin(String namespaceURI,String name,Attributes attributes) throws Exception{ currChildElementName = name; } @Override public void body(String namespace,String name,String text) 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 = LOGGER.isDebugEnabled(); if (debug){ LOGGER.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Setting property '" + propName + "' to '" + text + "'"); } // Populate the corresponding properties of the top object Object top = getDigester().peek(); if (debug){ if (top != null){ LOGGER.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Set " + top.getClass().getName() + " properties"); }else{ LOGGER.debug("[SetPropertiesRule]{" + getDigester().getMatch() + "} Set NULL properties"); } } if (trimData){ text = text.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, text); }catch (NullPointerException e){ LOGGER.error("NullPointerException: " + "top=" + top + ",propName=" + propName + ",value=" + text + "!"); throw e; } } @Override public void end(String namespace,String name) throws Exception{ currChildElementName = null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy