001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package org.atteo.evo.config; 015 016import java.io.File; 017import java.io.FileOutputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.lang.reflect.Field; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Properties; 025import java.util.Set; 026 027import javax.validation.ConstraintViolation; 028import javax.validation.Validation; 029import javax.validation.Validator; 030import javax.validation.ValidatorFactory; 031import javax.xml.bind.Binder; 032import javax.xml.bind.JAXBContext; 033import javax.xml.bind.JAXBException; 034import javax.xml.bind.SchemaOutputResolver; 035import javax.xml.bind.UnmarshalException; 036import javax.xml.bind.Unmarshaller; 037import javax.xml.bind.ValidationEvent; 038import javax.xml.bind.ValidationEventHandler; 039import javax.xml.bind.annotation.XmlElementRef; 040import javax.xml.parsers.DocumentBuilder; 041import javax.xml.parsers.DocumentBuilderFactory; 042import javax.xml.parsers.ParserConfigurationException; 043import javax.xml.transform.Result; 044import javax.xml.transform.TransformerConfigurationException; 045import javax.xml.transform.TransformerFactory; 046import javax.xml.transform.sax.SAXResult; 047import javax.xml.transform.sax.SAXTransformerFactory; 048import javax.xml.transform.sax.TransformerHandler; 049import javax.xml.transform.stream.StreamResult; 050 051import org.atteo.evo.classindex.ClassIndex; 052import org.atteo.evo.filtering.CompoundPropertyResolver; 053import org.atteo.evo.filtering.Filtering; 054import org.atteo.evo.filtering.PropertiesPropertyResolver; 055import org.atteo.evo.filtering.PropertyNotFoundException; 056import org.atteo.evo.filtering.PropertyResolver; 057import org.atteo.evo.jaxb.FilteringAnnotationReader; 058import org.atteo.evo.jaxb.JaxbBindings; 059import org.atteo.evo.jaxb.ScopedIdResolver; 060import org.atteo.evo.xmlmerge.CombineChildren; 061import org.atteo.evo.xmlmerge.CombineSelf; 062import org.atteo.evo.xmlmerge.XmlCombiner; 063import org.w3c.dom.Document; 064import org.w3c.dom.Element; 065import org.w3c.dom.Node; 066import org.xml.sax.ErrorHandler; 067import org.xml.sax.SAXException; 068import org.xml.sax.SAXParseException; 069 070import com.google.common.collect.Iterables; 071import com.sun.xml.bind.IDResolver; 072import com.sun.xml.bind.api.AccessorException; 073import com.sun.xml.bind.api.JAXBRIContext; 074import com.sun.xml.bind.v2.model.runtime.RuntimeNonElement; 075import com.sun.xml.bind.v2.runtime.IllegalAnnotationsException; 076import com.sun.xml.bind.v2.runtime.JAXBContextImpl; 077 078/** 079 * Generic configuration facility based on JAXB. 080 * 081 * <h3>Overview</h3> 082 * <p> 083 * Evo Config opens one or more XML files, merges their content, filters them 084 * and then converts into the tree of objects using JAXB. 085 * </p> 086 * <h3>Defining XML schema</h3> 087 * <p> 088 * To use Evo Config you need to define the schema for your configuration file. 089 * This is achieved by creating a number of classes which extend {@link Configurable} abstract class 090 * and annotating them with 091 * <a href="http://jaxb.java.net/2.2.6/docs/ch03.html#annotating-your-classes">JAXB annotations</a>. 092 * Let's start by defining classes for generic service and some specific database service: 093 * <pre> 094 * {@code 095 * abstract class Service extends Configurable { 096 * public abstract void start(); 097 * } 098 * 099 *. @XmlRootElement(name = "database") 100 * class Database extends Service { 101 *. @XmlElement 102 * private String url; 103 * 104 * public void start() { 105 * System.out.println("Connecting to database: " + url); 106 * } 107 * } 108 * } 109 * </pre> 110 * </p> 111 * <p> 112 * Also let's create the class which will define root of the configuration schema. Here the common idiom 113 * is to create field of type {@link List} annotated with {@link XmlElementRef}. Using this JAXB 114 * will be able to unmarshal any subclass of list element type. In this way the schema is open-ended, 115 * allowing anyone to implement our Service. There is no need to list all the implementations: 116 * <pre> 117 * {@code 118 *. @XmlRootElement(name = "config") 119 * class Config extends Configurable { 120 *. @XmlElementRef 121 *. @XmlElementWrapper(name = "services") 122 *. @Valid 123 * private List<Service> services; 124 * } 125 * } 126 * </pre> 127 * </p> 128 * <p> 129 * The above schema will match the following XML: 130 * 131 * <pre> 132 * {@code 133 * <config> 134 * <services> 135 * <database> 136 * <url>jdbc:h2:file:/data/sample</url> 137 * </database> 138 * <database> 139 * <url>jdbc:h2:tcp://localhost/~/test</url> 140 * </database> 141 * </services> 142 * </config> 143 * } 144 * </pre> 145 * </p> 146 * </p> 147 * 148 * <h3>Reading configuration files</h3> 149 * 150 * <pre> 151 * Configuration configuration = new Configuration(); 152 * configuration.combine("first.xml"); 153 * configuration.combine("second.xml"); 154 * configuration.filter(properties); 155 * Root root = configuration.read(Root.class); 156 * </pre> 157 * </p> 158 * <p> 159 * The following actions will be performed: 160 * <ul> 161 * <li>{@link JAXBContext} will be created for all the classes extending {@link Configurable}, 162 * those classes are indexed at compile-time using {@link ClassIndex} facility,</li> 163 * <li>provided XML files will be parsed and combined using {@link XmlCombiner} facility,</li> 164 * <li>any property references in the form of <code>${name}</code> will be substituted 165 * with the value using registered {@link PropertyResolver}, see {@link Filtering} for details,</li> 166 * <li>the result will be unmarshalled using {@link Unmarshaller JAXB} into provided root class,</li> 167 * <li>finally the unmarshalled object tree will be validated using JSR 303 168 * - {@link Validation Bean Validation framework}.</li> 169 * </ul> 170 * </p> 171 */ 172public class Configuration { 173 private JAXBContext context; 174 private Binder<Node> binder; 175 private final Iterable<Class<? extends Configurable>> klasses; 176 private DocumentBuilder builder; 177 private Document document; 178 private PropertyResolver propertyResolver; 179 180 /** 181 * Create Configuration by discovering all {@link Configurable}s. 182 * 183 * <p> 184 * Uses {@link ClassIndex#getSubclasses(Class)} to get {@link Configurable}s. 185 * </p> 186 */ 187 public Configuration() { 188 this(ClassIndex.getSubclasses(Configurable.class)); 189 } 190 191 /** 192 * Create Configuration by manually specifying all {@link Configurable}s. 193 * @param klasses list of {@link Configurable} classes. 194 * @throws JAXBException when JAXB context creation fails 195 */ 196 public Configuration(Iterable<Class<? extends Configurable>> klasses) { 197 this.klasses = klasses; 198 this.propertyResolver = new PropertiesPropertyResolver(new Properties()); 199 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 200 try { 201 builder = factory.newDocumentBuilder(); 202 // register null error handler, fatal errors will be reported with exception anyway 203 builder.setErrorHandler(new ErrorHandler() { 204 @Override 205 public void warning(SAXParseException exception) throws SAXException { 206 } 207 @Override 208 public void error(SAXParseException exception) throws SAXException { 209 } 210 @Override 211 public void fatalError(SAXParseException exception) throws SAXException { 212 } 213 }); 214 context = JAXBContext.newInstance(Iterables.toArray(klasses, Class.class)); 215 binder = context.createBinder(); 216 binder.setProperty(IDResolver.class.getName(), new ScopedIdResolver()); 217 binder.setEventHandler(new ValidationEventHandler() { 218 @Override 219 public boolean handleEvent(ValidationEvent event) { 220 return true; 221 } 222 }); 223 document = builder.newDocument(); 224 } catch (ParserConfigurationException e) { 225 throw new RuntimeException("Cannot configure XML parser", e); 226 } catch (IllegalAnnotationsException e) { 227 throw new RuntimeException("Cannot configure unmarshaller: " + e.toString()); 228 } catch (JAXBException e) { 229 throw new RuntimeException("Cannot configure unmarshaller", e); 230 } 231 } 232 233 /** 234 * Generate an XSD schema for the configuration file. 235 * @param filename file to store the schema to 236 * @throws IOException when IO error occurs 237 */ 238 public void generateSchema(final File filename) throws IOException { 239 context.generateSchema(new SchemaOutputResolver() { 240 @Override 241 public Result createOutput(String namespaceUri, String suggestedFileName) 242 throws IOException { 243 // We should just call: 244 // return new StreamResult(filename); 245 // but this does not work due to the https://java.net/jira/browse/JAXB-974 246 try { 247 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 248 TransformerHandler transformer = factory.newTransformerHandler(); 249 transformer.setResult(new StreamResult(new FileOutputStream(filename))); 250 SAXResult saxResult = new SAXResult(transformer); 251 saxResult.setSystemId("dummy"); 252 return saxResult; 253 } catch (TransformerConfigurationException e) { 254 throw new RuntimeException(e); 255 } 256 } 257 }); 258 } 259 260 /** 261 * Filter {@code ${name}} placeholders using given properties. 262 * <p> 263 * This method wraps given properties into {@link PropertiesPropertyResolver} 264 * and calls {@link #filter(PropertyResolver)}. 265 * </p> 266 * @param properties properties to filter into configuration files 267 */ 268 public void filter(Properties properties) throws IncorrectConfigurationException { 269 filter(new PropertiesPropertyResolver(properties)); 270 } 271 272 /** 273 * Filter {@code ${name}} placeholders using values from given {@link PropertyResolver}. 274 * 275 * @param resolver property resolver used for filtering the configuration files 276 * 277 * @see CompoundPropertyResolver 278 */ 279 public void filter(PropertyResolver resolver) throws IncorrectConfigurationException { 280 propertyResolver = resolver; 281 if (document.getDocumentElement() == null) { 282 return; 283 } 284 try { 285 Filtering.filter(document.getDocumentElement(), resolver); 286 } catch (PropertyNotFoundException e) { 287 throw new IncorrectConfigurationException("Cannot resolve configuration properties: " 288 + e.getMessage(), e); 289 } 290 } 291 292 /** 293 * Parse an XML file and combine it with the currently stored DOM tree. 294 * @param stream stream with the XML file 295 * @throws IncorrectConfigurationException when configuration file is invalid 296 * @throws IOException when the stream cannot be read 297 */ 298 public void combine(InputStream stream) throws IncorrectConfigurationException, IOException { 299 Document parentDocument = document; 300 301 try { 302 document = builder.parse(stream); 303 304 // Unmarshall the parent document to assign combine attributes annotated on classes 305 Element root = parentDocument.getDocumentElement(); 306 if (root != null) { 307 binder.unmarshal(root); 308 JaxbBindings.iterate(root, binder, new CombineAssigner()); 309 310 // Combine with parent 311 XmlCombiner combiner = new XmlCombiner(builder); 312 combiner.combine(parentDocument); 313 combiner.combine(document); 314 document = combiner.buildDocument(); 315 } 316 } catch (UnmarshalException e) { 317 if (e.getLinkedException() != null) { 318 throw new IncorrectConfigurationException("Cannot parse configuration file: " 319 + e.getLinkedException().getMessage(), e.getLinkedException()); 320 } else { 321 throw new RuntimeException("Cannot parse configuration file", e); 322 } 323 } catch (JAXBException e) { 324 throw new IncorrectConfigurationException("Unmarshall error: " + e.getMessage(), e); 325 } catch (SAXException e) { 326 throw new IncorrectConfigurationException("Parse error: " + e.getMessage(), e); 327 } 328 } 329 330 /** 331 * Unmarshals stored configuration DOM tree as object of the given class. 332 * @param rootClass the class to which unmarshal the DOM tree 333 * @param <T> type of the rootClass 334 * @return unmarshalled class tree, or null if no streams were provided 335 * @throws IncorrectConfigurationException if configuration is incorrect 336 */ 337 public <T extends Configurable> T read(Class<T> rootClass) throws IncorrectConfigurationException { 338 if (document.getDocumentElement() == null) { 339 return null; 340 } 341 T result; 342 final StringBuilder errors = new StringBuilder(); 343 try { 344 Map<String, Object> map = new HashMap<>(); 345 map.put(JAXBRIContext.ANNOTATION_READER, new FilteringAnnotationReader(propertyResolver)); 346 context = JAXBContext.newInstance(Iterables.toArray(klasses, Class.class), map); 347 binder = context.createBinder(); 348 binder.setProperty(IDResolver.class.getName(), new ScopedIdResolver()); 349 binder.setEventHandler(new ValidationEventHandler() { 350 @Override 351 public boolean handleEvent(ValidationEvent event) { 352 errors.append("\n At line ").append(event.getLocator().getLineNumber()) 353 .append(": ").append(event.getMessage()); 354 return false; 355 } 356 }); 357 result = rootClass.cast(binder.unmarshal(document.getDocumentElement())); 358 JaxbBindings.iterate(document.getDocumentElement(), binder, 359 new DefaultsSetter(context, propertyResolver)); 360 361 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); 362 Validator validator = validatorFactory.getValidator(); 363 Set<ConstraintViolation<T>> violations = validator.validate(result); 364 if (!violations.isEmpty()) { 365 for (ConstraintViolation<T> violation : violations) { 366 errors.append(" Error at: ").append(violation.getPropertyPath()).append("\n") 367 .append(" for value: ").append(violation.getInvalidValue()).append("\n") 368 .append(" with message: ").append(violation.getMessage()); 369 } 370 throw new IncorrectConfigurationException("Constraints violation:" + errors.toString()); 371 } 372 } catch (UnmarshalException e) { 373 if (e.getLinkedException() != null) { 374 throw new IncorrectConfigurationException("Parse error: " + e.getLinkedException().getMessage(), 375 e.getLinkedException()); 376 } else if (errors.length() > 0) { 377 throw new IncorrectConfigurationException("Parse error: " + errors.toString(), e); 378 } else { 379 throw new IncorrectConfigurationException("Parse error:" + e.getMessage(), e); 380 } 381 } catch (JAXBException e) { 382 throw new IncorrectConfigurationException("Cannot unmarshall configuration file", e); 383 } 384 385 return result; 386 } 387 388 /** 389 * Get root XML {@link Element} of the combined configuration file. 390 * @return root {@link Element} 391 */ 392 public Element getRootElement() { 393 return document.getDocumentElement(); 394 } 395 396 /** 397 * Combines several input XML documents and reads the configuration from it. 398 * 399 * <p> 400 * This is equivalent to executing {@link #combine(InputStream)} for each stream 401 * and then {@link #read(Class)} for rootClass. 402 * </p> 403 * @param rootClass the class to which unmarshal the DOM tree 404 * @param <T> type of the rootClass 405 * @param streams input streams with the configuration to combine 406 * @return unmarshalled class tree, or null if no streams were provided 407 * @throws IncorrectConfigurationException if configuration is incorrect 408 * @throws IOException when cannot access configuration files 409 */ 410 public <T extends Configurable> T read(Class<T> rootClass, InputStream... streams) 411 throws IncorrectConfigurationException, IOException { 412 for (InputStream stream : streams) { 413 combine(stream); 414 } 415 return read(rootClass); 416 } 417 418 419 private static class CombineAssigner implements JaxbBindings.Runnable { 420 /** 421 * Assigns {@link Configurable#combine} from the fields or class this objects unmarshals to. 422 * @param element DOM element 423 * @param object object into which DOM element was unmarshalled 424 * @param field field which holds unmarshalled object 425 */ 426 @Override 427 public void run(Element element, Object object, Field field) { 428 if (field != null) { 429 setAttributesFromAnnotation(element, field.getAnnotation(XmlCombine.class)); 430 } 431 432 if (object != null) { 433 setAttributesFromAnnotation(element, object.getClass().getAnnotation(XmlCombine.class)); 434 } 435 } 436 437 private void setAttributesFromAnnotation(Element element, XmlCombine annotation) { 438 if (annotation != null) { 439 if (!element.hasAttribute(CombineSelf.ATTRIBUTE_NAME)) { 440 CombineSelf combineSelf = annotation.self(); 441 if (combineSelf != null) { 442 element.setAttribute(CombineSelf.ATTRIBUTE_NAME, combineSelf.name()); 443 } 444 } 445 if (!element.hasAttribute(CombineChildren.ATTRIBUTE_NAME)) { 446 CombineChildren combineChildren = annotation.children(); 447 if (combineChildren != null) { 448 element.setAttribute(CombineChildren.ATTRIBUTE_NAME, combineChildren.name()); 449 } 450 } 451 } 452 } 453 } 454 455 private static class DefaultsSetter implements JaxbBindings.Runnable { 456 private JAXBRIContext context; 457 private PropertyResolver properties; 458 459 public DefaultsSetter(JAXBContext context, PropertyResolver properties) { 460 this.context = (JAXBContextImpl) context; 461 this.properties = properties; 462 } 463 464 @Override 465 public void run(Element element, Object object, Field field) { 466 Class<?> klass = object.getClass(); 467 468 while (klass != Object.class) { 469 for (Field f : klass.getDeclaredFields()) { 470 XmlDefaultValue defaultValue = f.getAnnotation(XmlDefaultValue.class); 471 if (defaultValue != null) { 472 if (f.getType().isPrimitive()) { 473 throw new RuntimeException("@XmlDefaultValue cannot be specified on primitive type: " 474 + klass.getCanonicalName() + "." + f.getName()); 475 } 476 477 boolean accessible = f.isAccessible(); 478 f.setAccessible(true); 479 try { 480 if (f.get(object) != null) { 481 continue; 482 } 483 } catch (IllegalArgumentException | IllegalAccessException e) { 484 throw new RuntimeException(e); 485 } 486 487 String value = defaultValue.value(); 488 try { 489 value = Filtering.filter(value, properties); 490 } catch (PropertyNotFoundException e) { 491 if (field != null) { 492 throw new RuntimeException("Property not found for field '" 493 + field.getName() + "'", e); 494 } else { 495 throw new RuntimeException("Property not found", e); 496 } 497 } 498 RuntimeNonElement typeInfo = context.getRuntimeTypeInfoSet().getTypeInfo(f.getType()); 499 Object v; 500 try { 501 v = typeInfo.getTransducer().parse(value); 502 } catch (AccessorException | SAXException e) { 503 throw new RuntimeException(e); 504 } 505 506 try { 507 f.set(object, v); 508 } catch (IllegalArgumentException | IllegalAccessException e) { 509 throw new RuntimeException(e); 510 } 511 f.setAccessible(accessible); 512 } 513 } 514 515 klass = klass.getSuperclass(); 516 } 517 } 518 } 519}