/**
* 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
*
* - all include instructions of a certain properties file are sorted alphabetically by their key name before being
* executed,
* - include instructions can be used within any properties files being loaded (even within other includes) and that
* - (wildcard) path/file declarations are always case insensitive, regardless of the underlying operating
* system.
* - included properties always override locally defined properties
*
*
* 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() + "'.");
}
}
}