001package org.andromda.core.common; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.Reader; 006import java.io.StringReader; 007import java.net.URL; 008import java.util.HashMap; 009import java.util.Map; 010import javax.xml.XMLConstants; 011import org.apache.commons.digester.Digester; 012import org.apache.commons.digester.xmlrules.DigesterLoader; 013import org.apache.commons.io.IOUtils; 014import org.apache.commons.lang.StringUtils; 015import org.apache.log4j.Logger; 016import org.xml.sax.EntityResolver; 017import org.xml.sax.ErrorHandler; 018import org.xml.sax.InputSource; 019import org.xml.sax.SAXException; 020import 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 separates 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 its 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 * @author Bob Fields 050 * @author Michail Plushnikov 051 */ 052public class XmlObjectFactory 053{ 054 /** 055 * The class logger. Note: visibility is protected to improve access within {@link XmlObjectValidator} 056 */ 057 protected static final Logger logger = Logger.getLogger(XmlObjectFactory.class); 058 059 /** 060 * The expected suffixes for rule files. 061 */ 062 private static final String RULES_SUFFIX = "-Rules.xml"; 063 064 /** 065 * The expected suffix for XSD files. 066 */ 067 private static final String SCHEMA_SUFFIX = ".xsd"; 068 069 /** 070 * The digester instance. 071 */ 072 private Digester digester = null; 073 074 /** 075 * The class of which the object we're instantiating. 076 */ 077 private Class objectClass = null; 078 079 /** 080 * The URL to the object rules. 081 */ 082 private URL objectRulesXml = null; 083 084 /** 085 * The URL of the schema. 086 */ 087 private URL schemaUri = null; 088 089 /** 090 * Whether or not validation should be turned on by default when using this factory to load XML configuration 091 * files. 092 */ 093 private static boolean defaultValidating = true; 094 095 /** 096 * Cache containing XmlObjectFactory instances which have already been configured for given objectRulesXml 097 */ 098 private static final Map<Class, XmlObjectFactory> factoryCache = new HashMap<Class, XmlObjectFactory>(); 099 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}