1 package org.apache.commons.digester3; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import static java.lang.String.format; 23 import static org.apache.commons.beanutils.BeanUtils.populate; 24 import static org.apache.commons.beanutils.PropertyUtils.isWriteable; 25 26 import java.util.HashMap; 27 import java.util.Map; 28 29 import org.xml.sax.Attributes; 30 31 /** 32 * <p> 33 * Rule implementation that sets properties on the object at the top of the stack, based on attributes with 34 * corresponding names. 35 * </p> 36 * <p> 37 * This rule supports custom mapping of attribute names to property names. The default mapping for particular attributes 38 * can be overridden by using {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. This allows 39 * attributes to be mapped to properties with different names. Certain attributes can also be marked to be ignored. 40 * </p> 41 */ 42 public class SetPropertiesRule 43 extends Rule 44 { 45 46 // ----------------------------------------------------------- Constructors 47 48 /** 49 * Base constructor. 50 */ 51 public SetPropertiesRule() 52 { 53 // nothing to set up 54 } 55 56 /** 57 * <p> 58 * Convenience constructor overrides the mapping for just one property. 59 * </p> 60 * <p> 61 * For details about how this works, see {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)} 62 * . 63 * </p> 64 * 65 * @param attributeName map this attribute 66 * @param propertyName to a property with this name 67 */ 68 public SetPropertiesRule( String attributeName, String propertyName ) 69 { 70 aliases.put( attributeName, propertyName ); 71 } 72 73 /** 74 * <p> 75 * Constructor allows attribute->property mapping to be overriden. 76 * </p> 77 * <p> 78 * Two arrays are passed in. One contains the attribute names and the other the property names. The attribute name / 79 * property name pairs are match by position In order words, the first string in the attribute name list matches to 80 * the first string in the property name list and so on. 81 * </p> 82 * <p> 83 * If a property name is null or the attribute name has no matching property name, then this indicates that the 84 * attibute should be ignored. 85 * </p> 86 * <h5>Example One</h5> 87 * <p> 88 * The following constructs a rule that maps the <code>alt-city</code> attribute to the <code>city</code> property 89 * and the <code>alt-state</code> to the <code>state</code> property. All other attributes are mapped as usual using 90 * exact name matching. <code><pre> 91 * SetPropertiesRule( 92 * new String[] {"alt-city", "alt-state"}, 93 * new String[] {"city", "state"}); 94 * </pre></code> 95 * <h5>Example Two</h5> 96 * <p> 97 * The following constructs a rule that maps the <code>class</code> attribute to the <code>className</code> 98 * property. The attribute <code>ignore-me</code> is not mapped. All other attributes are mapped as usual using 99 * exact name matching. <code><pre> 100 * SetPropertiesRule( 101 * new String[] {"class", "ignore-me"}, 102 * new String[] {"className"}); 103 * </pre></code> 104 * 105 * @param attributeNames names of attributes to map 106 * @param propertyNames names of properties mapped to 107 */ 108 public SetPropertiesRule( String[] attributeNames, String[] propertyNames ) 109 { 110 for ( int i = 0, size = attributeNames.length; i < size; i++ ) 111 { 112 String propName = null; 113 if ( i < propertyNames.length ) 114 { 115 propName = propertyNames[i]; 116 } 117 118 aliases.put( attributeNames[i], propName ); 119 } 120 } 121 122 /** 123 * Constructor allows attribute->property mapping to be overriden. 124 * 125 * @param aliases attribute->property mapping 126 * @since 3.0 127 */ 128 public SetPropertiesRule( Map<String, String> aliases ) 129 { 130 if ( aliases != null && !aliases.isEmpty() ) 131 { 132 this.aliases.putAll( aliases ); 133 } 134 } 135 136 // ----------------------------------------------------- Instance Variables 137 138 private final Map<String, String> aliases = new HashMap<String, String>(); 139 140 /** 141 * Used to determine whether the parsing should fail if an property specified in the XML is missing from the bean. 142 * Default is true for backward compatibility. 143 */ 144 private boolean ignoreMissingProperty = true; 145 146 // --------------------------------------------------------- Public Methods 147 148 /** 149 * {@inheritDoc} 150 */ 151 @Override 152 public void begin( String namespace, String name, Attributes attributes ) 153 throws Exception 154 { 155 // Build a set of attribute names and corresponding values 156 Map<String, String> values = new HashMap<String, String>(); 157 158 for ( int i = 0; i < attributes.getLength(); i++ ) 159 { 160 String attributeName = attributes.getLocalName( i ); 161 if ( "".equals( attributeName ) ) 162 { 163 attributeName = attributes.getQName( i ); 164 } 165 String value = attributes.getValue( i ); 166 167 // alias lookup has complexity O(1) 168 if ( aliases.containsKey( attributeName ) ) 169 { 170 attributeName = aliases.get( attributeName ); 171 } 172 173 if ( getDigester().getLogger().isDebugEnabled() ) 174 { 175 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Setting property '%s' to '%s'", 176 getDigester().getMatch(), 177 attributeName, 178 attributeName ) ); 179 } 180 181 if ( ( !ignoreMissingProperty ) && ( attributeName != null ) ) 182 { 183 // The BeanUtils.populate method silently ignores items in 184 // the map (ie xml entities) which have no corresponding 185 // setter method, so here we check whether each xml attribute 186 // does have a corresponding property before calling the 187 // BeanUtils.populate method. 188 // 189 // Yes having the test and set as separate steps is ugly and 190 // inefficient. But BeanUtils.populate doesn't provide the 191 // functionality we need here, and changing the algorithm which 192 // determines the appropriate setter method to invoke is 193 // considered too risky. 194 // 195 // Using two different classes (PropertyUtils vs BeanUtils) to 196 // do the test and the set is also ugly; the codepaths 197 // are different which could potentially lead to trouble. 198 // However the BeanUtils/ProperyUtils code has been carefully 199 // compared and the PropertyUtils functionality does appear 200 // compatible so we'll accept the risk here. 201 202 Object top = getDigester().peek(); 203 boolean test = isWriteable( top, attributeName ); 204 if ( !test ) 205 { 206 throw new NoSuchMethodException( "Property " + attributeName + " can't be set" ); 207 } 208 } 209 210 if ( attributeName != null ) 211 { 212 values.put( attributeName, value ); 213 } 214 } 215 216 // Populate the corresponding properties of the top object 217 Object top = getDigester().peek(); 218 if ( getDigester().getLogger().isDebugEnabled() ) 219 { 220 if ( top != null ) 221 { 222 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Set '%s' properties", 223 getDigester().getMatch(), 224 top.getClass().getName() ) ); 225 } 226 else 227 { 228 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Set NULL properties", 229 getDigester().getMatch() ) ); 230 } 231 } 232 populate( top, values ); 233 } 234 235 /** 236 * Add an additional attribute name to property name mapping. This is intended to be used from the xml rules. 237 * 238 * @param attributeName the attribute name has to be mapped 239 * @param propertyName the target property name 240 */ 241 public void addAlias( String attributeName, String propertyName ) 242 { 243 aliases.put( attributeName, propertyName ); 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override 250 public String toString() 251 { 252 return format( "SetPropertiesRule[aliases=%s, ignoreMissingProperty=%s]", aliases, ignoreMissingProperty ); 253 } 254 255 /** 256 * <p> 257 * Are attributes found in the xml without matching properties to be ignored? 258 * </p> 259 * <p> 260 * If false, the parsing will interrupt with an <code>NoSuchMethodException</code> if a property specified in the 261 * XML is not found. The default is true. 262 * </p> 263 * 264 * @return true if skipping the unmatched attributes. 265 */ 266 public boolean isIgnoreMissingProperty() 267 { 268 return this.ignoreMissingProperty; 269 } 270 271 /** 272 * Sets whether attributes found in the xml without matching properties should be ignored. If set to false, the 273 * parsing will throw an <code>NoSuchMethodException</code> if an unmatched attribute is found. This allows to trap 274 * misspellings in the XML file. 275 * 276 * @param ignoreMissingProperty false to stop the parsing on unmatched attributes. 277 */ 278 public void setIgnoreMissingProperty( boolean ignoreMissingProperty ) 279 { 280 this.ignoreMissingProperty = ignoreMissingProperty; 281 } 282 283 }