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.beans.IntrospectionException;
020    import java.io.IOException;
021    import java.util.HashSet;
022    import java.util.Set;
023    
024    import javax.xml.parsers.SAXParser;
025    
026    import org.apache.commons.betwixt.BindingConfiguration;
027    import org.apache.commons.betwixt.ElementDescriptor;
028    import org.apache.commons.betwixt.XMLBeanInfo;
029    import org.apache.commons.betwixt.XMLIntrospector;
030    import org.apache.commons.betwixt.io.read.ReadConfiguration;
031    import org.apache.commons.betwixt.io.read.ReadContext;
032    import org.apache.commons.digester.Digester;
033    import org.apache.commons.digester.ExtendedBaseRules;
034    import org.apache.commons.digester.RuleSet;
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    import org.xml.sax.InputSource;
038    import org.xml.sax.SAXException;
039    import org.xml.sax.XMLReader;
040    
041    /** <p><code>BeanReader</code> reads a tree of beans from an XML document.</p>
042      *
043      * <p>Call {@link #registerBeanClass(Class)} or {@link #registerBeanClass(String, Class)}
044      * to add rules to map a bean class.</p>
045      *
046      * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
047      */
048    public class BeanReader extends Digester {
049    
050        /** Introspector used */
051        private XMLIntrospector introspector = new XMLIntrospector();    
052        /** Log used for logging (Doh!) */
053        private Log log = LogFactory.getLog( BeanReader.class );
054        /** The registered classes */
055        private Set registeredClasses = new HashSet();
056        /** Dynamic binding configuration settings */
057        private BindingConfiguration bindingConfiguration = new BindingConfiguration();
058        /** Reading specific configuration settings */
059        private ReadConfiguration readConfiguration = new ReadConfiguration();
060        
061        /**
062         * Construct a new BeanReader with default properties.
063         */
064        public BeanReader() {
065            // TODO: now we require extended rules may need to document this
066            setRules(new ExtendedBaseRules());
067        }
068    
069        /**
070         * Construct a new BeanReader, allowing a SAXParser to be passed in.  This
071         * allows BeanReader to be used in environments which are unfriendly to
072         * JAXP1.1 (such as WebLogic 6.0).  Thanks for the request to change go to
073         * James House (james@interobjective.com).  This may help in places where
074         * you are able to load JAXP 1.1 classes yourself.
075         *
076         * @param parser use this <code>SAXParser</code>
077         */
078        public BeanReader(SAXParser parser) {
079            super(parser);
080                    setRules(new ExtendedBaseRules());
081        }
082    
083        /**
084         * Construct a new BeanReader, allowing an XMLReader to be passed in.  This
085         * allows BeanReader to be used in environments which are unfriendly to
086         * JAXP1.1 (such as WebLogic 6.0).  Note that if you use this option you
087         * have to configure namespace and validation support yourself, as these
088         * properties only affect the SAXParser and emtpy constructor.
089         *
090         * @param reader use this <code>XMLReader</code> as source for SAX events
091         */
092        public BeanReader(XMLReader reader) {
093            super(reader);
094                    setRules(new ExtendedBaseRules());
095        }
096    
097        
098        /** 
099         * <p>Register a bean class and add mapping rules for this bean class.</p>
100         * 
101         * <p>A bean class is introspected when it is registered.
102         * It will <strong>not</strong> be introspected again even if the introspection
103         * settings are changed.
104         * If re-introspection is required, then {@link #deregisterBeanClass} must be called 
105         * and the bean re-registered.</p>
106         *
107         * <p>A bean class can only be registered once. 
108         * If the same class is registered a second time, this registration will be ignored.
109         * In order to change a registration, call {@link #deregisterBeanClass} 
110         * before calling this method.</p>
111         *
112         * <p>All the rules required to digest this bean are added when this method is called.
113         * Other rules that you want to execute before these should be added before this
114         * method is called. 
115         * Those that should be executed afterwards, should be added afterwards.</p>
116         *
117         * @param beanClass the <code>Class</code> to be registered
118         * @throws IntrospectionException if the bean introspection fails
119         */
120        public void registerBeanClass(Class beanClass) throws IntrospectionException {
121            if ( ! registeredClasses.contains( beanClass ) ) {
122                register(beanClass, null);
123                
124            } else {
125                if ( log.isWarnEnabled() ) {
126                    log.warn("Cannot add class "  + beanClass.getName() + " since it already exists");
127                }
128            }
129        }
130        
131        /**
132         * Registers the given class at the given path.
133         * @param beanClass <code>Class</code> for binding
134         * @param path the path at which the bean class should be registered
135         * or null if the automatic path is to be used
136         * @throws IntrospectionException
137         */
138        private void register(Class beanClass, String path) throws IntrospectionException {
139            if ( log.isTraceEnabled() ) {
140                log.trace( "Registering class " + beanClass );
141            }
142            XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
143            registeredClasses.add( beanClass );
144    
145            ElementDescriptor elementDescriptor = xmlInfo.getElementDescriptor();        
146    
147            if (path == null) {
148                path = elementDescriptor.getQualifiedName();
149            }
150            
151            if (log.isTraceEnabled()) {
152                log.trace("Added path: " + path + ", mapped to: " + beanClass.getName());
153            }
154            addBeanCreateRule( path, elementDescriptor, beanClass );
155        }
156    
157        /** 
158         * <p>Registers a bean class  
159         * and add mapping rules for this bean class at the given path expression.</p>
160         * 
161         * 
162         * <p>A bean class is introspected when it is registered.
163         * It will <strong>not</strong> be introspected again even if the introspection
164         * settings are changed.
165         * If re-introspection is required, then {@link #deregisterBeanClass} must be called 
166         * and the bean re-registered.</p>
167         *
168         * <p>A bean class can only be registered once. 
169         * If the same class is registered a second time, this registration will be ignored.
170         * In order to change a registration, call {@link #deregisterBeanClass} 
171         * before calling this method.</p>
172         *
173         * <p>All the rules required to digest this bean are added when this method is called.
174         * Other rules that you want to execute before these should be added before this
175         * method is called. 
176         * Those that should be executed afterwards, should be added afterwards.</p>
177         *
178         * @param path the xml path expression where the class is to registered. 
179         * This should be in digester path notation
180         * @param beanClass the <code>Class</code> to be registered
181         * @throws IntrospectionException if the bean introspection fails
182         */
183        public void registerBeanClass(String path, Class beanClass) throws IntrospectionException {
184            if ( ! registeredClasses.contains( beanClass ) ) {
185                
186                register(beanClass, path);
187                
188            } else {
189                if ( log.isWarnEnabled() ) {
190                    log.warn("Cannot add class "  + beanClass.getName() + " since it already exists");
191                }
192            }
193        }
194        
195        /**
196         * <p>Registers a class with a multi-mapping.
197         * This mapping is specified by the multi-mapping document
198         * contained in the given <code>InputSource</code>.
199         * </p><p>
200         * <strong>Note:</strong> the custom mappings will be registered with
201         * the introspector. This must remain so for the reading to work correctly
202         * It is recommended that use of the pre-registeration process provided
203         * by {@link XMLIntrospector#register}  be considered as an alternative to this method.
204         * </p>
205         * @see #registerBeanClass(Class) since the general notes given there
206         * apply equally to this 
207         * @see XMLIntrospector#register(InputSource) for more details on the multi-mapping format
208         * @since 0.7
209         * @param mapping <code>InputSource</code> giving the multi-mapping document specifying 
210         * the mapping
211         * @throws IntrospectionException
212         * @throws SAXException
213         * @throws IOException
214         */
215        public void registerMultiMapping(InputSource mapping) throws IntrospectionException, IOException, SAXException {
216            Class[] mappedClasses = introspector.register(mapping);
217            for (int i=0, size=mappedClasses.length; i<size; i++) 
218            {
219                Class beanClass = mappedClasses[i];
220                    if ( ! registeredClasses.contains( beanClass ) ) {
221                        register(beanClass, null);
222                        
223                    }
224            }
225        }
226        
227        /**
228         * <p>Registers a class with a custom mapping.
229         * This mapping is specified by the standard dot betwixt document
230         * contained in the given <code>InputSource</code>.
231         * </p><p>
232         * <strong>Note:</strong> the custom mapping will be registered with
233         * the introspector. This must remain so for the reading to work correctly
234         * It is recommended that use of the pre-registeration process provided
235         * by {@link XMLIntrospector#register}  be considered as an alternative to this method.
236         * </p>
237         * @see #registerBeanClass(Class) since the general notes given there
238         * apply equally to this 
239         * @since 0.7
240         * @param mapping <code>InputSource</code> giving the dot betwixt document specifying 
241         * the mapping
242         * @param beanClass <code>Class</code> that should be register
243         * @throws IntrospectionException
244         * @throws SAXException
245         * @throws IOException
246         */
247        public void registerBeanClass(InputSource mapping, Class beanClass) throws IntrospectionException, IOException, SAXException {
248            if ( ! registeredClasses.contains( beanClass ) ) {
249                    
250                introspector.register( beanClass, mapping );
251                register(beanClass, null);
252                
253            } else {
254                if ( log.isWarnEnabled() ) {
255                    log.warn("Cannot add class "  + beanClass.getName() + " since it already exists");
256                }
257            }
258        }
259        
260        /**
261         * <p>Flush all registered bean classes.
262         * This allows all bean classes to be re-registered 
263         * by a subsequent calls to <code>registerBeanClass</code>.</p>
264         *
265         * <p><strong>Note</strong> that deregistering a bean does <strong>not</strong>
266         * remove the Digester rules associated with that bean.</p>
267         * @since 0.5
268         */
269        public void flushRegisteredBeanClasses() {    
270            registeredClasses.clear();
271        }
272        
273        /**
274         * <p>Remove the given class from the register.
275         * Calling this method will allow the bean class to be re-registered 
276         * by a subsequent call to <code>registerBeanClass</code>.
277         * This allows (for example) a bean to be reintrospected after a change
278         * to the introspection settings.</p>
279         *
280         * <p><strong>Note</strong> that deregistering a bean does <strong>not</strong>
281         * remove the Digester rules associated with that bean.</p>
282         *
283         * @param beanClass the <code>Class</code> to remove from the set of registered bean classes
284         * @since 0.5 
285         */
286        public void deregisterBeanClass( Class beanClass ) {
287            registeredClasses.remove( beanClass );
288        }
289        
290        // Properties
291        //-------------------------------------------------------------------------        
292    
293        /**
294         * <p> Get the introspector used. </p>
295         *
296         * <p> The {@link XMLBeanInfo} used to map each bean is 
297         * created by the <code>XMLIntrospector</code>.
298         * One way in which the mapping can be customized is by 
299         * altering the <code>XMLIntrospector</code>. </p>
300         * 
301         * @return the <code>XMLIntrospector</code> used for the introspection
302         */
303        public XMLIntrospector getXMLIntrospector() {
304            return introspector;
305        }
306        
307    
308        /**
309         * <p> Set the introspector to be used. </p>
310         *
311         * <p> The {@link XMLBeanInfo} used to map each bean is 
312         * created by the <code>XMLIntrospector</code>.
313         * One way in which the mapping can be customized is by 
314         * altering the <code>XMLIntrospector</code>. </p>
315         *
316         * @param introspector use this introspector
317         */
318        public void setXMLIntrospector(XMLIntrospector introspector) {
319            this.introspector = introspector;
320        }
321    
322        /**
323         * <p> Get the current level for logging. </p>
324         *
325         * @return the <code>Log</code> implementation this class logs to
326         */ 
327        public Log getLog() {
328            return log;
329        }
330    
331        /**
332         * <p> Set the current logging level. </p>
333         *
334         * @param log the <code>Log</code>implementation to use for logging
335         */ 
336        public void setLog(Log log) {
337            this.log = log;
338            setLogger(log);
339        }
340        
341        /** 
342         * Should the reader use <code>ID</code> attributes to match beans.
343         *
344         * @return true if <code>ID</code> and <code>IDREF</code> 
345         * attributes should be used to match instances
346         * @deprecated 0.5 use {@link BindingConfiguration#getMapIDs}
347         */
348        public boolean getMatchIDs() {
349            return getBindingConfiguration().getMapIDs();
350        }
351        
352        /**
353         * Set whether the read should use <code>ID</code> attributes to match beans.
354         *
355         * @param matchIDs pass true if <code>ID</code>'s should be matched
356         * @deprecated 0.5 use {@link BindingConfiguration#setMapIDs}
357         */
358        public void setMatchIDs(boolean matchIDs) {
359            getBindingConfiguration().setMapIDs( matchIDs );
360        }
361        
362        /**
363         * Gets the dynamic configuration setting to be used for bean reading.
364         * @return the BindingConfiguration settings, not null
365         * @since 0.5
366         */
367        public BindingConfiguration getBindingConfiguration() {
368            return bindingConfiguration;
369        }
370        
371        /**
372         * Sets the dynamic configuration setting to be used for bean reading.
373         * @param bindingConfiguration the BindingConfiguration settings, not null
374         * @since 0.5
375         */
376        public void setBindingConfiguration( BindingConfiguration bindingConfiguration ) {
377            this.bindingConfiguration = bindingConfiguration;
378        }
379        
380        /**
381         * Gets read specific configuration details.
382         * @return the ReadConfiguration, not null
383         * @since 0.5
384         */
385        public ReadConfiguration getReadConfiguration() {
386            return readConfiguration;
387        }
388        
389        /**
390         * Sets the read specific configuration details.
391         * @param readConfiguration not null
392         * @since 0.5
393         */
394        public void setReadConfiguration( ReadConfiguration readConfiguration ) {
395            this.readConfiguration = readConfiguration;
396        }
397            
398        // Implementation methods
399        //-------------------------------------------------------------------------    
400        
401        /** 
402         * Adds a new bean create rule for the specified path
403         *
404         * @param path the digester path at which this rule should be added
405         * @param elementDescriptor the <code>ElementDescriptor</code> describes the expected element 
406         * @param beanClass the <code>Class</code> of the bean created by this rule
407         */
408        protected void addBeanCreateRule( 
409                                        String path, 
410                                        ElementDescriptor elementDescriptor, 
411                                        Class beanClass ) {
412            if (log.isTraceEnabled()) {
413                log.trace("Adding BeanRuleSet for " + beanClass);
414            }
415            RuleSet ruleSet = new BeanRuleSet( 
416                                                introspector, 
417                                                path ,  
418                                                elementDescriptor, 
419                                                beanClass, 
420                                                makeContext());
421            addRuleSet( ruleSet );
422        }
423            
424        /**
425          * Factory method for new contexts.
426          * Ensure that they are correctly configured.
427          * @return the ReadContext created, not null
428          */
429        private ReadContext makeContext() {
430            return new ReadContext( log, bindingConfiguration, readConfiguration );
431        }
432    }