org.atteo.config.Configuration.java Source code

Java tutorial

Introduction

Here is the source code for org.atteo.config.Configuration.java

Source

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.atteo.config;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.xml.bind.Binder;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.atteo.classindex.ClassFilter;
import org.atteo.classindex.ClassIndex;
import org.atteo.config.jaxb.FilteringAnnotationReader;
import org.atteo.config.jaxb.JaxbBindings;
import org.atteo.filtering.CompoundPropertyResolver;
import org.atteo.filtering.Filtering;
import org.atteo.filtering.PropertiesPropertyResolver;
import org.atteo.filtering.PropertyFilter;
import org.atteo.filtering.PropertyNotFoundException;
import org.atteo.filtering.PropertyResolver;
import org.atteo.xmlcombiner.CombineChildren;
import org.atteo.xmlcombiner.CombineSelf;
import org.atteo.xmlcombiner.XmlCombiner;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.JAXBContextProperties;
import org.eclipse.persistence.jaxb.JAXBHelper;
import org.eclipse.persistence.jaxb.javamodel.reflection.AnnotationHelper;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.google.common.collect.Iterables;

/**
 * Generic configuration facility based on JAXB.
 *
 * <h3>Overview</h3>
 * <p>
 * Atteo Config opens one or more XML files, merges their content, filters them
 * and then converts into the tree of objects using JAXB.
 * </p>
 * <h3>Defining XML schema</h3>
 * <p>
 * To use Atteo Config you need to define the schema for your configuration file.
 * This is achieved by creating a number of classes which extend {@link Configurable} abstract class
 * and annotating them with
 * <a href="http://jaxb.java.net/2.2.6/docs/ch03.html#annotating-your-classes">JAXB annotations</a>.
 * Let's start by defining classes for generic service and some specific database service:
 * <pre>
 * {@code
 * abstract class Service extends Configurable {
 *   public abstract void start();
 * }
 *
 *. @XmlRootElement(name = "database")
 * class Database extends Service {
 *.  @XmlElement
 *   private String url;
 *
 *   public void start() {
 *     System.out.println("Connecting to database: " + url);
 *   }
 * }
 * }
 * </pre>
 * </p>
 * <p>
 * Also let's create the class which will define root of the configuration schema. Here the common idiom
 * is to create field of type {@link List} annotated with {@link XmlElementRef}. Using this JAXB
 * will be able to unmarshal any subclass of list element type. In this way the schema is open-ended,
 * allowing anyone to implement our Service. There is no need to list all the implementations:
 * <pre>
 * {@code
 *. @XmlRootElement(name = "config")
 *  class Config extends Configurable {
 *.    @XmlElementRef
 *.    @XmlElementWrapper(name = "services")
 *.    @Valid
 *     private List<Service> services;
 * }
 * }
 * </pre>
 * </p>
 * <p>
 * The above schema will match the following XML:
 *
 * <pre>
 * {@code
 * <config>
 *   <services>
 *     <database>
 *       <url>jdbc:h2:file:/data/sample</url>
 *     </database>
 *     <database>
 *       <url>jdbc:h2:tcp://localhost/~/test</url>
 *     </database>
 *   </services>
 * </config>
 * }
 * </pre>
 * </p>
 * </p>
 *
 * <h3>Reading configuration files</h3>
 *
 * <pre>
 *    Configuration configuration = new Configuration();
 *    configuration.combine("first.xml");
 *    configuration.combine("second.xml");
 *    configuration.filter(properties);
 *    Root root = configuration.read(Root.class);
 * </pre>
 * </p>
 * <p>
 * The following actions will be performed:
 * <ul>
 * <li>{@link JAXBContext} will be created for all the classes extending {@link Configurable},
 * those classes are indexed at compile-time using {@link ClassIndex} facility,</li>
 * <li>provided XML files will be parsed and combined using {@link XmlCombiner} facility,</li>
 * <li>any property references in the form of <code>${name}</code> will be substituted
 * with the value using registered {@link PropertyResolver}, see {@link Filtering} for details,</li>
 * <li>the result will be unmarshalled using {@link Unmarshaller JAXB} into provided root class,</li>
 * <li>finally the unmarshalled object tree will be validated using JSR 303
 * - {@link Validation Bean Validation framework}.</li>
 * </ul>
 * </p>
 */
public class Configuration {
    private JAXBContext context;
    private Binder<Node> binder;
    private final Iterable<Class<? extends Configurable>> klasses;
    private DocumentBuilder builder;
    private Document document;
    private PropertyFilter propertyFilter;
    //private RuntimeAnnotationReader annotationReader = new RuntimeInlineAnnotationReader();

    /**
     * Create Configuration by discovering all {@link Configurable}s.
     *
     * <p>
     * Uses {@link ClassIndex#getSubclasses(Class)} to get list of top-level classes implementing {@link Configurable}
     * interface.
     * </p>
     */
    public Configuration() {
        this(ClassFilter.only().topLevel().from(ClassIndex.getSubclasses(Configurable.class)));
    }

