View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */ 
17  package org.apache.commons.betwixt.io;
18  
19  import java.beans.IntrospectionException;
20  import java.io.IOException;
21  import java.util.HashSet;
22  import java.util.Set;
23  
24  import javax.xml.parsers.SAXParser;
25  
26  import org.apache.commons.betwixt.BindingConfiguration;
27  import org.apache.commons.betwixt.ElementDescriptor;
28  import org.apache.commons.betwixt.XMLBeanInfo;
29  import org.apache.commons.betwixt.XMLIntrospector;
30  import org.apache.commons.betwixt.io.read.ReadConfiguration;
31  import org.apache.commons.betwixt.io.read.ReadContext;
32  import org.apache.commons.digester.Digester;
33  import org.apache.commons.digester.ExtendedBaseRules;
34  import org.apache.commons.digester.RuleSet;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.xml.sax.InputSource;
38  import org.xml.sax.SAXException;
39  import org.xml.sax.XMLReader;
40  
41  /** <p><code>BeanReader</code> reads a tree of beans from an XML document.</p>
42    *
43    * <p>Call {@link #registerBeanClass(Class)} or {@link #registerBeanClass(String, Class)}
44    * to add rules to map a bean class.</p>
45    *
46    * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
47    */
48  public class BeanReader extends Digester {
49  
50      /** Introspector used */
51      private XMLIntrospector introspector = new XMLIntrospector();    
52      /** Log used for logging (Doh!) */
53      private Log log = LogFactory.getLog( BeanReader.class );
54      /** The registered classes */
55      private Set registeredClasses = new HashSet();
56      /** Dynamic binding configuration settings */
57      private BindingConfiguration bindingConfiguration = new BindingConfiguration();
58      /** Reading specific configuration settings */
59      private ReadConfiguration readConfiguration = new ReadConfiguration();
60      
61      /**
62       * Construct a new BeanReader with default properties.
63       */
64      public BeanReader() {
65      	// TODO: now we require extended rules may need to document this
66      	setRules(new ExtendedBaseRules());
67      }
68  
69      /**
70       * Construct a new BeanReader, allowing a SAXParser to be passed in.  This
71       * allows BeanReader to be used in environments which are unfriendly to
72       * JAXP1.1 (such as WebLogic 6.0).  Thanks for the request to change go to
73       * James House (james@interobjective.com).  This may help in places where
74       * you are able to load JAXP 1.1 classes yourself.
75       *
76       * @param parser use this <code>SAXParser</code>
77       */
78      public BeanReader(SAXParser parser) {
79          super(parser);
80  		setRules(new ExtendedBaseRules());
81      }
82  
83      /**
84       * Construct a new BeanReader, allowing an XMLReader to be passed in.  This
85       * allows BeanReader to be used in environments which are unfriendly to
86       * JAXP1.1 (such as WebLogic 6.0).  Note that if you use this option you
87       * have to configure namespace and validation support yourself, as these
88       * properties only affect the SAXParser and emtpy constructor.
89       *
90       * @param reader use this <code>XMLReader</code> as source for SAX events
91       */
92      public BeanReader(XMLReader reader) {
93          super(reader);
94  		setRules(new ExtendedBaseRules());
95      }
96  
97      
98      /** 
99       * <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 }