/** * Copyright 2006 by Know-Center, Graz, Austria * PDF-AS has been contracted by the E-Government Innovation Center EGIZ, a * joint initiative of the Federal Chancellery Austria and Graz University of * Technology. * * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * http://www.osor.eu/eupl/ * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. */ package at.knowcenter.wag.egov.egiz.cfg; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Enumeration; import java.util.InvalidPropertiesFormatException; import java.util.Iterator; import java.util.Properties; import java.util.SortedMap; import java.util.TreeMap; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Enhanced Java Properties allowing nested include instructions.
* In order to include further Properties use the following instruction: *

* include = [path/to/]foo.properties *

* Note that wildcard imports are allowed, e.g. *

* include = [path/to/]profile.*.properties *

* In order to use more than one include instruction within a file append an arbitary postfix to include. * in order to make each include key unique within a properties file, e.g. *

* include = profile.SIGNATURBLOCK*.properties
* include.amtssignaturen = profile.AMTSSIGNATURBLOCK*.properties
* include.1 = myProfiles1/*.properties
* include.2 = myProfiles2/*.properties *

* Note that *

*

* Mind creating circular includes! * * @author Datentechnik Innovation GmbH */ public class NestedProperties extends Properties { private static final long serialVersionUID = 1L; private Log log = LogFactory.getLog(getClass()); /** * Creates an empty property list with no default values. */ public NestedProperties() { super(); } /** * Creates an empty property list with the specified defaults. * @param defaults The defaults. */ public NestedProperties(Properties defaults) { super(defaults); } /** * The name of the key that triggers including of other properties. */ private final String INCLUDE_KEY_NAME = "include"; /** * Defines the default behaviour of the file matching filter. */ private final IOCase DEFAULT_IOCASE = IOCase.SENSITIVE; /** * The maximum depth of includes before being regarded as circular (throwing a {@link CircularIncludeException}). */ private final int MAX_NESTED_INCLUDE_DEPTH = 25; @Override /** * Warning: When Properties are loaded using InputStreams include instructions are not supported. */ public synchronized void load(InputStream inStream) throws IOException { log.debug("Loading properties from input stream. Include instructions are not supported."); super.load(inStream); } @Override public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException { // Loading from InputStream is not supported since including further property files that have been declared // using relative paths need a context directory which cannot be retrieved from InputStreams. throw new UnsupportedOperationException("Imports from XML files are not supported."); } /** * Reads a property list from a certain file including other properties files if include instructions are present. * Note that include instructions that do not match any files do not result in an exception. A respective message at * WARN level is logged. * * @param file * The file to be read. * @throws IOException * Thrown in case of an I/O error. * @throws CircularIncludeException * Thrown if circular includes have been detected (@link {@link RuntimeException}). */ public synchronized void load(File file) throws IOException, CircularIncludeException { load(file, 0); } /** * Reads a property list from a certain file including other properties files if include instructions are present. * Note that include instructions that do not match any files do not result in an exception. A respective message at * WARN level is logged. * * @param file * The file to be read. * @param currentDepth * The current include depth. * @throws IOException * Thrown in case of an I/O error. * @throws CircularIncludeException * Thrown if circular includes have been detected (@link {@link RuntimeException}). */ private synchronized void load(File file, int currentDepth) throws IOException, CircularIncludeException { if (currentDepth > MAX_NESTED_INCLUDE_DEPTH) { throw new CircularIncludeException("Circular include instruction(s) detected."); } InputStream in = null; try { in = new FileInputStream(file); log.debug("Loading '" + file.getCanonicalPath() + "'."); super.load(in); } finally { IOUtils.closeQuietly(in); } // Properties have been loaded. Apply preprocessing step in order to process include instructions. // Provide a context directory in order to be able to resolve relative path instructions. processIncludes(file.getParentFile(), currentDepth); } /** * Resolves all include instructions as part of a postprocessing step. * * @param contextFolder * The folder that should be assumed as starting folder for relative include instructions. * @param currentDepth * The current include depth. * @throws IOException * Thrown in case of error. */ private void processIncludes(File contextFolder, int currentDepth) throws IOException { SortedMap sortedIncludeInstructions = new TreeMap(); // Walk through properties, collecting include instructions. // Since the backing Hashtable does not guarantee any order, import instructions need to be sorted according to // their keys (natural order -> alphabetically). // This allows for defining a pseudo load order: include.1=path/to/settings.propertes, // include.2=other/path/to/settings.properties @SuppressWarnings("unchecked") Enumeration propertyNames = (Enumeration) propertyNames(); while (propertyNames.hasMoreElements()) { String key = propertyNames.nextElement(); // valid include instructions: include=xxx, include.foo=xxx, include.foo.foo=xxx... (keys are case // insensitive) if (INCLUDE_KEY_NAME.equalsIgnoreCase(key) || StringUtils.startsWithIgnoreCase(key, INCLUDE_KEY_NAME + ".")) { String includeValue = StringUtils.trimToNull(getProperty(key)); if (includeValue != null) { sortedIncludeInstructions.put(key, includeValue); } } } // performing imports Iterator includeIt = sortedIncludeInstructions.keySet().iterator(); while (includeIt.hasNext()) { String includeInstructionKey = includeIt.next(); String includePath = getProperty(includeInstructionKey); processInclude(contextFolder, includePath, currentDepth); // remove import instruction from properties remove(includeInstructionKey); } } /** * Processes a single include instruction (which may lead to several imports due to wildcard support). * * @param contextFolder * The folder that should be assumed as starting folder for relative include instructions. * @param includePath * The include path instruction. * @param currentDepth * The current include depth. * @throws IOException * Thrown in case of error. */ private void processInclude(File contextFolder, String includePath, int currentDepth) throws IOException { // Combine contextFolder with relative path instructions from includePath. File includeInstruction = new File(contextFolder, includePath); contextFolder = includeInstruction.getParentFile(); String includeName = includeInstruction.getName(); WildcardFileFilter fileFilter = new WildcardFileFilter(includeName, DEFAULT_IOCASE); Collection includeFiles = null; if (contextFolder != null && contextFolder.exists() && contextFolder.isDirectory()) { includeFiles = FileUtils.listFiles(contextFolder, fileFilter, null); } if (includeFiles != null && !includeFiles.isEmpty()) { log.info("Including '" + includePath + "'."); for (File includeFile : includeFiles) { NestedProperties includeProperties = new NestedProperties(); includeProperties.load(includeFile, currentDepth + 1); putAll(includeProperties); } } else { log.warn("Unable to find '" + includeName + "' in folder '" + contextFolder.getCanonicalPath() + "'."); } } }