001 /* $Id: SetPropertiesRule.java 471661 2006-11-06 08:09:25Z skitching $ 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 public SetPropertiesRule(Digester digester) { 056 057 this(); 058 059 } 060 061 062 /** 063 * Base constructor. 064 */ 065 public SetPropertiesRule() { 066 067 // nothing to set up 068 069 } 070 071 /** 072 * <p>Convenience constructor overrides the mapping for just one property.</p> 073 * 074 * <p>For details about how this works, see 075 * {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}.</p> 076 * 077 * @param attributeName map this attribute 078 * @param propertyName to a property with this name 079 */ 080 public SetPropertiesRule(String attributeName, String propertyName) { 081 082 attributeNames = new String[1]; 083 attributeNames[0] = attributeName; 084 propertyNames = new String[1]; 085 propertyNames[0] = propertyName; 086 } 087 088 /** 089 * <p>Constructor allows attribute->property mapping to be overriden.</p> 090 * 091 * <p>Two arrays are passed in. 092 * One contains the attribute names and the other the property names. 093 * The attribute name / property name pairs are match by position 094 * In order words, the first string in the attribute name list matches 095 * to the first string in the property name list and so on.</p> 096 * 097 * <p>If a property name is null or the attribute name has no matching 098 * property name, then this indicates that the attibute should be ignored.</p> 099 * 100 * <h5>Example One</h5> 101 * <p> The following constructs a rule that maps the <code>alt-city</code> 102 * attribute to the <code>city</code> property and the <code>alt-state</code> 103 * to the <code>state</code> property. 104 * All other attributes are mapped as usual using exact name matching. 105 * <code><pre> 106 * SetPropertiesRule( 107 * new String[] {"alt-city", "alt-state"}, 108 * new String[] {"city", "state"}); 109 * </pre></code> 110 * 111 * <h5>Example Two</h5> 112 * <p> The following constructs a rule that maps the <code>class</code> 113 * attribute to the <code>className</code> property. 114 * The attribute <code>ignore-me</code> is not mapped. 115 * All other attributes are mapped as usual using exact name matching. 116 * <code><pre> 117 * SetPropertiesRule( 118 * new String[] {"class", "ignore-me"}, 119 * new String[] {"className"}); 120 * </pre></code> 121 * 122 * @param attributeNames names of attributes to map 123 * @param propertyNames names of properties mapped to 124 */ 125 public SetPropertiesRule(String[] attributeNames, String[] propertyNames) { 126 // create local copies 127 this.attributeNames = new String[attributeNames.length]; 128 for (int i=0, size=attributeNames.length; i<size; i++) { 129 this.attributeNames[i] = attributeNames[i]; 130 } 131 132 this.propertyNames = new String[propertyNames.length]; 133 for (int i=0, size=propertyNames.length; i<size; i++) { 134 this.propertyNames[i] = propertyNames[i]; 135 } 136 } 137 138 // ----------------------------------------------------- Instance Variables 139 140 /** 141 * Attribute names used to override natural attribute->property mapping 142 */ 143 private String [] attributeNames; 144 /** 145 * Property names used to override natural attribute->property mapping 146 */ 147 private String [] propertyNames; 148 149 /** 150 * Used to determine whether the parsing should fail if an property specified 151 * in the XML is missing from the bean. Default is true for backward compatibility. 152 */ 153 private boolean ignoreMissingProperty = true; 154 155 156 // --------------------------------------------------------- Public Methods 157 158 159 /** 160 * Process the beginning of this element. 161 * 162 * @param attributes The attribute list of this element 163 */ 164 public void begin(Attributes attributes) throws Exception { 165 166 // Build a set of attribute names and corresponding values 167 HashMap values = new HashMap(); 168 169 // set up variables for custom names mappings 170 int attNamesLength = 0; 171 if (attributeNames != null) { 172 attNamesLength = attributeNames.length; 173 } 174 int propNamesLength = 0; 175 if (propertyNames != null) { 176 propNamesLength = propertyNames.length; 177 } 178 179 180 for (int i = 0; i < attributes.getLength(); i++) { 181 String name = attributes.getLocalName(i); 182 if ("".equals(name)) { 183 name = attributes.getQName(i); 184 } 185 String value = attributes.getValue(i); 186 187 // we'll now check for custom mappings 188 for (int n = 0; n<attNamesLength; n++) { 189 if (name.equals(attributeNames[n])) { 190 if (n < propNamesLength) { 191 // set this to value from list 192 name = propertyNames[n]; 193 194 } else { 195 // set name to null 196 // we'll check for this later 197 name = null; 198 } 199 break; 200 } 201 } 202 203 if (digester.log.isDebugEnabled()) { 204 digester.log.debug("[SetPropertiesRule]{" + digester.match + 205 "} Setting property '" + name + "' to '" + 206 value + "'"); 207 } 208 209 if ((!ignoreMissingProperty) && (name != null)) { 210 // The BeanUtils.populate method silently ignores items in 211 // the map (ie xml entities) which have no corresponding 212 // setter method, so here we check whether each xml attribute 213 // does have a corresponding property before calling the 214 // BeanUtils.populate method. 215 // 216 // Yes having the test and set as separate steps is ugly and 217 // inefficient. But BeanUtils.populate doesn't provide the 218 // functionality we need here, and changing the algorithm which 219 // determines the appropriate setter method to invoke is 220 // considered too risky. 221 // 222 // Using two different classes (PropertyUtils vs BeanUtils) to 223 // do the test and the set is also ugly; the codepaths 224 // are different which could potentially lead to trouble. 225 // However the BeanUtils/ProperyUtils code has been carefully 226 // compared and the PropertyUtils functionality does appear 227 // compatible so we'll accept the risk here. 228 229 Object top = digester.peek(); 230 boolean test = PropertyUtils.isWriteable(top, name); 231 if (!test) 232 throw new NoSuchMethodException("Property " + name + " can't be set"); 233 } 234 235 if (name != null) { 236 values.put(name, value); 237 } 238 } 239 240 // Populate the corresponding properties of the top object 241 Object top = digester.peek(); 242 if (digester.log.isDebugEnabled()) { 243 if (top != null) { 244 digester.log.debug("[SetPropertiesRule]{" + digester.match + 245 "} Set " + top.getClass().getName() + 246 " properties"); 247 } else { 248 digester.log.debug("[SetPropertiesRule]{" + digester.match + 249 "} Set NULL properties"); 250 } 251 } 252 BeanUtils.populate(top, values); 253 254 255 } 256 257 258 /** 259 * <p>Add an additional attribute name to property name mapping. 260 * This is intended to be used from the xml rules. 261 */ 262 public void addAlias(String attributeName, String propertyName) { 263 264 // this is a bit tricky. 265 // we'll need to resize the array. 266 // probably should be synchronized but digester's not thread safe anyway 267 if (attributeNames == null) { 268 269 attributeNames = new String[1]; 270 attributeNames[0] = attributeName; 271 propertyNames = new String[1]; 272 propertyNames[0] = propertyName; 273 274 } else { 275 int length = attributeNames.length; 276 String [] tempAttributes = new String[length + 1]; 277 for (int i=0; i<length; i++) { 278 tempAttributes[i] = attributeNames[i]; 279 } 280 tempAttributes[length] = attributeName; 281 282 String [] tempProperties = new String[length + 1]; 283 for (int i=0; i<length && i< propertyNames.length; i++) { 284 tempProperties[i] = propertyNames[i]; 285 } 286 tempProperties[length] = propertyName; 287 288 propertyNames = tempProperties; 289 attributeNames = tempAttributes; 290 } 291 } 292 293 294 /** 295 * Render a printable version of this Rule. 296 */ 297 public String toString() { 298 299 StringBuffer sb = new StringBuffer("SetPropertiesRule["); 300 sb.append("]"); 301 return (sb.toString()); 302 303 } 304 305 /** 306 * <p>Are attributes found in the xml without matching properties to be ignored? 307 * </p><p> 308 * If false, the parsing will interrupt with an <code>NoSuchMethodException</code> 309 * if a property specified in the XML is not found. The default is true. 310 * </p> 311 * @return true if skipping the unmatched attributes. 312 */ 313 public boolean isIgnoreMissingProperty() { 314 315 return this.ignoreMissingProperty; 316 } 317 318 /** 319 * Sets whether attributes found in the xml without matching properties 320 * should be ignored. 321 * If set to false, the parsing will throw an <code>NoSuchMethodException</code> 322 * if an unmatched 323 * attribute is found. This allows to trap misspellings in the XML file. 324 * @param ignoreMissingProperty false to stop the parsing on unmatched attributes. 325 */ 326 public void setIgnoreMissingProperty(boolean ignoreMissingProperty) { 327 328 this.ignoreMissingProperty = ignoreMissingProperty; 329 } 330 331 332 }