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}