    /**
     * Create Configuration by manually specifying all {@link Configurable}s.
     * @param klasses list of {@link Configurable} classes.
     * @throws JAXBException when JAXB context creation fails
     */
    public Configuration(Iterable<Class<? extends Configurable>> klasses) {
        this.klasses = klasses;
        propertyFilter = Filtering.getFilter((PropertyResolver) (String name, PropertyFilter filter) -> {
            throw new PropertyNotFoundException(name);
        });
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            builder = factory.newDocumentBuilder();
            // register null error handler, fatal errors will be reported with exception anyway
            builder.setErrorHandler(new ErrorHandler() {
                @Override
                public void warning(SAXParseException exception) throws SAXException {
                }

                @Override
                public void error(SAXParseException exception) throws SAXException {
                }

                @Override
                public void fatalError(SAXParseException exception) throws SAXException {
                }
            });
            context = JAXBContextFactory.createContext(Iterables.toArray(klasses, Class.class),
                    Collections.emptyMap());
            binder = context.createBinder();
            // JAXB Moxy does not allow to set resolver on binder
            //         binder.setProperty(UnmarshallerProperties.ID_RESOLVER, new ScopedIdResolver());
            binder.setEventHandler((ValidationEvent event) -> true);
            document = builder.newDocument();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException("Cannot configure XML parser", e);
        } catch (JAXBException e) {
            throw new RuntimeException("Cannot configure unmarshaller", e);
        }
    }

    /**
     * Generate an XSD schema for the configuration file.
     * @param filename file to store the schema to
     * @throws IOException when IO error occurs
     */
    public void generateSchema(final File filename) throws IOException {
        context.generateSchema(new SchemaOutputResolver() {
            @Override
            public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException {
                // We should just call:
                //     return new StreamResult(filename);
                // but this does not work due to the https://java.net/jira/browse/JAXB-974
                try {
                    SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
                    TransformerHandler transformer = factory.newTransformerHandler();
                    transformer.setResult(new StreamResult(new FileOutputStream(filename)));
                    SAXResult saxResult = new SAXResult(transformer);
                    saxResult.setSystemId("dummy");
                    return saxResult;
                } catch (TransformerConfigurationException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    /**
     * Filter {@code ${name}} placeholders using given properties.
     * <p>
     * This method wraps given properties into {@link PropertiesPropertyResolver}
     * and calls {@link #filter(PropertyResolver)}.
     * </p>
     * @param properties properties to filter into configuration files
     */
    public void filter(Properties properties) throws IncorrectConfigurationException {
        filter(new PropertiesPropertyResolver(properties));
    }

    /**
     * Filter {@code ${name}} placeholders using values from given {@link PropertyResolver}.
     *
     * @param resolver property resolver used for filtering the configuration files
     *
     * @see CompoundPropertyResolver
     */
    public void filter(PropertyResolver resolver) throws IncorrectConfigurationException {
        propertyFilter = Filtering.getFilter(resolver);
        if (document.getDocumentElement() == null) {
            return;
        }
        try {
            propertyFilter.filter(document.getDocumentElement());
        } catch (PropertyNotFoundException e) {
            throw new IncorrectConfigurationException("Cannot resolve configuration properties: " + e.getMessage(),
                    e);
        }
    }

    /**
     * Parse an XML file and combine it with the currently stored DOM tree.
     * @param stream stream with the XML file
     * @throws IncorrectConfigurationException when configuration file is invalid
     * @throws IOException when the stream cannot be read
     */
    public void combine(InputStream stream) throws IncorrectConfigurationException, IOException {
        Document parentDocument = document;

        try {
            document = builder.parse(stream);

            Element root = parentDocument.getDocumentElement();
            if (root != null) {
                // Combine with parent
                XmlCombiner combiner = new XmlCombiner(builder, "id");
                combiner.combine(parentDocument);
                combiner.combine(document);
                document = combiner.buildDocument();
            }
        } catch (SAXException e) {
            throw new IncorrectConfigurationException("Parse error: " + e.getMessage(), e);
        }
    }

    /**
     * Unmarshals stored configuration DOM tree as object of the given class.
     * @param rootClass the class to which unmarshal the DOM tree
     * @param <T> type of the rootClass
     * @return unmarshalled class tree, or null if no streams were provided
     * @throws IncorrectConfigurationException if configuration is incorrect
     */
    public <T extends Configurable> T read(Class<T> rootClass) throws IncorrectConfigurationException {
        if (document.getDocumentElement() == null) {
            return null;
        }
        T result;
        final StringBuilder errors = new StringBuilder();
        try {
            Map<String, Object> properties = new HashMap<>();
            AnnotationHelper helper = new FilteringAnnotationReader(propertyFilter);
            properties.put(JAXBContextProperties.ANNOTATION_HELPER, helper);
            context = JAXBContextFactory.createContext(Iterables.toArray(klasses, Class.class), properties);
            binder = context.createBinder();
            // JAXB Moxy does not allow to set resolver on binder
            //         binder.setProperty(UnmarshallerProperties.ID_RESOLVER, new ScopedIdResolver());
            binder.setEventHandler((ValidationEvent event) -> {
                if (event.getLocator().getLineNumber() != -1) {
                    errors.append("\n  At line ").append(event.getLocator().getLineNumber());
                } else if (event.getLocator().getNode() != null
                        && event.getLocator().getNode().getParentNode() != null) {
                    errors.append("\n  In <");
                    errors.append(event.getLocator().getNode().getParentNode().getNodeName());
                    errors.append(">");
                }

                errors.append(": ").append(event.getMessage());
                return false;
            });
            result = rootClass.cast(binder.unmarshal(document.getDocumentElement()));
            JaxbBindings.iterate(document.getDocumentElement(), binder,
                    new DefaultsSetter(context, propertyFilter));

            ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
            Validator validator = validatorFactory.getValidator();
            Set<ConstraintViolation<T>> violations = validator.validate(result);
            if (!violations.isEmpty()) {
                for (ConstraintViolation<T> violation : violations) {
                    errors.append("  Error at:   ").append(violation.getPropertyPath()).append("\n")
                            .append("    for value:   ").append(violation.getInvalidValue()).append("\n")
                            .append("    with message:    ").append(violation.getMessage());
                }
                throw new IncorrectConfigurationException("Constraints violation:" + errors.toString());
            }
        } catch (UnmarshalException e) {
            if (e.getLinkedException() != null) {
                throw new IncorrectConfigurationException("Parse error: " + e.getLinkedException().getMessage(),
                        e.getLinkedException());
            } else if (errors.length() > 0) {
                throw new IncorrectConfigurationException("Parse error: " + errors.toString(), e);
            } else {
                throw new IncorrectConfigurationException("Parse error:" + e.getMessage(), e);
            }
        } catch (JAXBException e) {
            throw new IncorrectConfigurationException("Cannot unmarshall configuration file", e);
        }

        return result;
    }

    /**
     * Get root XML {@link Element} of the combined configuration file.
     * @return root {@link Element}
     */
    public Element getRootElement() {
        return document.getDocumentElement();
    }

    private static class DefaultsSetter implements JaxbBindings.Runnable {
        private final JAXBContext context;
        private final PropertyFilter properties;

        public DefaultsSetter(JAXBContext context, PropertyFilter properties) {
            this.context = context;
            this.properties = properties;
        }

        @Override
        public void run(Element element, Object object, Field field) {
            Class<?> klass = object.getClass();

            while (klass != Object.class) {
                for (Field f : klass.getDeclaredFields()) {
                    XmlDefaultValue defaultValue = f.getAnnotation(XmlDefaultValue.class);
                    if (defaultValue != null) {
                        if (f.getType().isPrimitive()) {
                            throw new RuntimeException("@XmlDefaultValue cannot be specified on primitive type: "
                                    + klass.getCanonicalName() + "." + f.getName());
                        }

                        boolean accessible = f.isAccessible();
                        f.setAccessible(true);
                        try {
                            if (f.get(object) != null) {
                                continue;
                            }
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            throw new RuntimeException(e);
                        }

                        String value = defaultValue.value();
                        try {
                            value = properties.filter(value);
                        } catch (PropertyNotFoundException e) {
                            if (field != null) {
                                throw new RuntimeException("Property not found for field '" + field.getName() + "'",
                                        e);
                            } else {
                                throw new RuntimeException("Property not found", e);
                            }
                        }

                        AbstractSession session = JAXBHelper.getJAXBContext(context).getXMLContext()
                                .getSession(klass);
                        ClassDescriptor classDescriptor = session.getClassDescriptor(klass);
                        DatabaseMapping mapping = classDescriptor.getMappingForAttributeName(f.getName());
                        if (mapping == null) {
                            throw new RuntimeException("Field '" + f.getName() + "' cannot be annotated with" + " @"
                                    + XmlDefaultValue.class.getSimpleName() + ", because it is not mapped"
                                    + ", mark it with @" + XmlElement.class.getSimpleName());
                        }
                        mapping.setAttributeValueInObject(object, value);

                        f.setAccessible(accessible);

                        /**
                         * For reference, how it worked in JAXB RI:
                         *
                        RuntimeNonElement typeInfo = context.getRuntimeTypeInfoSet().getTypeInfo(f.getType());
                        Object v;
                        try {
                           v = typeInfo.getTransducer().parse(value);
                        } catch (AccessorException | SAXException e) {
                           throw new RuntimeException(e);
                        }
                            
                        try {
                           f.set(object, v);
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                           throw new RuntimeException(e);
                        }
                        */
                    }
                }

                klass = klass.getSuperclass();
            }
        }
    }
}