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
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
412 path = ResourceUtils.normalizePath(path);
413
414
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
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
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 }