View Javadoc

1   /*
2    * Copyright 2001-2004 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */ 
16  package org.apache.commons.betwixt.io;
17  
18  import java.util.HashMap;
19  import java.util.List;
20  import java.util.Map;
21  
22  import org.apache.commons.betwixt.AttributeDescriptor;
23  import org.apache.commons.betwixt.ElementDescriptor;
24  import org.apache.commons.betwixt.XMLBeanInfo;
25  import org.apache.commons.betwixt.XMLIntrospector;
26  import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
27  import org.apache.commons.betwixt.expression.Context;
28  import org.apache.commons.betwixt.expression.MethodUpdater;
29  import org.apache.commons.betwixt.expression.Updater;
30  import org.apache.commons.digester.Rule;
31  import org.apache.commons.digester.Rules;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.xml.sax.Attributes;
35  
36  /*** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
37    * from the betwixt XML metadata.</p>
38    *
39    * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
40    * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
41    * @version $Revision: 1.25.2.1 $
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     /*** Allows the navigation from a reference to a property object to the descriptor defining what 
545      * the property is. i.e. doing the join from a reference to a type to lookup its descriptor.
546      * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
547      *
548      * @param propertyDescriptor find descriptor for property object referenced by this descriptor
549      * @return descriptor for the singular property class type referenced.
550      */
551     protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
552         Class beanClass = propertyDescriptor.getSingularPropertyType();
553         if ( beanClass != null ) {
554             XMLIntrospector introspector = getBeanReader().getXMLIntrospector();
555             try {
556                 XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
557                 return xmlInfo.getElementDescriptor();
558                 
559             } catch (Exception e) {
560                 log.warn( "Could not introspect class: " + beanClass, e );
561             }
562         }
563         // could not find a better descriptor so use the one we've got
564         return propertyDescriptor;
565     }
566     
567     /*** 
568      * Adds a new Digester rule to process the text as a primitive type
569      *
570      * @param path digester path where this rule will be attached
571      * @param childDescriptor update this <code>ElementDescriptor</code> with the body text
572      */
573     protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) {
574         Rule rule = new Rule() {
575             public void body(String text) throws Exception {
576                 childDescriptor.getUpdater().update( context, text );
577             }        
578         };
579         addRule( path, rule );
580     }
581     
582     /***
583      * Safely add a rule with given path.
584      *
585      * @param path the digester path to add rule at
586      * @param rule the <code>Rule</code> to add
587      */
588     protected void addRule(String path, Rule rule) {
589         Rules rules = digester.getRules();
590         List matches = rules.match(null, path);
591         if ( matches.isEmpty() ) {
592             if ( log.isDebugEnabled() ) {
593                 log.debug( "Adding digester rule for path: " + path + " rule: " + rule );
594             }
595             digester.addRule( path, rule );
596             
597         } else {
598             if ( log.isDebugEnabled() ) {
599                 log.debug( "Ignoring duplicate digester rule for path: " 
600                             + path + " rule: " + rule );
601                 log.debug( "New rule (not added): " + rule );
602                 log.debug( "Existing rule:" + matches.get(0) );
603             }
604         }
605     }    
606 
607     /***
608      * Get the map used to index beans (previously read in) by id.
609      * This is stored in the evaluation context.
610      *
611      * @return map indexing beans created by id
612      */
613     protected Map getBeansById() {
614         //
615         // we need a single index for beans read in by id
616         // so that we can use them for idref-matching
617         // store this in the context
618         //
619         Map beansById = (Map) context.getVariable( "beans-index" );
620         if ( beansById == null ) {
621             // lazy creation
622             beansById = new HashMap();
623             context.setVariable( "beans-index", beansById );
624             log.trace( "Created new index-by-id map" );
625         }
626         
627         return beansById;
628     }
629     
630     /***
631      * Return something meaningful for logging.
632      *
633      * @return something useful for logging
634      */
635     public String toString() {
636         return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
637     }
638     
639 }