001 package org.apache.commons.betwixt.digester; 002 003 /* 004 * Licensed to the Apache Software Foundation (ASF) under one or more 005 * contributor license agreements. See the NOTICE file distributed with 006 * this work for additional information regarding copyright ownership. 007 * The ASF licenses this file to You under the Apache License, Version 2.0 008 * (the "License"); you may not use this file except in compliance with 009 * the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019 import java.beans.PropertyDescriptor; 020 import java.lang.reflect.Method; 021 import java.lang.reflect.Modifier; 022 import java.util.Map; 023 024 import org.apache.commons.betwixt.ElementDescriptor; 025 import org.apache.commons.betwixt.XMLBeanInfo; 026 import org.apache.commons.betwixt.XMLUtils; 027 import org.apache.commons.betwixt.expression.ConstantExpression; 028 import org.apache.commons.betwixt.expression.Expression; 029 import org.apache.commons.betwixt.expression.IteratorExpression; 030 import org.apache.commons.betwixt.expression.MethodExpression; 031 import org.apache.commons.betwixt.expression.MethodUpdater; 032 import org.apache.commons.logging.Log; 033 import org.apache.commons.logging.LogFactory; 034 import org.xml.sax.Attributes; 035 import org.xml.sax.SAXException; 036 037 /** 038 * <p> 039 * <code>ElementRule</code> the digester Rule for parsing the <element> 040 * elements. 041 * </p> 042 * 043 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a> 044 */ 045 public class ElementRule extends MappedPropertyRule { 046 047 /** Logger */ 048 private static Log log = LogFactory.getLog(ElementRule.class); 049 050 /** 051 * Sets the log for this class 052 * 053 * @param newLog 054 * the new Log implementation for this class to use 055 * @since 0.5 056 */ 057 public static final void setLog(Log newLog) { 058 log = newLog; 059 } 060 061 /** Class for which the .bewixt file is being digested */ 062 private Class beanClass; 063 064 /** Base constructor */ 065 public ElementRule() { 066 } 067 068 // Rule interface 069 // ------------------------------------------------------------------------- 070 071 /** 072 * Process the beginning of this element. 073 * 074 * @param attributes 075 * The attribute list of this element 076 * @throws SAXException 077 * 1. If this tag's parent is not either an info or element tag. 078 * 2. If the name attribute is not valid XML element name. 3. If 079 * the name attribute is not present 4. If the class attribute 080 * is not a loadable (fully qualified) class name 081 */ 082 public void begin(String name, String namespace, Attributes attributes) 083 throws SAXException { 084 String nameAttributeValue = attributes.getValue("name"); 085 086 ElementDescriptor descriptor = new ElementDescriptor(); 087 descriptor.setLocalName(nameAttributeValue); 088 String uri = attributes.getValue("uri"); 089 String qName = nameAttributeValue; 090 if (uri != null && nameAttributeValue != null) { 091 descriptor.setURI(uri); 092 String prefix = getXMLIntrospector().getConfiguration() 093 .getPrefixMapper().getPrefix(uri); 094 qName = prefix + ":" + nameAttributeValue; 095 } 096 descriptor.setQualifiedName(qName); 097 098 String propertyName = attributes.getValue("property"); 099 descriptor.setPropertyName(propertyName); 100 101 String propertyType = attributes.getValue("type"); 102 103 if (log.isTraceEnabled()) { 104 log.trace("(BEGIN) name=" + nameAttributeValue + " uri=" + uri 105 + " property=" + propertyName + " type=" + propertyType); 106 } 107 108 // set mapping derivation 109 String mappingDerivation = attributes.getValue("mappingDerivation"); 110 if ("introspection".equals(mappingDerivation)) { 111 descriptor.setUseBindTimeTypeForMapping(false); 112 } else if ("bind".equals(mappingDerivation)) { 113 descriptor.setUseBindTimeTypeForMapping(true); 114 } 115 116 // set the property type using reflection 117 descriptor.setPropertyType(getPropertyType(propertyType, beanClass, 118 propertyName)); 119 120 boolean isCollective = getXMLIntrospector().getConfiguration() 121 .isLoopType(descriptor.getPropertyType()); 122 123 descriptor.setCollective(isCollective); 124 125 // check that the name attribute is present 126 if (!isCollective 127 && (nameAttributeValue == null || nameAttributeValue.trim() 128 .equals(""))) { 129 // allow polymorphic mappings but log note for user 130 log 131 .info("No name attribute has been specified. This element will be polymorphic."); 132 } 133 134 // check that name is well formed 135 if (nameAttributeValue != null 136 && !XMLUtils.isWellFormedXMLName(nameAttributeValue)) { 137 throw new SAXException("'" + nameAttributeValue 138 + "' would not be a well formed xml element name."); 139 } 140 141 String implementationClass = attributes.getValue("class"); 142 if (log.isTraceEnabled()) { 143 log.trace("'class' attribute=" + implementationClass); 144 } 145 if (implementationClass != null) { 146 try { 147 148 Class clazz = loadClass(implementationClass); 149 descriptor.setImplementationClass(clazz); 150 151 } catch (Exception e) { 152 if (log.isDebugEnabled()) { 153 log.debug( 154 "Cannot load class named: " + implementationClass, 155 e); 156 } 157 throw new SAXException("Cannot load class named: " 158 + implementationClass); 159 } 160 } 161 162 if (propertyName != null && propertyName.length() > 0) { 163 boolean forceAccessible = "true".equals(attributes 164 .getValue("forceAccessible")); 165 configureDescriptor(descriptor, attributes.getValue("updater"), 166 forceAccessible); 167 168 } else { 169 String value = attributes.getValue("value"); 170 if (value != null) { 171 descriptor.setTextExpression(new ConstantExpression(value)); 172 } 173 } 174 175 Object top = digester.peek(); 176 if (top instanceof XMLBeanInfo) { 177 XMLBeanInfo beanInfo = (XMLBeanInfo) top; 178 beanInfo.setElementDescriptor(descriptor); 179 beanClass = beanInfo.getBeanClass(); 180 descriptor.setPropertyType(beanClass); 181 182 } else if (top instanceof ElementDescriptor) { 183 ElementDescriptor parent = (ElementDescriptor) top; 184 parent.addElementDescriptor(descriptor); 185 186 } else { 187 throw new SAXException("Invalid use of <element>. It should " 188 + "be nested inside <info> or other <element> nodes"); 189 } 190 191 digester.push(descriptor); 192 } 193 194 /** 195 * Process the end of this element. 196 */ 197 public void end(String name, String namespace) { 198 ElementDescriptor descriptor = (ElementDescriptor)digester.pop(); 199 200 final Object peek = digester.peek(); 201 202 if(peek instanceof ElementDescriptor) { 203 ElementDescriptor parent = (ElementDescriptor)digester.peek(); 204 205 // check for element suppression 206 if( getXMLIntrospector().getConfiguration().getElementSuppressionStrategy().suppress(descriptor)) { 207 parent.removeElementDescriptor(descriptor); 208 } 209 } 210 } 211 212 // Implementation methods 213 // ------------------------------------------------------------------------- 214 215 /** 216 * Sets the Expression and Updater from a bean property name Uses the 217 * default updater (from the standard java bean property). 218 * 219 * @param elementDescriptor 220 * configure this <code>ElementDescriptor</code> 221 * @since 0.5 222 */ 223 protected void configureDescriptor(ElementDescriptor elementDescriptor) { 224 configureDescriptor(elementDescriptor, null); 225 } 226 227 /** 228 * Sets the Expression and Updater from a bean property name Allows a custom 229 * updater to be passed in. 230 * 231 * @param elementDescriptor 232 * configure this <code>ElementDescriptor</code> 233 * @param updateMethodName 234 * custom update method. If null, then use standard 235 * @since 0.5 236 * @deprecated now calls 237 * <code>#configureDescriptor(ElementDescriptor, String, boolean)</code> 238 * which allow accessibility to be forced. The subclassing API 239 * was not really considered carefully when this class was 240 * created. If anyone subclasses this method please contact the 241 * mailing list and suitable hooks will be placed into the code. 242 */ 243 protected void configureDescriptor(ElementDescriptor elementDescriptor, 244 String updateMethodName) { 245 configureDescriptor(elementDescriptor, null, false); 246 } 247 248 /** 249 * Sets the Expression and Updater from a bean property name Allows a custom 250 * updater to be passed in. 251 * 252 * @param elementDescriptor 253 * configure this <code>ElementDescriptor</code> 254 * @param updateMethodName 255 * custom update method. If null, then use standard 256 * @param forceAccessible 257 * if true and updateMethodName is not null, then non-public 258 * methods will be searched and made accessible 259 * (Method.setAccessible(true)) 260 */ 261 private void configureDescriptor(ElementDescriptor elementDescriptor, 262 String updateMethodName, boolean forceAccessible) { 263 Class beanClass = getBeanClass(); 264 if (beanClass != null) { 265 String name = elementDescriptor.getPropertyName(); 266 PropertyDescriptor descriptor = getPropertyDescriptor(beanClass, 267 name); 268 269 if (descriptor == null) { 270 if (log.isDebugEnabled()) { 271 log.debug("Cannot find property matching " + name); 272 } 273 } else { 274 configureProperty(elementDescriptor, descriptor, 275 updateMethodName, forceAccessible, beanClass); 276 277 getProcessedPropertyNameSet().add(name); 278 } 279 } 280 } 281 282 /** 283 * Configure an <code>ElementDescriptor</code> from a 284 * <code>PropertyDescriptor</code>. A custom update method may be set. 285 * 286 * @param elementDescriptor 287 * configure this <code>ElementDescriptor</code> 288 * @param propertyDescriptor 289 * configure from this <code>PropertyDescriptor</code> 290 * @param updateMethodName 291 * the name of the custom updater method to user. If null, then 292 * then 293 * @param forceAccessible 294 * if true and updateMethodName is not null, then non-public 295 * methods will be searched and made accessible 296 * (Method.setAccessible(true)) 297 * @param beanClass 298 * the <code>Class</code> from which the update method should 299 * be found. This may be null only when 300 * <code>updateMethodName</code> is also null. 301 */ 302 private void configureProperty(ElementDescriptor elementDescriptor, 303 PropertyDescriptor propertyDescriptor, String updateMethodName, 304 boolean forceAccessible, Class beanClass) { 305 306 Class type = propertyDescriptor.getPropertyType(); 307 Method readMethod = propertyDescriptor.getReadMethod(); 308 Method writeMethod = propertyDescriptor.getWriteMethod(); 309 310 elementDescriptor.setPropertyType(type); 311 312 // TODO: associate more bean information with the descriptor? 313 // nodeDescriptor.setDisplayName( propertyDescriptor.getDisplayName() ); 314 // nodeDescriptor.setShortDescription( 315 // propertyDescriptor.getShortDescription() ); 316 317 if (readMethod == null) { 318 log.trace("No read method"); 319 return; 320 } 321 322 if (log.isTraceEnabled()) { 323 log.trace("Read method=" + readMethod.getName()); 324 } 325 326 // choose response from property type 327 328 final MethodExpression methodExpression = new MethodExpression(readMethod); 329 if (getXMLIntrospector().isPrimitiveType(type)) { 330 elementDescriptor 331 .setTextExpression(methodExpression); 332 333 } else if (getXMLIntrospector().isLoopType(type)) { 334 log.trace("Loop type ??"); 335 336 // don't wrap this in an extra element as its specified in the 337 // XML descriptor so no need. 338 Expression expression = methodExpression; 339 340 // Support collectives with standard property setters (not adders) 341 // that use polymorphism to read objects. 342 boolean standardProperty = false; 343 if (updateMethodName != null && writeMethod != null && writeMethod.getName().equals(updateMethodName)) { 344 final Class[] parameters = writeMethod.getParameterTypes(); 345 if (parameters.length == 1) { 346 Class setterType = parameters[0]; 347 if (type.equals(setterType)) { 348 standardProperty = true; 349 } 350 } 351 } 352 if (!standardProperty) { 353 expression = new IteratorExpression(methodExpression); 354 } 355 elementDescriptor.setContextExpression(expression); 356 elementDescriptor.setHollow(true); 357 358 writeMethod = null; 359 360 if (Map.class.isAssignableFrom(type)) { 361 elementDescriptor.setLocalName("entry"); 362 // add elements for reading 363 ElementDescriptor keyDescriptor = new ElementDescriptor("key"); 364 keyDescriptor.setHollow(true); 365 elementDescriptor.addElementDescriptor(keyDescriptor); 366 367 ElementDescriptor valueDescriptor = new ElementDescriptor( 368 "value"); 369 valueDescriptor.setHollow(true); 370 elementDescriptor.addElementDescriptor(valueDescriptor); 371 } 372 373 } else { 374 log.trace("Standard property"); 375 elementDescriptor.setHollow(true); 376 elementDescriptor.setContextExpression(methodExpression); 377 } 378 379 // see if we have a custom method update name 380 if (updateMethodName == null) { 381 // set standard write method 382 if (writeMethod != null) { 383 elementDescriptor.setUpdater(new MethodUpdater(writeMethod)); 384 } 385 386 } else { 387 // see if we can find and set the custom method 388 if (log.isTraceEnabled()) { 389 log.trace("Finding custom method: "); 390 log.trace(" on:" + beanClass); 391 log.trace(" name:" + updateMethodName); 392 } 393 394 Method updateMethod; 395 boolean isMapTypeProperty = Map.class.isAssignableFrom(type); 396 if (forceAccessible) { 397 updateMethod = findAnyMethod(updateMethodName, beanClass, isMapTypeProperty); 398 } else { 399 updateMethod = findPublicMethod(updateMethodName, beanClass, isMapTypeProperty); 400 } 401 402 if (updateMethod == null) { 403 if (log.isInfoEnabled()) { 404 405 log.info("No method with name '" + updateMethodName 406 + "' found for update"); 407 } 408 } else { 409 // assign updater to elementDescriptor 410 if (Map.class.isAssignableFrom(type)) { 411 412 getXMLIntrospector().assignAdder(updateMethod, elementDescriptor); 413 414 } else { 415 elementDescriptor 416 .setUpdater(new MethodUpdater(updateMethod)); 417 Class singularType = updateMethod.getParameterTypes()[0]; 418 elementDescriptor.setSingularPropertyType(singularType); 419 if (singularType != null) 420 { 421 boolean isPrimitive = getXMLIntrospector().isPrimitiveType(singularType); 422 if (isPrimitive) 423 { 424 log.debug("Primitive collective: setting hollow to false"); 425 elementDescriptor.setHollow(false); 426 } 427 } 428 if (log.isTraceEnabled()) { 429 log.trace("Set custom updater on " + elementDescriptor); 430 } 431 } 432 } 433 } 434 } 435 436 private Method findPublicMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) { 437 Method[] methods = beanType.getMethods(); 438 Method updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty); 439 return updateMethod; 440 } 441 442 private Method searchMethodsForMatch(String updateMethodName, 443 Method[] methods, boolean isMapType) { 444 Method updateMethod = null; 445 for (int i = 0, size = methods.length; i < size; i++) { 446 Method method = methods[i]; 447 if (updateMethodName.equals(method.getName())) { 448 449 // updater should have one parameter unless type is Map 450 int numParams = 1; 451 if (isMapType) { 452 // updater for Map should have two parameters 453 numParams = 2; 454 } 455 456 // we have a matching name 457 // check paramters are correct 458 if (methods[i].getParameterTypes().length == numParams) { 459 // we'll use first match 460 updateMethod = methods[i]; 461 if (log.isTraceEnabled()) { 462 log.trace("Matched method:" + updateMethod); 463 } 464 // done since we're using the first match 465 break; 466 } 467 } 468 } 469 return updateMethod; 470 } 471 472 private Method findAnyMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) { 473 // TODO: suspect that this algorithm may run into difficulties 474 // on older JVMs (particularly with package privilage interfaces). 475 // This seems like too esoteric a use case to worry to much about now 476 Method updateMethod = null; 477 Class classToTry = beanType; 478 do { 479 Method[] methods = classToTry.getDeclaredMethods(); 480 updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty); 481 482 // try next superclass - Object will return null and end loop if no 483 // method is found 484 classToTry = classToTry.getSuperclass(); 485 } while (updateMethod == null && classToTry != null); 486 487 if (updateMethod != null) { 488 boolean isPublic = Modifier.isPublic(updateMethod.getModifiers()) 489 && Modifier.isPublic(beanType.getModifiers()); 490 if (!isPublic) { 491 updateMethod.setAccessible(true); 492 } 493 } 494 return updateMethod; 495 } 496 }