001/*
002 * Copyright 2013 Atteo.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.atteo.moonshine;
017
018import java.io.ByteArrayInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.Writer;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.nio.file.StandardOpenOption;
028
029import org.atteo.evo.config.Configuration;
030import org.atteo.evo.config.IncorrectConfigurationException;
031import org.atteo.evo.config.XmlUtils;
032import org.atteo.evo.filtering.CompoundPropertyResolver;
033import org.atteo.evo.filtering.EnvironmentPropertyResolver;
034import org.atteo.evo.filtering.OneOfPropertyResolver;
035import org.atteo.evo.filtering.PropertyResolver;
036import org.atteo.evo.filtering.SystemPropertyResolver;
037import org.atteo.evo.filtering.XmlPropertyResolver;
038import org.atteo.moonshine.directories.FileAccessor;
039import org.w3c.dom.Element;
040import org.w3c.dom.NodeList;
041
042import com.google.common.base.Charsets;
043
044public class ConfigurationReader {
045    public final static String SCHEMA_FILE_NAME = "schema.xsd";
046    public final static String CONFIG_FILE_NAME = "config.xml";
047    public final static String DEFAULT_CONFIG_RESOURCE_NAME = "/default-config.xml";
048
049    private final Configuration configuration = new Configuration();
050    private final CompoundPropertyResolver customPropertyResolvers = new CompoundPropertyResolver();
051    private PropertyResolver propertyResolver = null;
052    private final FileAccessor fileAccessor;
053
054    public ConfigurationReader(FileAccessor fileAccessor) {
055        this.fileAccessor = fileAccessor;
056    }
057
058    public void filter() throws IncorrectConfigurationException {
059        Element propertiesElement = null;
060        if (configuration.getRootElement() != null) {
061            NodeList nodesList = configuration.getRootElement().getElementsByTagName("properties");
062            if (nodesList.getLength() == 1) {
063                propertiesElement = (Element) nodesList.item(0);
064            }
065        }
066
067        propertyResolver = new CompoundPropertyResolver(
068                new OneOfPropertyResolver(),
069                new SystemPropertyResolver(),
070                new EnvironmentPropertyResolver(),
071                new XmlPropertyResolver(propertiesElement, false),
072                customPropertyResolvers,
073                new XmlPropertyResolver(configuration.getRootElement(), true));
074
075        configuration.filter(propertyResolver);
076    }
077
078    public Config read() throws IncorrectConfigurationException {
079        return configuration.read(Config.class);
080    }
081
082    public PropertyResolver getPropertyResolver() {
083        return propertyResolver;
084    }
085
086    /**
087     * Reads configuration from '/default-config.xml' resource.
088     */
089    public void combineDefaultConfiguration() {
090        try {
091            combineConfigurationFromResource(DEFAULT_CONFIG_RESOURCE_NAME, false);
092        } catch (IOException | IncorrectConfigurationException e) {
093            throw new RuntimeException(e);
094        }
095    }
096
097    /**
098     * Reads configuration from config.xml files found in ${configDirs} and ${configHome} directories.
099     * @throws IncorrectConfigurationException when configuration is incorrect
100     * @throws IOException when cannot read resource
101     */
102    public void combineConfigDirConfiguration() throws IncorrectConfigurationException, IOException {
103        for (Path path : fileAccessor.getConfigFiles(CONFIG_FILE_NAME)) {
104            try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) {
105                combineConfigurationFromStream(stream);
106            }
107        }
108    }
109
110    /**
111     * Reads configuration from given resource.
112     * @param resourcePath path to the resource
113     * @throws IncorrectConfigurationException when configuration is incorrect
114     * @throws IOException when cannot read resource
115     */
116    public void combineConfigurationFromResource(String resourcePath, boolean throwIfNotFound)
117            throws IncorrectConfigurationException, IOException {
118        // TODO: what if more than one resource with given name?
119        try(InputStream stream = getClass().getResourceAsStream(resourcePath)) {
120            if (stream != null) {
121                configuration.combine(stream);
122            } else if (throwIfNotFound) {
123                throw new RuntimeException("Configuration resource not found: " + resourcePath);
124            }
125        }
126    }
127
128    public void combineConfigurationFromStream(InputStream stream)
129            throws IncorrectConfigurationException, IOException {
130        configuration.combine(stream);
131    }
132
133    /**
134     * Reads configuration from given file.
135     * @param file file with configuration
136     * @param throwIfNotFound whether to throw exception if file is missing
137     * @throws IncorrectConfigurationException when configuration is incorrect
138     * @throws IOException when cannot read file
139     */
140    public void combineConfigurationFromFile(File file, boolean throwIfNotFound)
141            throws IncorrectConfigurationException, IOException {
142        if (!file.exists()) {
143            if (throwIfNotFound) {
144                throw new RuntimeException("Configuration file not found: " + file.getAbsolutePath());
145            } else {
146                return;
147            }
148        }
149        try(InputStream stream = new FileInputStream(file)) {
150            configuration.combine(stream);
151        }
152    }
153
154    /**
155     * Reads configuration from given string.
156     * @param string string with configuration
157     * @throws IncorrectConfigurationException when configuration is incorrect
158     */
159    public void combineConfigurationFromString(String string) throws IncorrectConfigurationException {
160        try (InputStream stream = new ByteArrayInputStream(string.getBytes(Charsets.UTF_8))) {
161            configuration.combine(stream);
162        } catch (IOException e) {
163            throw new RuntimeException(e);
164        }
165    }
166
167    public String printCombinedXml() {
168        return XmlUtils.prettyPrint(configuration.getRootElement());
169    }
170
171    public void addCustomPropertyResolver(PropertyResolver resolver) {
172        customPropertyResolvers.addPropertyResolver(resolver);
173    }
174
175    public void generateTemplateConfigurationFile() throws FileNotFoundException, IOException {
176        Path schemaPath = fileAccessor.getWritebleConfigFile(SCHEMA_FILE_NAME);
177        Files.createDirectories(schemaPath.getParent());
178        configuration.generateSchema(schemaPath.toFile());
179
180        Path configPath = fileAccessor.getWritebleConfigFile(CONFIG_FILE_NAME);
181        if (Files.exists(configPath)) {
182            return;
183        }
184        try (Writer writer = Files.newBufferedWriter(configPath, Charsets.UTF_8)) {
185            writer.append("<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
186                    + " xsi:noNamespaceSchemaLocation=\"" + SCHEMA_FILE_NAME
187                    + "\">\n</config>\n");
188        }
189    }
190}