001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.betwixt.io; 018 019 import java.util.HashMap; 020 import java.util.List; 021 import java.util.Map; 022 023 import org.apache.commons.betwixt.AttributeDescriptor; 024 import org.apache.commons.betwixt.ElementDescriptor; 025 import org.apache.commons.betwixt.XMLBeanInfo; 026 import org.apache.commons.betwixt.XMLIntrospector; 027 import org.apache.commons.betwixt.digester.XMLIntrospectorHelper; 028 import org.apache.commons.betwixt.expression.Context; 029 import org.apache.commons.betwixt.expression.MethodUpdater; 030 import org.apache.commons.betwixt.expression.Updater; 031 import org.apache.commons.digester.Rule; 032 import org.apache.commons.digester.Rules; 033 import org.apache.commons.logging.Log; 034 import org.apache.commons.logging.LogFactory; 035 import org.xml.sax.Attributes; 036 037 /** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans 038 * from the betwixt XML metadata.</p> 039 * 040 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a> 041 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a> 042 * @deprecated 0.5 this Rule does not allowed good integration with other Rules - 043 * use {@link BeanRuleSet} instead. 044 */ 045 public class BeanCreateRule extends Rule { 046 047 /** Logger */ 048 private static Log log = LogFactory.getLog( BeanCreateRule.class ); 049 050 /** 051 * Set log to be used by <code>BeanCreateRule</code> instances 052 * @param aLog the <code>Log</code> implementation for this class to log to 053 */ 054 public static void setLog(Log aLog) { 055 log = aLog; 056 } 057 058 /** The descriptor of this element */ 059 private ElementDescriptor descriptor; 060 /** The Context used when evaluating Updaters */ 061 private Context context; 062 /** Have we added our child rules to the digester? */ 063 private boolean addedChildren; 064 /** In this begin-end loop did we actually create a new bean */ 065 private boolean createdBean; 066 /** The type of the bean to create */ 067 private Class beanClass; 068 /** The prefix added to digester rules */ 069 private String pathPrefix; 070 /** Use id's to match beans? */ 071 private boolean matchIDs = true; 072 /** allows an attribute to be specified to overload the types of beans used */ 073 private String classNameAttribute = "className"; 074 075 /** 076 * Convenience constructor which uses <code>ID's</code> for matching. 077 * 078 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 079 * @param beanClass the <code>Class</code> to be created 080 * @param pathPrefix the digester style path 081 */ 082 public BeanCreateRule( 083 ElementDescriptor descriptor, 084 Class beanClass, 085 String pathPrefix ) { 086 this( descriptor, beanClass, pathPrefix, true ); 087 } 088 089 /** 090 * Constructor taking a class. 091 * 092 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 093 * @param beanClass the <code>Class</code> to be created 094 * @param pathPrefix the digester style path 095 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching 096 */ 097 public BeanCreateRule( 098 ElementDescriptor descriptor, 099 Class beanClass, 100 String pathPrefix, 101 boolean matchIDs ) { 102 this( 103 descriptor, 104 beanClass, 105 new Context(), 106 pathPrefix, 107 matchIDs); 108 } 109 110 /** 111 * Convenience constructor which uses <code>ID's</code> for matching. 112 * 113 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 114 * @param beanClass the <code>Class</code> to be created 115 */ 116 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass ) { 117 this( descriptor, beanClass, true ); 118 } 119 120 /** 121 * Constructor uses standard qualified name. 122 * 123 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 124 * @param beanClass the <code>Class</code> to be created 125 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching 126 */ 127 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass, boolean matchIDs ) { 128 this( descriptor, beanClass, descriptor.getQualifiedName() + "/" , matchIDs ); 129 } 130 131 /** 132 * Convenience constructor which uses <code>ID's</code> for match. 133 * 134 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 135 * @param context the <code>Context</code> to be used to evaluate expressions 136 * @param pathPrefix the digester path prefix 137 */ 138 public BeanCreateRule( 139 ElementDescriptor descriptor, 140 Context context, 141 String pathPrefix ) { 142 this( descriptor, context, pathPrefix, true ); 143 } 144 145 /** 146 * Constructor taking a context. 147 * 148 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 149 * @param context the <code>Context</code> to be used to evaluate expressions 150 * @param pathPrefix the digester path prefix 151 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching 152 */ 153 public BeanCreateRule( 154 ElementDescriptor descriptor, 155 Context context, 156 String pathPrefix, 157 boolean matchIDs ) { 158 this( 159 descriptor, 160 descriptor.getSingularPropertyType(), 161 context, 162 pathPrefix, 163 matchIDs ); 164 } 165 166 /** 167 * Base constructor (used by other constructors). 168 * 169 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped 170 * @param beanClass the <code>Class</code> of the bean to be created 171 * @param context the <code>Context</code> to be used to evaluate expressions 172 * @param pathPrefix the digester path prefix 173 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching 174 */ 175 private BeanCreateRule( 176 ElementDescriptor descriptor, 177 Class beanClass, 178 Context context, 179 String pathPrefix, 180 boolean matchIDs ) { 181 this.descriptor = descriptor; 182 this.context = context; 183 this.beanClass = beanClass; 184 this.pathPrefix = pathPrefix; 185 this.matchIDs = matchIDs; 186 if (log.isTraceEnabled()) { 187 log.trace("Created bean create rule"); 188 log.trace("Descriptor=" + descriptor); 189 log.trace("Class=" + beanClass); 190 log.trace("Path prefix=" + pathPrefix); 191 } 192 } 193 194 195 196 // Rule interface 197 //------------------------------------------------------------------------- 198 199 /** 200 * Process the beginning of this element. 201 * 202 * @param attributes The attribute list of this element 203 */ 204 public void begin(Attributes attributes) { 205 log.debug( "Called with descriptor: " + descriptor 206 + " propertyType: " + descriptor.getPropertyType() ); 207 208 if (log.isTraceEnabled()) { 209 int attributesLength = attributes.getLength(); 210 if (attributesLength > 0) { 211 log.trace("Attributes:"); 212 } 213 for (int i=0, size=attributesLength; i<size; i++) { 214 log.trace("Local:" + attributes.getLocalName(i)); 215 log.trace("URI:" + attributes.getURI(i)); 216 log.trace("QName:" + attributes.getQName(i)); 217 } 218 } 219 220 221 222 // XXX: if a single rule instance gets reused and nesting occurs 223 // XXX: we should probably use a stack of booleans to test if we created a bean 224 // XXX: or let digester take nulls, which would be easier for us ;-) 225 createdBean = false; 226 227 Object instance = null; 228 if ( beanClass != null ) { 229 instance = createBean(attributes); 230 if ( instance != null ) { 231 createdBean = true; 232 233 context.setBean( instance ); 234 digester.push(instance); 235 236 237 // if we are a reference to a type we should lookup the original 238 // as this ElementDescriptor will be 'hollow' and have no child attributes/elements. 239 // XXX: this should probably be done by the NodeDescriptors... 240 ElementDescriptor typeDescriptor = getElementDescriptor( descriptor ); 241 //ElementDescriptor typeDescriptor = descriptor; 242 243 // iterate through all attributes 244 AttributeDescriptor[] attributeDescriptors 245 = typeDescriptor.getAttributeDescriptors(); 246 if ( attributeDescriptors != null ) { 247 for ( int i = 0, size = attributeDescriptors.length; i < size; i++ ) { 248 AttributeDescriptor attributeDescriptor = attributeDescriptors[i]; 249 250 // The following isn't really the right way to find the attribute 251 // but it's quite robust. 252 // The idea is that you try both namespace and local name first 253 // and if this returns null try the qName. 254 String value = attributes.getValue( 255 attributeDescriptor.getURI(), 256 attributeDescriptor.getLocalName() 257 ); 258 259 if (value == null) { 260 value = attributes.getValue(attributeDescriptor.getQualifiedName()); 261 } 262 263 if (log.isTraceEnabled()) { 264 log.trace("Attr URL:" + attributeDescriptor.getURI()); 265 log.trace("Attr LocalName:" + attributeDescriptor.getLocalName() ); 266 log.trace(value); 267 } 268 269 Updater updater = attributeDescriptor.getUpdater(); 270 log.trace(updater); 271 if ( updater != null && value != null ) { 272 updater.update( context, value ); 273 } 274 } 275 } 276 277 addChildRules(); 278 279 // add bean for ID matching 280 if ( matchIDs ) { 281 // XXX need to support custom ID attribute names 282 // XXX i have a feeling that the current mechanism might need to change 283 // XXX so i'm leaving this till later 284 String id = attributes.getValue( "id" ); 285 if ( id != null ) { 286 getBeansById().put( id, instance ); 287 } 288 } 289 } 290 } 291 } 292 293 /** 294 * Process the end of this element. 295 */ 296 public void end() { 297 if ( createdBean ) { 298 299 // force any setters of the parent bean to be called for this new bean instance 300 Updater updater = descriptor.getUpdater(); 301 Object instance = context.getBean(); 302 303 Object top = digester.pop(); 304 if (digester.getCount() == 0) { 305 context.setBean(null); 306 }else{ 307 context.setBean( digester.peek() ); 308 } 309 310 if ( updater != null ) { 311 if ( log.isDebugEnabled() ) { 312 log.debug( "Calling updater for: " + descriptor + " with: " 313 + instance + " on bean: " + context.getBean() ); 314 } 315 updater.update( context, instance ); 316 } else { 317 if ( log.isDebugEnabled() ) { 318 log.debug( "No updater for: " + descriptor + " with: " 319 + instance + " on bean: " + context.getBean() ); 320 } 321 } 322 } 323 } 324 325 /** 326 * Tidy up. 327 */ 328 public void finish() {} 329 330 331 // Properties 332 //------------------------------------------------------------------------- 333 334 335 /** 336 * The name of the attribute which can be specified in the XML to override the 337 * type of a bean used at a certain point in the schema. 338 * 339 * <p>The default value is 'className'.</p> 340 * 341 * @return The name of the attribute used to overload the class name of a bean 342 */ 343 public String getClassNameAttribute() { 344 return classNameAttribute; 345 } 346 347 /** 348 * Sets the name of the attribute which can be specified in 349 * the XML to override the type of a bean used at a certain 350 * point in the schema. 351 * 352 * <p>The default value is 'className'.</p> 353 * 354 * @param classNameAttribute The name of the attribute used to overload the class name of a bean 355 */ 356 public void setClassNameAttribute(String classNameAttribute) { 357 this.classNameAttribute = classNameAttribute; 358 } 359 360 // Implementation methods 361 //------------------------------------------------------------------------- 362 363 /** 364 * Factory method to create new bean instances 365 * 366 * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code> 367 * @return the created bean 368 */ 369 protected Object createBean(Attributes attributes) { 370 // 371 // See if we've got an IDREF 372 // 373 // XXX This should be customizable but i'm not really convinced by the existing system 374 // XXX maybe it's going to have to change so i'll use 'idref' for nows 375 // 376 if ( matchIDs ) { 377 String idref = attributes.getValue( "idref" ); 378 if ( idref != null ) { 379 // XXX need to check up about ordering 380 // XXX this is a very simple system that assumes that id occurs before idrefs 381 // XXX would need some thought about how to implement a fuller system 382 log.trace( "Found IDREF" ); 383 Object bean = getBeansById().get( idref ); 384 if ( bean != null ) { 385 if (log.isTraceEnabled()) { 386 log.trace( "Matched bean " + bean ); 387 } 388 return bean; 389 } 390 log.trace( "No match found" ); 391 } 392 } 393 394 Class theClass = beanClass; 395 try { 396 397 String className = attributes.getValue(classNameAttribute); 398 if (className != null) { 399 // load the class we should instantiate 400 theClass = getDigester().getClassLoader().loadClass(className); 401 } 402 if (log.isTraceEnabled()) { 403 log.trace( "Creating instance of " + theClass ); 404 } 405 return theClass.newInstance(); 406 407 } catch (Exception e) { 408 log.warn( "Could not create instance of type: " + theClass.getName() ); 409 return null; 410 } 411 } 412 413 /** Adds the rules to the digester for all child elements */ 414 protected void addChildRules() { 415 if ( ! addedChildren ) { 416 addedChildren = true; 417 418 addChildRules( pathPrefix, descriptor ); 419 } 420 } 421 422 /** 423 * Add child rules for given descriptor at given prefix 424 * 425 * @param prefix add child rules at this (digester) path prefix 426 * @param currentDescriptor add child rules for this descriptor 427 */ 428 protected void addChildRules(String prefix, ElementDescriptor currentDescriptor ) { 429 430 if (log.isTraceEnabled()) { 431 log.trace("Adding child rules for " + currentDescriptor + "@" + prefix); 432 } 433 434 // if we are a reference to a type we should lookup the original 435 // as this ElementDescriptor will be 'hollow' and have no child attributes/elements. 436 // XXX: this should probably be done by the NodeDescriptors... 437 ElementDescriptor typeDescriptor = getElementDescriptor( currentDescriptor ); 438 //ElementDescriptor typeDescriptor = descriptor; 439 440 441 ElementDescriptor[] childDescriptors = typeDescriptor.getElementDescriptors(); 442 if ( childDescriptors != null ) { 443 for ( int i = 0, size = childDescriptors.length; i < size; i++ ) { 444 final ElementDescriptor childDescriptor = childDescriptors[i]; 445 if (log.isTraceEnabled()) { 446 log.trace("Processing child " + childDescriptor); 447 } 448 449 String qualifiedName = childDescriptor.getQualifiedName(); 450 if ( qualifiedName == null ) { 451 log.trace( "Ignoring" ); 452 continue; 453 } 454 String path = prefix + qualifiedName; 455 // this code is for making sure that recursive elements 456 // can also be used.. 457 458 if ( qualifiedName.equals( currentDescriptor.getQualifiedName() ) 459 && currentDescriptor.getPropertyName() != null ) { 460 log.trace("Creating generic rule for recursive elements"); 461 int index = -1; 462 if (childDescriptor.isWrapCollectionsInElement()) { 463 index = prefix.indexOf(qualifiedName); 464 if (index == -1) { 465 // shouldn't happen.. 466 log.debug( "Oops - this shouldn't happen" ); 467 continue; 468 } 469 int removeSlash = prefix.endsWith("/")?1:0; 470 path = "*/" + prefix.substring(index, prefix.length()-removeSlash); 471 }else{ 472 // we have a element/element type of thing.. 473 ElementDescriptor[] desc = currentDescriptor.getElementDescriptors(); 474 if (desc.length == 1) { 475 path = "*/"+desc[0].getQualifiedName(); 476 } 477 } 478 Rule rule = new BeanCreateRule( childDescriptor, context, path, matchIDs); 479 addRule(path, rule); 480 continue; 481 } 482 if ( childDescriptor.getUpdater() != null ) { 483 if (log.isTraceEnabled()) { 484 log.trace("Element has updater " 485 + ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName()); 486 } 487 if ( childDescriptor.isPrimitiveType() ) { 488 addPrimitiveTypeRule(path, childDescriptor); 489 490 } else { 491 // add the first child to the path 492 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors(); 493 if ( grandChildren != null && grandChildren.length > 0 ) { 494 ElementDescriptor grandChild = grandChildren[0]; 495 String grandChildQName = grandChild.getQualifiedName(); 496 if ( grandChildQName != null && grandChildQName.length() > 0 ) { 497 if (childDescriptor.isWrapCollectionsInElement()) { 498 path += '/' + grandChildQName; 499 500 } else { 501 path = prefix + (prefix.endsWith("/")?"":"/") + grandChildQName; 502 } 503 } 504 } 505 506 // maybe we are adding a primitve type to a collection/array 507 Class beanClass = childDescriptor.getSingularPropertyType(); 508 if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) { 509 addPrimitiveTypeRule(path, childDescriptor); 510 511 } else { 512 Rule rule = new BeanCreateRule( 513 childDescriptor, 514 context, 515 path + '/', 516 matchIDs ); 517 addRule( path, rule ); 518 } 519 } 520 } else { 521 log.trace("Element does not have updater"); 522 } 523 524 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors(); 525 if ( grandChildren != null && grandChildren.length > 0 ) { 526 log.trace("Adding grand children"); 527 addChildRules( path + '/', childDescriptor ); 528 } 529 } 530 } 531 } 532 533 /** 534 * Get the associated bean reader. 535 * 536 * @return the <code>BeanReader</code digesting the xml 537 */ 538 protected BeanReader getBeanReader() { 539 // XXX this breaks the rule contact 540 // XXX maybe the reader should be passed in the constructor 541 return (BeanReader) getDigester(); 542 } 543 544 /** 545 * Allows the navigation from a reference to a property object to the descriptor defining what 546 * the property is. In other words, doing the join from a reference to a type to lookup its descriptor. 547 * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info. 548 * 549 * @param propertyDescriptor find descriptor for property object referenced by this descriptor 550 * @return descriptor for the singular property class type referenced. 551 */ 552 protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) { 553 Class beanClass = propertyDescriptor.getSingularPropertyType(); 554 if ( beanClass != null ) { 555 XMLIntrospector introspector = getBeanReader().getXMLIntrospector(); 556 try { 557 XMLBeanInfo xmlInfo = introspector.introspect( beanClass ); 558 return xmlInfo.getElementDescriptor(); 559 560 } catch (Exception e) { 561 log.warn( "Could not introspect class: " + beanClass, e ); 562 } 563 } 564 // could not find a better descriptor so use the one we've got 565 return propertyDescriptor; 566 } 567 568 /** 569 * Adds a new Digester rule to process the text as a primitive type 570 * 571 * @param path digester path where this rule will be attached 572 * @param childDescriptor update this <code>ElementDescriptor</code> with the body text 573 */ 574 protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) { 575 Rule rule = new Rule() { 576 public void body(String text) throws Exception { 577 childDescriptor.getUpdater().update( context, text ); 578 } 579 }; 580 addRule( path, rule ); 581 } 582 583 /** 584 * Safely add a rule with given path. 585 * 586 * @param path the digester path to add rule at 587 * @param rule the <code>Rule</code> to add 588 */ 589 protected void addRule(String path, Rule rule) { 590 Rules rules = digester.getRules(); 591 List matches = rules.match(null, path); 592 if ( matches.isEmpty() ) { 593 if ( log.isDebugEnabled() ) { 594 log.debug( "Adding digester rule for path: " + path + " rule: " + rule ); 595 } 596 digester.addRule( path, rule ); 597 598 } else { 599 if ( log.isDebugEnabled() ) { 600 log.debug( "Ignoring duplicate digester rule for path: " 601 + path + " rule: " + rule ); 602 log.debug( "New rule (not added): " + rule ); 603 log.debug( "Existing rule:" + matches.get(0) ); 604 } 605 } 606 } 607 608 /** 609 * Get the map used to index beans (previously read in) by id. 610 * This is stored in the evaluation context. 611 * 612 * @return map indexing beans created by id 613 */ 614 protected Map getBeansById() { 615 // 616 // we need a single index for beans read in by id 617 // so that we can use them for idref-matching 618 // store this in the context 619 // 620 Map beansById = (Map) context.getVariable( "beans-index" ); 621 if ( beansById == null ) { 622 // lazy creation 623 beansById = new HashMap(); 624 context.setVariable( "beans-index", beansById ); 625 log.trace( "Created new index-by-id map" ); 626 } 627 628 return beansById; 629 } 630 631 /** 632 * Return something meaningful for logging. 633 * 634 * @return something useful for logging 635 */ 636 public String toString() { 637 return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]"; 638 } 639 640 }