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 }