XmlObjectFactory.java
package org.andromda.core.common;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* <p>
* Creates and returns Objects based on a set of Apache Digester rules in a consistent manner, providing validation in
* the process.
* </p>
* <p>
* This XML object factory allows us to define a consistent/clean of configuring java objects from XML configuration
* 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
* allows us to define a consistent way in which schema validation is performed.
* </p>
* <p>
* It separates each concern into one file, for example: to configure and perform validation on the MetafacadeMappings
* class, we need 3 files 1.) the java object (MetafacadeMappings.java), 2.) the rules file which tells the apache
* digester how to populate the java object from the XML configuration file (MetafacadeMappings-Rules.xml), and 3.) the
* XSD schema validation file (MetafacadeMappings.xsd). Note that each file is based on the name of the java object:
* 'java object name'.xsd and 'java object name'-Rules.xml'. After you have these three files then you just need to call
* the method #getInstance(java.net.URL objectClass) in this class from the java object you want to configure. This
* keeps the dependency to digester (or whatever XML configuration tool we are using at the time) to this single file.
* </p>
* <p>
* In order to add/modify an existing element/attribute in your configuration file, first make the modification in your
* java object, then modify its rules file to instruct the digester on how to configure your new attribute/method in
* the java object, and then modify your XSD file to provide correct validation for this new method/attribute. Please
* see the org.andromda.core.metafacade.MetafacadeMappings* files for an example on how to do this.
* </p>
*
* @author Chad Brandon
* @author Bob Fields
* @author Michail Plushnikov
*/
public class XmlObjectFactory
{
/**
* The class logger. Note: visibility is protected to improve access within {@link XmlObjectValidator}
*/
protected static final Logger logger = Logger.getLogger(XmlObjectFactory.class);
/**
* The expected suffixes for rule files.
*/
private static final String RULES_SUFFIX = "-Rules.xml";
/**
* The expected suffix for XSD files.
*/
private static final String SCHEMA_SUFFIX = ".xsd";
/**
* The digester instance.
*/
private Digester digester = null;
/**
* The class of which the object we're instantiating.
*/
private Class objectClass = null;
/**
* The URL to the object rules.
*/
private URL objectRulesXml = null;
/**
* The URL of the schema.
*/
private URL schemaUri = null;
/**
* Whether or not validation should be turned on by default when using this factory to load XML configuration
* files.
*/
private static boolean defaultValidating = true;
/**
* Cache containing XmlObjectFactory instances which have already been configured for given objectRulesXml
*/
private static final Map<Class, XmlObjectFactory> factoryCache = new HashMap<Class, XmlObjectFactory>();
/**
* Creates an instance of this XmlObjectFactory with the given <code>objectRulesXml</code>
*
* @param objectRulesXml URL to the XML file defining the digester rules for object
*/
private XmlObjectFactory(final URL objectRulesXml)
{
ExceptionUtils.checkNull(
"objectRulesXml",
objectRulesXml);
this.digester = DigesterLoader.createDigester(objectRulesXml);
this.digester.setUseContextClassLoader(true);
}
/**
* Gets an instance of this XmlObjectFactory using the digester rules belonging to the <code>objectClass</code>.
*
* @param objectClass the Class of the object from which to configure this factory.
* @return the XmlObjectFactoy instance.
*/
public static XmlObjectFactory getInstance(final Class objectClass)
{
ExceptionUtils.checkNull(
"objectClass",
objectClass);
XmlObjectFactory factory = factoryCache.get(objectClass);
if (factory == null)
{
final URL objectRulesXml =
XmlObjectFactory.class.getResource('/' + objectClass.getName().replace(
'.',
'/') + RULES_SUFFIX);
if (objectRulesXml == null)
{
throw new XmlObjectFactoryException("No configuration rules found for class --> '" + objectClass + '\'');
}
factory = new XmlObjectFactory(objectRulesXml);
factory.objectClass = objectClass;
factory.objectRulesXml = objectRulesXml;
factory.setValidating(defaultValidating);
factoryCache.put(
objectClass,
factory);
}
return factory;
}
/**
* Allows us to set default validation to true/false for all instances of objects instantiated by this factory. This
* is necessary in some cases where the underlying parser doesn't support schema validation (such as when performing
* JUnit tests)
*
* @param validating true/false
*/
public static void setDefaultValidating(final boolean validating)
{
defaultValidating = validating;
}
/**
* Sets whether or not the XmlObjectFactory should be validating, default is <code>true</code>. If it IS set to be
* validating, then there needs to be a schema named objectClass.xsd in the same package as the objectClass that
* this factory was created from.
*
* @param validating true/false
*/
public void setValidating(final boolean validating)
{
this.digester.setValidating(validating);
if (validating)
{
if (this.schemaUri == null)
{
final String schemaLocation = '/' + this.objectClass.getName().replace(
'.',
'/') + SCHEMA_SUFFIX;
this.schemaUri = XmlObjectFactory.class.getResource(schemaLocation);
InputStream stream = null;
try
{
if (this.schemaUri != null)
{
stream = this.schemaUri.openStream();
IOUtils.closeQuietly(stream);
}
}
catch (final IOException exception)
{
this.schemaUri = null;
}
finally
{
if (stream != null)
{
IOUtils.closeQuietly(stream);
}
}
if (this.schemaUri == null)
{
logger.warn(
"WARNING! Was not able to find schemaUri --> '" + schemaLocation +
"' continuing in non validating mode");
}
}
if (this.schemaUri != null)
{
try
{
this.digester.setProperty("http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation", this.schemaUri.toString());
this.digester.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", XMLConstants.W3C_XML_SCHEMA_NS_URI);
this.digester.setErrorHandler(new XmlObjectValidator());
}
catch (final Exception exception)
{
logger.warn(
"WARNING! Your parser does NOT support the " +
" schema validation continuing in non validation mode",
exception);
}
}
}
}
/**
* Returns a configured Object based on the objectXml configuration file
*
* @param objectXml the path to the Object XML config file.
* @return Object the created instance.
*/
public Object getObject(final URL objectXml)
{
return this.getObject(
objectXml != null ? ResourceUtils.getContents(objectXml) : null,
objectXml);
}
/**
* Returns a configured Object based on the objectXml configuration reader.
*
* @param objectXml the path to the Object XML config file.
* @return Object the created instance.
*/
public Object getObject(final Reader objectXml)
{
return getObject(ResourceUtils.getContents(objectXml));
}
/**
* Returns a configured Object based on the objectXml configuration file passed in as a String.
*
* @param objectXml the path to the Object XML config file.
* @return Object the created instance.
*/
public Object getObject(String objectXml)
{
return this.getObject(
objectXml,
null);
}
/**
* Returns a configured Object based on the objectXml configuration file passed in as a String.
*
* @param objectXml the path to the Object XML config file.
* @param resource the resource from which the objectXml was retrieved (this is needed to resolve
* any relative references; like XML entities).
* @return Object the created instance.
*/
public Object getObject(
String objectXml,
final URL resource)
{
ExceptionUtils.checkNull(
"objectXml",
objectXml);
Object object = null;
try
{
this.digester.setEntityResolver(new XmlObjectEntityResolver(resource));
object = this.digester.parse(new StringReader(objectXml));
objectXml = null;
if (object == null)
{
final String message =
"Was not able to instantiate an object using objectRulesXml '" + this.objectRulesXml +
"' with objectXml '" + objectXml + "', please check either the objectXml " +
"or objectRulesXml file for inconsistencies";
throw new XmlObjectFactoryException(message);
}
}
catch (final SAXException exception)
{
final Throwable cause = ExceptionUtils.getRootCause(exception);
logger.warn("SAXException " + schemaUri, cause);
if (cause instanceof SAXException)
{
final String message =
"VALIDATION FAILED for --> '" + objectXml + "' against SCHEMA --> '" + this.schemaUri +
"' --> message: '" + exception.getMessage() + '\'';
throw new XmlObjectFactoryException(message);
}
throw new XmlObjectFactoryException(cause);
}
catch (final Throwable throwable)
{
final String message = "XML resource could not be loaded --> '" + objectXml + '\'';
throw new XmlObjectFactoryException(message, throwable);
}
return object;
}
/**
* Handles the validation errors.
*/
static final class XmlObjectValidator
implements ErrorHandler
{
/**
* @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
*/
public final void error(final SAXParseException exception)
throws SAXException
{
throw new SAXException(getMessage(exception));
}
/**
* @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
*/
public final void fatalError(final SAXParseException exception)
throws SAXException
{
throw new SAXException(getMessage(exception));
}
/**
* @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
*/
public final void warning(final SAXParseException exception)
{
logger.warn("WARNING!: " + getMessage(exception));
}
/**
* Constructs and returns the appropriate error message.
*
* @param exception the exception from which to extract the message.
* @return the message.
*/
@SuppressWarnings("static-method")
private String getMessage(final SAXParseException exception)
{
final StringBuilder message = new StringBuilder(100);
message.append(exception.getMessage());
message.append(", line: ");
message.append(exception.getLineNumber());
message.append(", column: ").append(exception.getColumnNumber());
return message.toString();
}
}
/**
* The prefix that the systemId should start with when attempting
* to resolve it within a jar.
*/
private static final String SYSTEM_ID_FILE = "file:";
/**
* Provides the resolution of external entities from the classpath.
*/
private static final class XmlObjectEntityResolver
implements EntityResolver
{
private URL xmlResource;
XmlObjectEntityResolver(final URL xmlResource)
{
this.xmlResource = xmlResource;
}
/**
* @see org.xml.sax.EntityResolver#resolveEntity(String, String)
*/
public InputSource resolveEntity(
final String publicId,
final String systemId)
throws SAXException, IOException
{
InputSource source = null;
if (this.xmlResource != null)
{
String path = systemId;
if (path != null && path.startsWith(SYSTEM_ID_FILE))
{
final String xmlResource = this.xmlResource.toString();
path = path.replaceFirst(
SYSTEM_ID_FILE,
"");
// - remove any extra starting slashes
path = ResourceUtils.normalizePath(path);
// - if we still have one starting slash, remove it
if (path.startsWith("/"))
{
path = path.substring(
1,
path.length());
}
final String xmlResourceName = xmlResource.replaceAll(
".*(\\+|/)",
"");
URL uri = null;
InputStream inputStream = null;
try
{
uri = ResourceUtils.toURL(StringUtils.replace(
xmlResource,
xmlResourceName,
path));
if (uri != null)
{
inputStream = uri.openStream();
}
}
catch (final IOException exception)
{
// - ignore
}
if (inputStream == null)
{
try
{
uri = ResourceUtils.getResource(path);
if (uri != null)
{
inputStream = uri.openStream();
}
}
catch (final IOException exception)
{
// - ignore
}
}
if (inputStream != null && uri != null)
{
source = new InputSource(inputStream);
source.setPublicId(publicId);
source.setSystemId(uri.toString());
}
// TODO Close InputStream while still returning a valid InputSource
}
}
return source;
}
}
/**
* @see Object#toString()
*/
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append(super.toString()).append(" [digester=").append(this.digester)
.append(", objectClass=").append(this.objectClass).append(", objectRulesXml=")
.append(this.objectRulesXml).append(", schemaUri=").append(this.schemaUri)
.append("]");
return builder.toString();
}
}