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