View Javadoc

1   package org.andromda.core.common;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.Reader;
6   import java.io.StringReader;
7   
8   import java.net.URL;
9   
10  import java.util.HashMap;
11  import java.util.Map;
12  
13  import org.apache.commons.digester.Digester;
14  import org.apache.commons.digester.xmlrules.DigesterLoader;
15  import org.apache.commons.lang.StringUtils;
16  import org.apache.log4j.Logger;
17  import org.xml.sax.EntityResolver;
18  import org.xml.sax.InputSource;
19  import org.xml.sax.SAXException;
20  import org.xml.sax.SAXParseException;
21  
22  
23  /***
24   * <p>
25   * Creates and returns Objects based on a set of Apache Digester rules in a consistent manner, providing validation in
26   * the process.
27   * </p>
28   * <p>
29   * This XML object factory allows us to define a consistent/clean of configuring java objects from XML configuration
30   * files (i.e. it uses the class name of the java object to find what rule file and what XSD file to use). It also
31   * allows us to define a consistent way in which schema validation is performed.
32   * <p/>
33   * <p>
34   * It seperates each concern into one file, for example: to configure and perform validation on the MetafacadeMappings
35   * class, we need 3 files 1.) the java object (MetafacadeMappings.java), 2.) the rules file which tells the apache
36   * digester how to populate the java object from the XML configuration file (MetafacadeMappings-Rules.xml), and 3.) the
37   * XSD schema validation file (MetafacadeMappings.xsd). Note that each file is based on the name of the java object:
38   * 'java object name'.xsd and 'java object name'-Rules.xml'. After you have these three files then you just need to call
39   * the method #getInstance(java.net.URL objectClass) in this class from the java object you want to configure. This
40   * keeps the dependency to digester (or whatever XML configuration tool we are using at the time) to this single file.
41   * </p>
42   * <p>
43   * In order to add/modify an existing element/attribute in your configuration file, first make the modification in your
44   * java object, then modify it's rules file to instruct the digester on how to configure your new attribute/method in
45   * the java object, and then modify your XSD file to provide correct validation for this new method/attribute. Please
46   * see the org.andromda.core.metafacade.MetafacadeMappings* files for an example on how to do this.
47   * </p>
48   *
49   * @author Chad Brandon
50   */
51  public class XmlObjectFactory
52  {
53      /***
54       * The class logger. Note: visibility is protected to improve access within {@link XmlObjectValidator}
55       */
56      protected static final Logger logger = Logger.getLogger(XmlObjectFactory.class);
57  
58      /***
59       * The expected suffixes for rule files.
60       */
61      private static final String RULES_SUFFIX = "-Rules.xml";
62  
63      /***
64       * The expected suffix for XSD files.
65       */
66      private static final String SCHEMA_SUFFIX = ".xsd";
67  
68      /***
69       * The digester instance.
70       */
71      private Digester digester = null;
72  
73      /***
74       * The class of which the object we're instantiating.
75       */
76      private Class objectClass = null;
77  
78      /***
79       * The URL to the object rules.
80       */
81      private URL objectRulesXml = null;
82  
83      /***
84       * The URL of the schema.
85       */
86      private URL schemaUri = null;
87  
88      /***
89       * Whether or not validation should be turned on by default when using this factory to load XML configuration
90       * files.
91       */
92      private static boolean defaultValidating = true;
93  
94      /***
95       * Cache containing XmlObjectFactory instances which have already been configured for given objectRulesXml
96       */
97      private static final Map factoryCache = new HashMap();
98  
99      /***
100      * Creates an instance of this XmlObjectFactory with the given <code>objectRulesXml</code>
101      *
102      * @param objectRulesXml
103      */
104     private XmlObjectFactory(final URL objectRulesXml)
105     {
106         ExceptionUtils.checkNull(
107             "objectRulesXml",
108             objectRulesXml);
109         this.digester = DigesterLoader.createDigester(objectRulesXml);
110         this.digester.setUseContextClassLoader(true);
111     }
112 
113     /***
114      * Gets an instance of this XmlObjectFactory using the digester rules belonging to the <code>objectClass</code>.
115      *
116      * @param objectClass the Class of the object from which to configure this factory.
117      * @return the XmlObjectFactoy instance.
118      */
119     public static XmlObjectFactory getInstance(final Class objectClass)
120     {
121         ExceptionUtils.checkNull(
122             "objectClass",
123             objectClass);
124 
125         XmlObjectFactory factory = (XmlObjectFactory)factoryCache.get(objectClass);
126         if (factory == null)
127         {
128             final URL objectRulesXml =
129                 XmlObjectFactory.class.getResource('/' + objectClass.getName().replace(
130                         '.',
131                         '/') + RULES_SUFFIX);
132             if (objectRulesXml == null)
133             {
134                 throw new XmlObjectFactoryException("No configuration rules found for class --> '" + objectClass + "'");
135             }
136             factory = new XmlObjectFactory(objectRulesXml);
137             factory.objectClass = objectClass;
138             factory.objectRulesXml = objectRulesXml;
139             factory.setValidating(defaultValidating);
140             factoryCache.put(
141                 objectClass,
142                 factory);
143         }
144 
145         return factory;
146     }
147 
148     /***
149      * Allows us to set default validation to true/false for all instances of objects instantiated by this factory. This
150      * is necessary in some cases where the underlying parser doesn't support schema validation (such as when performing
151      * JUnit tests)
152      *
153      * @param validating true/false
154      */
155     public static void setDefaultValidating(final boolean validating)
156     {
157         defaultValidating = validating;
158     }
159 
160     /***
161      * Sets whether or not the XmlObjectFactory should be validating, default is <code>true</code>. If it IS set to be
162      * validating, then there needs to be a schema named objectClass.xsd in the same package as the objectClass that
163      * this factory was created from.
164      *
165      * @param validating true/false
166      */
167     public void setValidating(final boolean validating)
168     {
169         this.digester.setValidating(validating);
170         if (validating)
171         {
172             if (this.schemaUri == null)
173             {
174                 final String schemaLocation = '/' + this.objectClass.getName().replace(
175                         '.',
176                         '/') + SCHEMA_SUFFIX;
177                 this.schemaUri = XmlObjectFactory.class.getResource(schemaLocation);
178                 try
179                 {
180                     if (this.schemaUri != null)
181                     {
182                         InputStream stream = this.schemaUri.openStream();
183                         stream.close();
184                         stream = null;
185                     }
186                 }
187                 catch (final IOException exception)
188                 {
189                     this.schemaUri = null;
190                 }
191                 if (this.schemaUri == null)
192                 {
193                     logger.warn(
194                         "WARNING! Was not able to find schemaUri --> '" + schemaLocation +
195                         "' continuing in non validating mode");
196                 }
197             }
198             if (this.schemaUri != null)
199             {
200                 try
201                 {
202                     this.digester.setSchema(this.schemaUri.toString());
203                     this.digester.setErrorHandler(new XmlObjectValidator());
204 
205                     // also set the JAXP properties in case we're using a parser that needs those
206                     this.digester.setProperty(
207                         JAXP_SCHEMA_LANGUAGE,
208                         this.digester.getSchemaLanguage());
209                     this.digester.setProperty(
210                         JAXP_SCHEMA_SOURCE,
211                         this.digester.getSchema());
212                 }
213                 catch (final Exception exception)
214                 {
215                     logger.warn(
216                         "WARNING! Your parser does NOT support the " +
217                         " schema validation continuing in non validation mode",
218                         exception);
219                 }
220             }
221         }
222     }
223 
224     /***
225      * The JAXP 1.2 property required to set up the schema location.
226      */
227     protected static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
228 
229     /***
230      * The JAXP 1.2 property to set up the schemaLanguage used.
231      */
232     protected String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
233 
234     /***
235      * Returns a configured Object based on the objectXml configuration file
236      *
237      * @param objectXml the path to the Object XML config file.
238      * @return Object the created instance.
239      */
240     public Object getObject(final URL objectXml)
241     {
242         return this.getObject(
243             objectXml != null ? ResourceUtils.getContents(objectXml) : null,
244             objectXml);
245     }
246 
247     /***
248      * Returns a configured Object based on the objectXml configuration reader.
249      *
250      * @param objectXml the path to the Object XML config file.
251      * @return Object the created instance.
252      */
253     public Object getObject(final Reader objectXml)
254     {
255         return getObject(ResourceUtils.getContents(objectXml));
256     }
257 
258     /***
259      * Returns a configured Object based on the objectXml configuration file passed in as a String.
260      *
261      * @param objectXml the path to the Object XML config file.
262      * @return Object the created instance.
263      */
264     public Object getObject(String objectXml)
265     {
266         return this.getObject(
267             objectXml,
268             null);
269     }
270 
271     /***
272      * Returns a configured Object based on the objectXml configuration file passed in as a String.
273      *
274      * @param objectXml the path to the Object XML config file.
275      * @param resource the resource from which the objectXml was retrieved (this is needed to resolve
276      *        any relative references; like XML entities).
277      * @return Object the created instance.
278      */
279     public Object getObject(
280         String objectXml,
281         final URL resource)
282     {
283         ExceptionUtils.checkNull(
284             "objectXml",
285             objectXml);
286         Object object = null;
287         try
288         {
289             this.digester.setEntityResolver(new XmlObjectEntityResolver(resource));
290             object = this.digester.parse(new StringReader(objectXml));
291             objectXml = null;
292             if (object == null)
293             {
294                 final String message =
295                     "Was not able to instantiate an object using objectRulesXml '" + this.objectRulesXml +
296                     "' with objectXml '" + objectXml + "', please check either the objectXml " +
297                     "or objectRulesXml file for inconsistencies";
298                 throw new XmlObjectFactoryException(message);
299             }
300         }
301         catch (final SAXException exception)
302         {
303             final Throwable cause = ExceptionUtils.getRootCause(exception);
304             if (cause instanceof SAXException)
305             {
306                 final String message =
307                     "VALIDATION FAILED for --> '" + objectXml + "' against SCHEMA --> '" + this.schemaUri +
308                     "' --> message: '" + exception.getMessage() + "'";
309                 throw new XmlObjectFactoryException(message);
310             }
311             throw new XmlObjectFactoryException(cause);
312         }
313         catch (final Throwable throwable)
314         {
315             final String message = "XML resource could not be loaded --> '" + objectXml + "'";
316             throw new XmlObjectFactoryException(message, throwable);
317         }
318         return object;
319     }
320 
321     /***
322      * Handles the validation errors.
323      */
324     static final class XmlObjectValidator
325         implements org.xml.sax.ErrorHandler
326     {
327         /***
328          * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
329          */
330         public final void error(final SAXParseException exception)
331             throws SAXException
332         {
333             throw new SAXException(this.getMessage(exception));
334         }
335 
336         /***
337          * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
338          */
339         public final void fatalError(final SAXParseException exception)
340             throws SAXException
341         {
342             throw new SAXException(this.getMessage(exception));
343         }
344 
345         /***
346          * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
347          */
348         public final void warning(final SAXParseException exception)
349         {
350             logger.warn("WARNING!: " + this.getMessage(exception));
351         }
352 
353         /***
354          * Constructs and returns the appropriate error message.
355          *
356          * @param exception the exception from which to extract the message.
357          * @return the message.
358          */
359         private String getMessage(final SAXParseException exception)
360         {
361             final StringBuffer message = new StringBuffer();
362             if (exception != null)
363             {
364                 message.append(exception.getMessage());
365                 message.append(", line: ");
366                 message.append(exception.getLineNumber());
367                 message.append(", column: ").append(exception.getColumnNumber());
368             }
369             return message.toString();
370         }
371     }
372 
373     /***
374      * The prefix that the systemId should start with when attempting
375      * to resolve it within a jar.
376      */
377     private static final String SYSTEM_ID_FILE = "file:";
378 
379     /***
380      * Provides the resolution of external entities from the classpath.
381      */
382     private static final class XmlObjectEntityResolver
383         implements EntityResolver
384     {
385         private URL xmlResource;
386 
387         XmlObjectEntityResolver(final URL xmlResource)
388         {
389             this.xmlResource = xmlResource;
390         }
391 
392         /***
393          * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
394          */
395         public InputSource resolveEntity(
396             final String publicId,
397             final String systemId)
398             throws SAXException, IOException
399         {
400             InputSource source = null;
401             if (this.xmlResource != null)
402             {
403                 String path = systemId;
404                 if (path != null && path.startsWith(SYSTEM_ID_FILE))
405                 {
406                     final String xmlResource = this.xmlResource.toString();
407                     path = path.replaceFirst(
408                             SYSTEM_ID_FILE,
409                             "");
410 
411                     // - remove any extra starting slashes
412                     path = ResourceUtils.normalizePath(path);
413 
414                     // - if we still have one starting slash, remove it
415                     if (path.startsWith("/"))
416                     {
417                         path = path.substring(
418                                 1,
419                                 path.length());
420                     }
421                     final String xmlResourceName = xmlResource.replaceAll(
422                             ".*(//+|/)",
423                             "");
424                     URL uri = null;
425                     InputStream inputStream = null;
426                     try
427                     {
428                         uri = ResourceUtils.toURL(StringUtils.replace(
429                                     xmlResource,
430                                     xmlResourceName,
431                                     path));
432                         if (uri != null)
433                         {
434                             inputStream = uri.openStream();
435                         }
436                     }
437                     catch (final IOException exception)
438                     {
439                         // - ignore
440                     }
441                     if (inputStream == null)
442                     {
443                         try
444                         {
445                             uri = ResourceUtils.getResource(path);
446                             if (uri != null)
447                             {
448                                 inputStream = uri.openStream();
449                             }
450                         }
451                         catch (final IOException exception)
452                         {
453                             // - ignore
454                         }
455                     }
456                     if (inputStream != null)
457                     {
458                         source = new InputSource(inputStream);
459                         source.setPublicId(publicId);
460                         source.setSystemId(uri.toString());
461                     }
462                 }
463             }
464             return source;
465         }
466     }
467 }