001 /* $Id: SetPropertiesRule.java 992060 2010-09-02 19:09:47Z simonetripodi $ 002 * 003 * Licensed to the Apache Software Foundation (ASF) under one or more 004 * contributor license agreements. See the NOTICE file distributed with 005 * this work for additional information regarding copyright ownership. 006 * The ASF licenses this file to You under the Apache License, Version 2.0 007 * (the "License"); you may not use this file except in compliance with 008 * the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019 020 package org.apache.commons.digester; 021 022 023 import java.util.HashMap; 024 025 import org.apache.commons.beanutils.BeanUtils; 026 import org.apache.commons.beanutils.PropertyUtils; 027 import org.xml.sax.Attributes; 028 029 030 /** 031 * <p>Rule implementation that sets properties on the object at the top of the 032 * stack, based on attributes with corresponding names.</p> 033 * 034 * <p>This rule supports custom mapping of attribute names to property names. 035 * The default mapping for particular attributes can be overridden by using 036 * {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. 037 * This allows attributes to be mapped to properties with different names. 038 * Certain attributes can also be marked to be ignored.</p> 039 */ 040 041 public class SetPropertiesRule extends Rule { 042 043 044 // ----------------------------------------------------------- Constructors 045 046 047 /** 048 * Default constructor sets only the the associated Digester. 049 * 050 * @param digester The digester with which this rule is associated 051 * 052 * @deprecated The digester instance is now set in the {@link Digester#addRule} method. 053 * Use {@link #SetPropertiesRule()} instead. 054 */ 055 @Deprecated 056 public SetPropertiesRule(Digester digester) { 057 058 this(); 059 060 } 061 062 063 /** 064 * Base constructor. 065 */ 066 public SetPropertiesRule() { 067 068 // nothing to set up 069 070 } 071 072 /** 073 * <p>Convenience constructor overrides the mapping for just one property.</p> 074 * 075 * <p>For details about how this works, see 076 * {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}.</p> 077 * 078 * @param attributeName map this attribute 079 * @param propertyName to a property with this name 080 */ 081 public SetPropertiesRule(String attributeName, String propertyName) { 082 083 attributeNames = new String[1]; 084 attributeNames[0] = attributeName; 085 propertyNames = new String[1]; 086 propertyNames[0] = propertyName; 087 } 088 089 /** 090 * <p>Constructor allows attribute->property mapping to be overriden.</p> 091 * 092 * <p>Two arrays are passed in. 093 * One contains the attribute names and the other the property names. 094 * The attribute name / property name pairs are match by position 095 * In order words, the first string in the attribute name list matches 096 * to the first string in the property name list and so on.</p> 097 * 098 * <p>If a property name is null or the attribute name has no matching 099 * property name, then this indicates that the attibute should be ignored.</p> 100 * 101 * <h5>Example One</h5> 102 * <p> The following constructs a rule that maps the <code>alt-city</code> 103 * attribute to the <code>city</code> property and the <code>alt-state</code> 104 * to the <code>state</code> property. 105 * All other attributes are mapped as usual using exact name matching. 106 * <code><pre> 107 * SetPropertiesRule( 108 * new String[] {"alt-city", "alt-state"}, 109 * new String[] {"city", "state"}); 110 * </pre></code> 111 * 112 * <h5>Example Two</h5> 113 * <p> The following constructs a rule that maps the <code>class</code> 114 * attribute to the <code>className</code> property. 115 * The attribute <code>ignore-me</code> is not mapped. 116 * All other attributes are mapped as usual using exact name matching. 117 * <code><pre> 118 * SetPropertiesRule( 119 * new String[] {"class", "ignore-me"}, 120 * new String[] {"className"}); 121 * </pre></code> 122 * 123 * @param attributeNames names of attributes to map 124 * @param propertyNames names of properties mapped to 125 */ 126 public SetPropertiesRule(String[] attributeNames, String[] propertyNames) { 127 // create local copies 128 this.attributeNames = new String[attributeNames.length]; 129 for (int i=0, size=attributeNames.length; i<size; i++) { 130 this.attributeNames[i] = attributeNames[i]; 131 } 132 133 this.propertyNames = new String[propertyNames.length]; 134 for (int i=0, size=propertyNames.length; i<size; i++) { 135 this.propertyNames[i] = propertyNames[i]; 136 } 137 } 138 139 // ----------------------------------------------------- Instance Variables 140 141 /** 142 * Attribute names used to override natural attribute->property mapping 143 */ 144 private String [] attributeNames; 145 /** 146 * Property names used to override natural attribute->property mapping 147 */ 148 private String [] propertyNames; 149 150 /** 151 * Used to determine whether the parsing should fail if an property specified 152 * in the XML is missing from the bean. Default is true for backward compatibility. 153 */ 154 private boolean ignoreMissingProperty = true; 155 156 157 // --------------------------------------------------------- Public Methods 158 159 160 /** 161 * Process the beginning of this element. 162 * 163 * @param attributes The attribute list of this element 164 */ 165 @Override 166 public void begin(Attributes attributes) throws Exception { 167 168 // Build a set of attribute names and corresponding values 169 HashMap<String, String> values = new HashMap<String, String>(); 170 171 // set up variables for custom names mappings 172 int attNamesLength = 0; 173 if (attributeNames != null) { 174 attNamesLength = attributeNames.length; 175 } 176 int propNamesLength = 0; 177 if (propertyNames != null) { 178 propNamesLength = propertyNames.length; 179 } 180 181 182 for (int i = 0; i < attributes.getLength(); i++) { 183 String name = attributes.getLocalName(i); 184 if ("".equals(name)) { 185 name = attributes.getQName(i); 186 } 187 String value = attributes.getValue(i); 188 189 // we'll now check for custom mappings 190 for (int n = 0; n<attNamesLength; n++) { 191 if (name.equals(attributeNames[n])) { 192 if (n < propNamesLength) { 193 // set this to value from list 194 name = propertyNames[n]; 195 196 } else { 197 // set name to null 198 // we'll check for this later 199 name = null; 200 } 201 break; 202 } 203 } 204 205 if (digester.log.isDebugEnabled()) { 206 digester.log.debug("[SetPropertiesRule]{" + digester.match + 207 "} Setting property '" + name + "' to '" + 208 value + "'"); 209 } 210 211 if ((!ignoreMissingProperty) && (name != null)) { 212 // The BeanUtils.populate method silently ignores items in 213 // the map (ie xml entities) which have no corresponding 214 // setter method, so here we check whether each xml attribute 215 // does have a corresponding property before calling the 216 // BeanUtils.populate method. 217 // 218 // Yes having the test and set as separate steps is ugly and 219 // inefficient. But BeanUtils.populate doesn't provide the 220 // functionality we need here, and changing the algorithm which 221 // determines the appropriate setter method to invoke is 222 // considered too risky. 223 // 224 // Using two different classes (PropertyUtils vs BeanUtils) to 225 // do the test and the set is also ugly; the codepaths 226 // are different which could potentially lead to trouble. 227 // However the BeanUtils/ProperyUtils code has been carefully 228 // compared and the PropertyUtils functionality does appear 229 // compatible so we'll accept the risk here. 230 231 Object top = digester.peek(); 232 boolean test = PropertyUtils.isWriteable(top, name); 233 if (!test) 234 throw new NoSuchMethodException("Property " + name + " can't be set"); 235 } 236 237 if (name != null) { 238 values.put(name, value); 239 } 240 } 241 242 // Populate the corresponding properties of the top object 243 Object top = digester.peek(); 244 if (digester.log.isDebugEnabled()) { 245 if (top != null) { 246 digester.log.debug("[SetPropertiesRule]{" + digester.match + 247 "} Set " + top.getClass().getName() + 248 " properties"); 249 } else { 250 digester.log.debug("[SetPropertiesRule]{" + digester.match + 251 "} Set NULL properties"); 252 } 253 } 254 BeanUtils.populate(top, values); 255 256 257 } 258 259 260 /** 261 * <p>Add an additional attribute name to property name mapping. 262 * This is intended to be used from the xml rules. 263 */ 264 public void addAlias(String attributeName, String propertyName) { 265 266 // this is a bit tricky. 267 // we'll need to resize the array. 268 // probably should be synchronized but digester's not thread safe anyway 269 if (attributeNames == null) { 270 271 attributeNames = new String[1]; 272 attributeNames[0] = attributeName; 273 propertyNames = new String[1]; 274 propertyNames[0] = propertyName; 275 276 } else { 277 int length = attributeNames.length; 278 String [] tempAttributes = new String[length + 1]; 279 for (int i=0; i<length; i++) { 280 tempAttributes[i] = attributeNames[i]; 281 } 282 tempAttributes[length] = attributeName; 283 284 String [] tempProperties = new String[length + 1]; 285 for (int i=0; i<length && i< propertyNames.length; i++) { 286 tempProperties[i] = propertyNames[i]; 287 } 288 tempProperties[length] = propertyName; 289 290 propertyNames = tempProperties; 291 attributeNames = tempAttributes; 292 } 293 } 294 295 296 /** 297 * Render a printable version of this Rule. 298 */ 299 @Override 300 public String toString() { 301 302 StringBuffer sb = new StringBuffer("SetPropertiesRule["); 303 sb.append("]"); 304 return (sb.toString()); 305 306 } 307 308 /** 309 * <p>Are attributes found in the xml without matching properties to be ignored? 310 * </p><p> 311 * If false, the parsing will interrupt with an <code>NoSuchMethodException</code> 312 * if a property specified in the XML is not found. The default is true. 313 * </p> 314 * @return true if skipping the unmatched attributes. 315 */ 316 public boolean isIgnoreMissingProperty() { 317 318 return this.ignoreMissingProperty; 319 } 320 321 /** 322 * Sets whether attributes found in the xml without matching properties 323 * should be ignored. 324 * If set to false, the parsing will throw an <code>NoSuchMethodException</code> 325 * if an unmatched 326 * attribute is found. This allows to trap misspellings in the XML file. 327 * @param ignoreMissingProperty false to stop the parsing on unmatched attributes. 328 */ 329 public void setIgnoreMissingProperty(boolean ignoreMissingProperty) { 330 331 this.ignoreMissingProperty = ignoreMissingProperty; 332 } 333 334 335 }