/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.deltaspike.core.api.config;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;
import org.apache.deltaspike.core.spi.config.ConfigSource;
import org.apache.deltaspike.core.util.ClassUtils;
import org.apache.deltaspike.core.util.ProjectStageProducer;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility class to load configuration properties via arbitrary property files in a well defined order.
*
* <p>
* This will also pick up property files with names suffixed with {@code -<project stage>}, e.g.
* myconfig-Production.properties.</p>
* <p>
* User configurations should have {@code deltaspike_ordinal} as the first property, with a value greater than
* 100.</p>
*
*/
public class PropertyLoader
{
public static final int CONFIGURATION_ORDINAL_DEFAULT_VALUE = 100;
private static final String FILE_EXTENSION = ".properties";
private static final Logger LOG = Logger.getLogger(PropertyLoader.class.getName());
private PropertyLoader()
{
// utility class doesn't have a public ct
}
/**
* Looks for all properties files with the given name in the classpath, loads them in ascending order determined by
* their ordinal and merges them.
*
* <p>
* The idea is to be able to override properties by just providing a new properties file with the same name but a
* higher 'deltaspike_ordinal' than the old one.</p>
*
* <p>
* If a property file defines no 'deltaspike_ordinal' property than a default value of
* {@link #CONFIGURATION_ORDINAL_DEFAULT_VALUE} is assumed. Any sensitive default which is provided by the system
* parsing for the configuration should have a 'deltaspike_ordinal' value lower than 10. In most cases a value of
* 1.</p>
*
* <p>
* If two property files have the same 'deltaspike_ordinal', their order is undefined. The Properties file which
* gets found first will be processed first and thus gets overwritten by the one found later.</p>
*
* @param propertyFileName the name of the properties file, without the extension '.properties'
*
* @return the final property values
*/
public static synchronized Properties getProperties(String propertyFileName)
{
if (propertyFileName == null)
{
throw new IllegalArgumentException("propertyFileName must not be null!");
}
try
{
if (propertyFileName.endsWith(FILE_EXTENSION))
{
// if the given propertyFileName already contains the extension, then remove it.
propertyFileName = propertyFileName.substring(0, propertyFileName.length() - FILE_EXTENSION.length());
}
List<Properties> allProperties = loadAllProperties(propertyFileName);
if (allProperties == null)
{
return null;
}
List<Properties> sortedProperties = sortProperties(allProperties);
Properties properties = mergeProperties(sortedProperties);
return properties;
}
catch (IOException e)
{
LOG.log(Level.SEVERE, "Error while loading the propertyFile " + propertyFileName, e);
return null;
}
}
private static List<Properties> loadAllProperties(String propertyFileName)
throws IOException
{
ClassLoader cl = ClassUtils.getClassLoader(null);
List<Properties> properties = new ArrayList<Properties>();
// read the normal property file names
Enumeration<URL> propertyUrls = cl.getResources(propertyFileName + FILE_EXTENSION);
while (propertyUrls != null && propertyUrls.hasMoreElements())
{
URL propertyUrl = propertyUrls.nextElement();
fillProperties(properties, propertyUrl);
}
// and also read the ones post-fixed with the projectStage
ProjectStage ps = ProjectStageProducer.getInstance().getProjectStage();
propertyUrls = cl.getResources(propertyFileName + "-" + ps + FILE_EXTENSION);
while (propertyUrls != null && propertyUrls.hasMoreElements())
{
URL propertyUrl = propertyUrls.nextElement();
fillProperties(properties, propertyUrl);
}
if (properties.isEmpty())
{
if (LOG.isLoggable(Level.INFO))
{
LOG.info("could not find any property files with name " + propertyFileName);
}
return null;
}
return properties;
}
private static void fillProperties(List<Properties> properties, URL propertyUrl) throws IOException
{
InputStream is = null;
try
{
is = propertyUrl.openStream();
Properties prop = new Properties();
prop.load(is);
properties.add(prop);
// a bit debugging output
int ordinal = getConfigurationOrdinal(prop);
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("loading properties with ordinal " + ordinal + " from file " + propertyUrl.getFile());
}
}
finally
{
if (is != null)
{
is.close();
}
}
}
/**
* Implement a quick and dirty sorting mechanism for the given Properties.
* @param allProperties
* @return the Properties list sorted by it's 'configuration.ordinal' in ascending order.
*/
private static List<Properties> sortProperties(List<Properties> allProperties)
{
List<Properties> sortedProperties = new ArrayList<Properties>();
for (Properties p : allProperties)
{
int configOrder = getConfigurationOrdinal(p);
int i;
for (i = 0; i < sortedProperties.size(); i++)
{
int listConfigOrder = getConfigurationOrdinal(sortedProperties.get(i));
if (listConfigOrder > configOrder)
{
// only go as far as we found a higher priority Properties file
break;
}
}
sortedProperties.add(i, p);
}
return sortedProperties;
}
/**
* Determine the 'deltaspike_ordinal' of the given properties.
* {@link #CONFIGURATION_ORDINAL_DEFAULT_VALUE} if
* {@link ConfigSource#DELTASPIKE_ORDINAL} is not set in the
* Properties file.
*
* @param p the Properties from the file.
* @return the ordinal number of the given Properties file.
*/
private static int getConfigurationOrdinal(Properties p)
{
int configOrder = CONFIGURATION_ORDINAL_DEFAULT_VALUE;
String configOrderString = p.getProperty(ConfigSource.DELTASPIKE_ORDINAL);
if (configOrderString != null && configOrderString.length() > 0)
{
try
{
configOrder = Integer.parseInt(configOrderString);
}
catch (NumberFormatException nfe)
{
LOG.severe(ConfigSource.DELTASPIKE_ORDINAL + " must be an integer value!");
throw nfe;
}
}
return configOrder;
}
/**
* Merge the given Properties in order of appearance.
* @param sortedProperties
* @return the merged Properties
*/
private static Properties mergeProperties(List<Properties> sortedProperties)
{
Properties mergedProperties = new Properties();
for (Properties p : sortedProperties)
{
for (Map.Entry<?, ?> entry : p.entrySet())
{
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (!ConfigSource.DELTASPIKE_ORDINAL.equals(key))
{
// simply overwrite the old properties with the new ones.
mergedProperties.setProperty(key, value);
}
}
}
return mergedProperties;
}
}