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