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    }