/* * Copyright 2013 Atteo. * * 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.moonshine; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import org.atteo.classindex.ClassFilter; import org.atteo.classindex.ClassIndex; import org.atteo.config.Configuration; import org.atteo.config.IncorrectConfigurationException; import org.atteo.config.XmlDefaultValue; import org.atteo.config.XmlUtils; import org.atteo.filtering.CompoundPropertyResolver; import org.atteo.filtering.EnvironmentPropertyResolver; import org.atteo.filtering.OneOfPropertyResolver; import org.atteo.filtering.PropertyResolver; import org.atteo.filtering.SystemPropertyResolver; import org.atteo.filtering.XmlPropertyResolver; import org.atteo.moonshine.directories.FileAccessor; import org.atteo.moonshine.services.Service; import org.atteo.xmlcombiner.CombineSelf; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.google.common.base.CaseFormat; import com.google.common.base.Charsets; public class ConfigurationReader { public final static String SCHEMA_FILE_NAME = "schema.xsd"; public final static String CONFIG_FILE_NAME = "config.xml"; public final static String AUTO_CONFIG_FILE_NAME = "auto-config.xml"; public final static String DEFAULT_CONFIG_RESOURCE_NAME = "/default-config.xml"; private final Configuration configuration = new Configuration(); private final CompoundPropertyResolver customPropertyResolvers = new CompoundPropertyResolver(); private PropertyResolver propertyResolver = null; private final FileAccessor fileAccessor; public ConfigurationReader(FileAccessor fileAccessor) { this.fileAccessor = fileAccessor; } public void filter() throws IncorrectConfigurationException { Element propertiesElement = null; if (configuration.getRootElement() != null) { NodeList nodesList = configuration.getRootElement().getElementsByTagName("properties"); if (nodesList.getLength() == 1) { propertiesElement = (Element) nodesList.item(0); } } propertyResolver = new CompoundPropertyResolver( new OneOfPropertyResolver(), new SystemPropertyResolver(), new EnvironmentPropertyResolver(), new XmlPropertyResolver(propertiesElement, false), customPropertyResolvers, new XmlPropertyResolver(configuration.getRootElement(), true)); configuration.filter(propertyResolver); } public Config read() throws IncorrectConfigurationException { return configuration.read(Config.class); } public PropertyResolver getPropertyResolver() { return propertyResolver; } /** * Generate auto-config.xml. */ public void generateAutoConfiguration() throws IncorrectConfigurationException, IOException { Iterable<Class<? extends Service>> services = ClassFilter.only() .topLevel() .withoutModifiers(Modifier.ABSTRACT) .satisfying(TopLevelService.class::isAssignableFrom) .satisfying((Class<?> type) -> !containsRequiredFieldWithoutDefault(type)) .satisfying((Class<?> type) -> { ServiceConfiguration annotation = type.getAnnotation(ServiceConfiguration.class); return annotation == null || annotation.auto(); }) .from(ClassIndex.getSubclasses(Service.class)); StringBuilder builder = new StringBuilder(); builder.append("<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" + " xsi:noNamespaceSchemaLocation=\"" + SCHEMA_FILE_NAME + "\">\n"); for (Class<? extends Service> service : services) { ServiceConfiguration annotation = service.getAnnotation(ServiceConfiguration.class); String name = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, service.getSimpleName()); XmlRootElement xmlRootElement = service.getAnnotation(XmlRootElement.class); if (xmlRootElement != null && !"##default".equals(xmlRootElement.name())) { name = xmlRootElement.name(); } builder.append("\t<").append(name); builder.append(" combine.self='").append(CombineSelf.OVERRIDABLE_BY_TAG.name()); if (annotation == null || annotation.autoConfiguration().isEmpty()) { builder.append("'/>\n"); } else { builder.append("'>\n"); builder.append(annotation.autoConfiguration()); builder.append("\n</").append(name).append(">\n"); } } builder.append("</config>\n"); Path autoConfigPath = fileAccessor.getWritableConfigFile(AUTO_CONFIG_FILE_NAME); try (Writer writer = Files.newBufferedWriter(autoConfigPath, Charsets.UTF_8)) { writer.write(builder.toString()); } } /** * Removes auto-config.xml file. */ public void removeAutoConfiguration() throws IOException { Path autoConfigPath = fileAccessor.getWritableConfigFile(AUTO_CONFIG_FILE_NAME); Files.deleteIfExists(autoConfigPath); } /** * Reads automatic configuration from auto-config.xml file. * @throws IncorrectConfigurationException when configuration is incorrect * @throws IOException when cannot read resource */ public void combineAutoConfiguration() throws IncorrectConfigurationException, IOException { Path path = fileAccessor.getConfigFile(AUTO_CONFIG_FILE_NAME); try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) { combineConfigurationFromStream(stream); } } /** * Reads configuration from '/default-config.xml' resource. */ public void combineDefaultConfiguration() { try { combineConfigurationFromResource(DEFAULT_CONFIG_RESOURCE_NAME, false); } catch (IOException | IncorrectConfigurationException e) { throw new RuntimeException(e); } } /** * Reads configuration from config.xml files found in ${configDirs} and ${configHome} directories. * @throws IncorrectConfigurationException when configuration is incorrect * @throws IOException when cannot read resource */ public void combineConfigDirConfiguration() throws IncorrectConfigurationException, IOException { for (Path path : fileAccessor.getConfigFiles(CONFIG_FILE_NAME)) { try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) { combineConfigurationFromStream(stream); } } } /** * Reads configuration from given resource. * @param resourcePath path to the resource * @throws IncorrectConfigurationException when configuration is incorrect * @throws IOException when cannot read resource */ public void combineConfigurationFromResource(String resourcePath, boolean throwIfNotFound) throws IncorrectConfigurationException, IOException { // TODO: what if more than one resource with given name? try(InputStream stream = getClass().getResourceAsStream(resourcePath)) { if (stream != null) { configuration.combine(stream); } else if (throwIfNotFound) { throw new RuntimeException("Configuration resource not found: " + resourcePath); } } } public void combineConfigurationFromStream(InputStream stream) throws IncorrectConfigurationException, IOException { configuration.combine(stream); } /** * Reads configuration from given file. * @param file file with configuration * @param throwIfNotFound whether to throw exception if file is missing * @throws IncorrectConfigurationException when configuration is incorrect * @throws IOException when cannot read file */ public void combineConfigurationFromFile(File file, boolean throwIfNotFound) throws IncorrectConfigurationException, IOException { if (!file.exists()) { if (throwIfNotFound) { throw new RuntimeException("Configuration file not found: " + file.getAbsolutePath()); } else { return; } } try(InputStream stream = new FileInputStream(file)) { configuration.combine(stream); } } /** * Reads configuration from given string. * @param string string with configuration * @throws IncorrectConfigurationException when configuration is incorrect */ public void combineConfigurationFromString(String string) throws IncorrectConfigurationException { try (InputStream stream = new ByteArrayInputStream(string.getBytes(Charsets.UTF_8))) { configuration.combine(stream); } catch (IOException e) { throw new RuntimeException(e); } } public String printCombinedXml() { return XmlUtils.prettyPrint(configuration.getRootElement()); } public void addCustomPropertyResolver(PropertyResolver resolver) { customPropertyResolvers.addPropertyResolver(resolver); } public void generateTemplateConfigurationFile() throws FileNotFoundException, IOException { Path schemaPath = fileAccessor.getWritableConfigFile(SCHEMA_FILE_NAME); Files.createDirectories(schemaPath.getParent()); configuration.generateSchema(schemaPath.toFile()); Path configPath = fileAccessor.getWritableConfigFile(CONFIG_FILE_NAME); if (Files.exists(configPath)) { return; } try (Writer writer = Files.newBufferedWriter(configPath, Charsets.UTF_8)) { writer.append("<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" + " xsi:noNamespaceSchemaLocation=\"" + SCHEMA_FILE_NAME + "\">\n</config>\n"); } } private static boolean containsRequiredFieldWithoutDefault(Class<?> type) { while (type != Object.class) { for (Field field : type.getDeclaredFields()) { if (field.isAnnotationPresent(XmlDefaultValue.class)) { continue; } XmlElement annotation = field.getAnnotation(XmlElement.class); XmlAttribute annotation2 = field.getAnnotation(XmlAttribute.class); if (annotation != null && annotation.required()) { return true; } if (annotation2 != null && annotation2.required()) { return true; } } type = type.getSuperclass(); } return false; } }