001package org.apache.commons.digester3; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import static java.lang.String.format; 023import static org.apache.commons.beanutils.BeanUtils.populate; 024import static org.apache.commons.beanutils.PropertyUtils.isWriteable; 025 026import java.util.HashMap; 027import java.util.Map; 028 029import org.xml.sax.Attributes; 030 031/** 032 * <p> 033 * Rule implementation that sets properties on the object at the top of the stack, based on attributes with 034 * corresponding names. 035 * </p> 036 * <p> 037 * This rule supports custom mapping of attribute names to property names. The default mapping for particular attributes 038 * can be overridden by using {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. This allows 039 * attributes to be mapped to properties with different names. Certain attributes can also be marked to be ignored. 040 * </p> 041 */ 042public class SetPropertiesRule 043 extends Rule 044{ 045 046 // ----------------------------------------------------------- Constructors 047 048 /** 049 * Base constructor. 050 */ 051 public SetPropertiesRule() 052 { 053 // nothing to set up 054 } 055 056 /** 057 * <p> 058 * Convenience constructor overrides the mapping for just one property. 059 * </p> 060 * <p> 061 * For details about how this works, see {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)} 062 * . 063 * </p> 064 * 065 * @param attributeName map this attribute 066 * @param propertyName to a property with this name 067 */ 068 public SetPropertiesRule( String attributeName, String propertyName ) 069 { 070 aliases.put( attributeName, propertyName ); 071 } 072 073 /** 074 * <p> 075 * Constructor allows attribute->property mapping to be overriden. 076 * </p> 077 * <p> 078 * Two arrays are passed in. One contains the attribute names and the other the property names. The attribute name / 079 * property name pairs are match by position In order words, the first string in the attribute name list matches to 080 * the first string in the property name list and so on. 081 * </p> 082 * <p> 083 * If a property name is null or the attribute name has no matching property name, then this indicates that the 084 * attibute should be ignored. 085 * </p> 086 * <h5>Example One</h5> 087 * <p> 088 * The following constructs a rule that maps the <code>alt-city</code> attribute to the <code>city</code> property 089 * and the <code>alt-state</code> to the <code>state</code> property. All other attributes are mapped as usual using 090 * exact name matching. <code><pre> 091 * SetPropertiesRule( 092 * new String[] {"alt-city", "alt-state"}, 093 * new String[] {"city", "state"}); 094 * </pre></code> 095 * <h5>Example Two</h5> 096 * <p> 097 * The following constructs a rule that maps the <code>class</code> attribute to the <code>className</code> 098 * property. The attribute <code>ignore-me</code> is not mapped. All other attributes are mapped as usual using 099 * 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}