001: package org.andromda.core.common;
002:
003: import java.io.IOException;
004: import java.io.InputStream;
005: import java.io.Reader;
006: import java.io.StringReader;
007:
008: import java.net.URL;
009:
010: import java.util.HashMap;
011: import java.util.Map;
012:
013: import org.apache.commons.digester.Digester;
014: import org.apache.commons.digester.xmlrules.DigesterLoader;
015: import org.apache.commons.lang.StringUtils;
016: import org.apache.log4j.Logger;
017: import org.xml.sax.EntityResolver;
018: import org.xml.sax.InputSource;
019: import org.xml.sax.SAXException;
020: import org.xml.sax.SAXParseException;
021:
022: /**
023: * <p>
024: * Creates and returns Objects based on a set of Apache Digester rules in a consistent manner, providing validation in
025: * the process.
026: * </p>
027: * <p>
028: * This XML object factory allows us to define a consistent/clean of configuring java objects from XML configuration
029: * 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
030: * allows us to define a consistent way in which schema validation is performed.
031: * <p/>
032: * <p>
033: * It seperates each concern into one file, for example: to configure and perform validation on the MetafacadeMappings
034: * class, we need 3 files 1.) the java object (MetafacadeMappings.java), 2.) the rules file which tells the apache
035: * digester how to populate the java object from the XML configuration file (MetafacadeMappings-Rules.xml), and 3.) the
036: * XSD schema validation file (MetafacadeMappings.xsd). Note that each file is based on the name of the java object:
037: * 'java object name'.xsd and 'java object name'-Rules.xml'. After you have these three files then you just need to call
038: * the method #getInstance(java.net.URL objectClass) in this class from the java object you want to configure. This
039: * keeps the dependency to digester (or whatever XML configuration tool we are using at the time) to this single file.
040: * </p>
041: * <p>
042: * In order to add/modify an existing element/attribute in your configuration file, first make the modification in your
043: * java object, then modify it's rules file to instruct the digester on how to configure your new attribute/method in
044: * the java object, and then modify your XSD file to provide correct validation for this new method/attribute. Please
045: * see the org.andromda.core.metafacade.MetafacadeMappings* files for an example on how to do this.
046: * </p>
047: *
048: * @author Chad Brandon
049: */
050: public class XmlObjectFactory {
051: /**
052: * The class logger. Note: visibility is protected to improve access within {@link XmlObjectValidator}
053: */
054: protected static final Logger logger = Logger
055: .getLogger(XmlObjectFactory.class);
056:
057: /**
058: * The expected suffixes for rule files.
059: */
060: private static final String RULES_SUFFIX = "-Rules.xml";
061:
062: /**
063: * The expected suffix for XSD files.
064: */
065: private static final String SCHEMA_SUFFIX = ".xsd";
066:
067: /**
068: * The digester instance.
069: */
070: private Digester digester = null;
071:
072: /**
073: * The class of which the object we're instantiating.
074: */
075: private Class objectClass = null;
076:
077: /**
078: * The URL to the object rules.
079: */
080: private URL objectRulesXml = null;
081:
082: /**
083: * The URL of the schema.
084: */
085: private URL schemaUri = null;
086:
087: /**
088: * Whether or not validation should be turned on by default when using this factory to load XML configuration
089: * files.
090: */
091: private static boolean defaultValidating = true;
092:
093: /**
094: * Cache containing XmlObjectFactory instances which have already been configured for given objectRulesXml
095: */
096: private static final Map factoryCache = new HashMap();
097:
098: /**
099: * Creates an instance of this XmlObjectFactory with the given <code>objectRulesXml</code>
100: *
101: * @param objectRulesXml
102: */
103: private XmlObjectFactory(final URL objectRulesXml) {
104: ExceptionUtils.checkNull("objectRulesXml", objectRulesXml);
105: this .digester = DigesterLoader.createDigester(objectRulesXml);
106: this .digester.setUseContextClassLoader(true);
107: }
108:
109: /**
110: * Gets an instance of this XmlObjectFactory using the digester rules belonging to the <code>objectClass</code>.
111: *
112: * @param objectClass the Class of the object from which to configure this factory.
113: * @return the XmlObjectFactoy instance.
114: */
115: public static XmlObjectFactory getInstance(final Class objectClass) {
116: ExceptionUtils.checkNull("objectClass", objectClass);
117:
118: XmlObjectFactory factory = (XmlObjectFactory) factoryCache
119: .get(objectClass);
120: if (factory == null) {
121: final URL objectRulesXml = XmlObjectFactory.class
122: .getResource('/'
123: + objectClass.getName().replace('.', '/')
124: + RULES_SUFFIX);
125: if (objectRulesXml == null) {
126: throw new XmlObjectFactoryException(
127: "No configuration rules found for class --> '"
128: + objectClass + "'");
129: }
130: factory = new XmlObjectFactory(objectRulesXml);
131: factory.objectClass = objectClass;
132: factory.objectRulesXml = objectRulesXml;
133: factory.setValidating(defaultValidating);
134: factoryCache.put(objectClass, factory);
135: }
136:
137: return factory;
138: }
139:
140: /**
141: * Allows us to set default validation to true/false for all instances of objects instantiated by this factory. This
142: * is necessary in some cases where the underlying parser doesn't support schema validation (such as when performing
143: * JUnit tests)
144: *
145: * @param validating true/false
146: */
147: public static void setDefaultValidating(final boolean validating) {
148: defaultValidating = validating;
149: }
150:
151: /**
152: * Sets whether or not the XmlObjectFactory should be validating, default is <code>true</code>. If it IS set to be
153: * validating, then there needs to be a schema named objectClass.xsd in the same package as the objectClass that
154: * this factory was created from.
155: *
156: * @param validating true/false
157: */
158: public void setValidating(final boolean validating) {
159: this .digester.setValidating(validating);
160: if (validating) {
161: if (this .schemaUri == null) {
162: final String schemaLocation = '/'
163: + this .objectClass.getName().replace('.', '/')
164: + SCHEMA_SUFFIX;
165: this .schemaUri = XmlObjectFactory.class
166: .getResource(schemaLocation);
167: try {
168: if (this .schemaUri != null) {
169: InputStream stream = this .schemaUri
170: .openStream();
171: stream.close();
172: stream = null;
173: }
174: } catch (final IOException exception) {
175: this .schemaUri = null;
176: }
177: if (this .schemaUri == null) {
178: logger
179: .warn("WARNING! Was not able to find schemaUri --> '"
180: + schemaLocation
181: + "' continuing in non validating mode");
182: }
183: }
184: if (this .schemaUri != null) {
185: try {
186: this .digester.setSchema(this .schemaUri.toString());
187: this .digester
188: .setErrorHandler(new XmlObjectValidator());
189:
190: // also set the JAXP properties in case we're using a parser that needs those
191: this .digester.setProperty(JAXP_SCHEMA_LANGUAGE,
192: this .digester.getSchemaLanguage());
193: this .digester.setProperty(JAXP_SCHEMA_SOURCE,
194: this .digester.getSchema());
195: } catch (final Exception exception) {
196: logger
197: .warn(
198: "WARNING! Your parser does NOT support the "
199: + " schema validation continuing in non validation mode",
200: exception);
201: }
202: }
203: }
204: }
205:
206: /**
207: * The JAXP 1.2 property required to set up the schema location.
208: */
209: protected static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
210:
211: /**
212: * The JAXP 1.2 property to set up the schemaLanguage used.
213: */
214: protected String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
215:
216: /**
217: * Returns a configured Object based on the objectXml configuration file
218: *
219: * @param objectXml the path to the Object XML config file.
220: * @return Object the created instance.
221: */
222: public Object getObject(final URL objectXml) {
223: return this .getObject(objectXml != null ? ResourceUtils
224: .getContents(objectXml) : null, objectXml);
225: }
226:
227: /**
228: * Returns a configured Object based on the objectXml configuration reader.
229: *
230: * @param objectXml the path to the Object XML config file.
231: * @return Object the created instance.
232: */
233: public Object getObject(final Reader objectXml) {
234: return getObject(ResourceUtils.getContents(objectXml));
235: }
236:
237: /**
238: * Returns a configured Object based on the objectXml configuration file passed in as a String.
239: *
240: * @param objectXml the path to the Object XML config file.
241: * @return Object the created instance.
242: */
243: public Object getObject(String objectXml) {
244: return this .getObject(objectXml, null);
245: }
246:
247: /**
248: * Returns a configured Object based on the objectXml configuration file passed in as a String.
249: *
250: * @param objectXml the path to the Object XML config file.
251: * @param resource the resource from which the objectXml was retrieved (this is needed to resolve
252: * any relative references; like XML entities).
253: * @return Object the created instance.
254: */
255: public Object getObject(String objectXml, final URL resource) {
256: ExceptionUtils.checkNull("objectXml", objectXml);
257: Object object = null;
258: try {
259: this .digester
260: .setEntityResolver(new XmlObjectEntityResolver(
261: resource));
262: object = this .digester.parse(new StringReader(objectXml));
263: objectXml = null;
264: if (object == null) {
265: final String message = "Was not able to instantiate an object using objectRulesXml '"
266: + this .objectRulesXml
267: + "' with objectXml '"
268: + objectXml
269: + "', please check either the objectXml "
270: + "or objectRulesXml file for inconsistencies";
271: throw new XmlObjectFactoryException(message);
272: }
273: } catch (final SAXException exception) {
274: final Throwable cause = ExceptionUtils
275: .getRootCause(exception);
276: if (cause instanceof SAXException) {
277: final String message = "VALIDATION FAILED for --> '"
278: + objectXml + "' against SCHEMA --> '"
279: + this .schemaUri + "' --> message: '"
280: + exception.getMessage() + "'";
281: throw new XmlObjectFactoryException(message);
282: }
283: throw new XmlObjectFactoryException(cause);
284: } catch (final Throwable throwable) {
285: final String message = "XML resource could not be loaded --> '"
286: + objectXml + "'";
287: throw new XmlObjectFactoryException(message, throwable);
288: }
289: return object;
290: }
291:
292: /**
293: * Handles the validation errors.
294: */
295: static final class XmlObjectValidator implements
296: org.xml.sax.ErrorHandler {
297: /**
298: * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
299: */
300: public final void error(final SAXParseException exception)
301: throws SAXException {
302: throw new SAXException(this .getMessage(exception));
303: }
304:
305: /**
306: * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
307: */
308: public final void fatalError(final SAXParseException exception)
309: throws SAXException {
310: throw new SAXException(this .getMessage(exception));
311: }
312:
313: /**
314: * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
315: */
316: public final void warning(final SAXParseException exception) {
317: logger.warn("WARNING!: " + this .getMessage(exception));
318: }
319:
320: /**
321: * Constructs and returns the appropriate error message.
322: *
323: * @param exception the exception from which to extract the message.
324: * @return the message.
325: */
326: private String getMessage(final SAXParseException exception) {
327: final StringBuffer message = new StringBuffer();
328: if (exception != null) {
329: message.append(exception.getMessage());
330: message.append(", line: ");
331: message.append(exception.getLineNumber());
332: message.append(", column: ").append(
333: exception.getColumnNumber());
334: }
335: return message.toString();
336: }
337: }
338:
339: /**
340: * The prefix that the systemId should start with when attempting
341: * to resolve it within a jar.
342: */
343: private static final String SYSTEM_ID_FILE = "file:";
344:
345: /**
346: * Provides the resolution of external entities from the classpath.
347: */
348: private static final class XmlObjectEntityResolver implements
349: EntityResolver {
350: private URL xmlResource;
351:
352: XmlObjectEntityResolver(final URL xmlResource) {
353: this .xmlResource = xmlResource;
354: }
355:
356: /**
357: * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
358: */
359: public InputSource resolveEntity(final String publicId,
360: final String systemId) throws SAXException, IOException {
361: InputSource source = null;
362: if (this .xmlResource != null) {
363: String path = systemId;
364: if (path != null && path.startsWith(SYSTEM_ID_FILE)) {
365: final String xmlResource = this .xmlResource
366: .toString();
367: path = path.replaceFirst(SYSTEM_ID_FILE, "");
368:
369: // - remove any extra starting slashes
370: path = ResourceUtils.normalizePath(path);
371:
372: // - if we still have one starting slash, remove it
373: if (path.startsWith("/")) {
374: path = path.substring(1, path.length());
375: }
376: final String xmlResourceName = xmlResource
377: .replaceAll(".*(\\+|/)", "");
378: URL uri = null;
379: InputStream inputStream = null;
380: try {
381: uri = ResourceUtils.toURL(StringUtils.replace(
382: xmlResource, xmlResourceName, path));
383: if (uri != null) {
384: inputStream = uri.openStream();
385: }
386: } catch (final IOException exception) {
387: // - ignore
388: }
389: if (inputStream == null) {
390: try {
391: uri = ResourceUtils.getResource(path);
392: if (uri != null) {
393: inputStream = uri.openStream();
394: }
395: } catch (final IOException exception) {
396: // - ignore
397: }
398: }
399: if (inputStream != null) {
400: source = new InputSource(inputStream);
401: source.setPublicId(publicId);
402: source.setSystemId(uri.toString());
403: }
404: }
405: }
406: return source;
407: }
408: }
409: }
|