View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */ 
17  package org.apache.commons.betwixt.io;
18  
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  
23  import org.apache.commons.betwixt.AttributeDescriptor;
24  import org.apache.commons.betwixt.ElementDescriptor;
25  import org.apache.commons.betwixt.XMLBeanInfo;
26  import org.apache.commons.betwixt.XMLIntrospector;
27  import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
28  import org.apache.commons.betwixt.expression.Context;
29  import org.apache.commons.betwixt.expression.MethodUpdater;
30  import org.apache.commons.betwixt.expression.Updater;
31  import org.apache.commons.digester.Rule;
32  import org.apache.commons.digester.Rules;
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.xml.sax.Attributes;
36  
37  /** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
38    * from the betwixt XML metadata.</p>
39    *
40    * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
41    * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
42    * @deprecated 0.5 this Rule does not allowed good integration with other Rules -
43    * use {@link BeanRuleSet} instead.
44    */
45  public class BeanCreateRule extends Rule {
46  
47      /** Logger */
48      private static Log log = LogFactory.getLog( BeanCreateRule.class );
49      
50      /** 
51       * Set log to be used by <code>BeanCreateRule</code> instances 
52       * @param aLog the <code>Log</code> implementation for this class to log to
53       */
54      public static void setLog(Log aLog) {
55          log = aLog;
56      }
57      
58      /** The descriptor of this element */
59      private ElementDescriptor descriptor;
60      /** The Context used when evaluating Updaters */
61      private Context context;
62      /** Have we added our child rules to the digester? */
63      private boolean addedChildren;
64      /** In this begin-end loop did we actually create a new bean */
65      private boolean createdBean;
66      /** The type of the bean to create */
67      private Class beanClass;
68      /** The prefix added to digester rules */
69      private String pathPrefix;
70      /** Use id's to match beans? */
71      private boolean matchIDs = true;
72      /** allows an attribute to be specified to overload the types of beans used */
73      private String classNameAttribute = "className";
74      
75      /**
76       * Convenience constructor which uses <code>ID's</code> for matching.
77       *
78       * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
79       * @param beanClass the <code>Class</code> to be created
80       * @param pathPrefix the digester style path
81       */
82      public BeanCreateRule(
83                              ElementDescriptor descriptor, 
84                              Class beanClass, 
85                              String pathPrefix ) {
86          this( descriptor, beanClass, pathPrefix, true );
87      }
88      
89      /**
90       * Constructor taking a class.
91       *
92       * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
93       * @param beanClass the <code>Class</code> to be created
94       * @param pathPrefix the digester style path
95       * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
96       */
97      public BeanCreateRule(
98                              ElementDescriptor descriptor, 
99                              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 }