/**
* The MIT License
*
* Copyright (C) 2007 Asterios Raptis
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.alpharogroup.lang;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FilenameUtils;
import de.alpharogroup.file.FileExtension;
import de.alpharogroup.file.namefilter.ResourceBundleFilenameFilter;
/**
* The Class PropertiesUtils provides methods loading properties and other related operations for
* properties like find redundant values or getting all available languages from a bundle.
*/
public final class PropertiesUtils
{
/**
* The Constant SEARCH_FILE_PATTERN is a regex for searching java and html files.
*/
public static final String SEARCH_FILE_PATTERN = "([^\\s]+(\\.(?i)(java|html|htm))$)";
/**
* The Constant PROPERTIES_DELIMITERS contains all valid delimiters for properties files.
*/
public static final String[] PROPERTIES_DELIMITERS = { "=", ":", " " };
/** The LOGGER. */
private static final Logger LOGGER = Logger.getLogger(PropertiesUtils.class.getName());
/**
* Finds redundant values from the given Properties object and saves it to a Map.
*
* @param properties
* The Properties to check.
* @return A map that contains the redundant value as the key of the map and a List(as value of
* the map) of keys that have the redundant value.
*/
public static Map<String, List<String>> findRedundantValues(final Properties properties)
{
final Map<String, List<String>> reverseEntries = new LinkedHashMap<>();
for (final Map.Entry<Object, Object> entry : properties.entrySet())
{
final String key = (String)entry.getKey();
final String value = (String)entry.getValue();
if (!reverseEntries.containsKey(value))
{
final List<String> keys = new ArrayList<>();
keys.add(key);
reverseEntries.put(value, keys);
}
else
{
final List<String> keys = reverseEntries.get(value);
keys.add(key);
}
}
final Map<String, List<String>> redundantValues = new LinkedHashMap<>();
for (final Map.Entry<String, List<String>> entry : reverseEntries.entrySet())
{
final String key = entry.getKey();
final List<String> keys = entry.getValue();
if (1 < keys.size())
{
redundantValues.put(key, keys);
}
}
return redundantValues;
}
/**
* Gets all the available languages to the given resource bundle name in the given bundle
* package.
*
* @param bundlepackage
* The package that contains the properties files.
* @param bundlename
* The name of the resource bundle.
* @return a Set of String objects with the available languages.
*/
public static Set<String> getAvailableLanguages(final String bundlepackage,
final String bundlename)
{
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final File root = new File(loader.getResource(bundlepackage.replace('.', '/')).getFile());
final File[] files = root.listFiles(new ResourceBundleFilenameFilter(bundlename));
final Set<String> languages = new TreeSet<>();
for (final File file : files)
{
languages.add(file.getName().replaceAll("^" + bundlename + "(_)?|\\.properties$", ""));
}
return languages;
}
/**
* Gets the bundle name from the given properties file.
*
* @param propertiesFile
* the properties file
* @return the bundle name
*/
public static String getBundlename(final File propertiesFile)
{
final String filename = propertiesFile.getName();
final int indexOfUnderscore = filename.indexOf("_");
String bundlename = filename;
if (0 < indexOfUnderscore)
{
bundlename = propertiesFile.getName().substring(0, filename.indexOf("_"));
}
return bundlename;
}
/**
* Gets from the given properties file the locale.
*
* @param propertiesFile
* the properties file
* @return the locale
*/
public static Locale getLocale(final File propertiesFile)
{
final String localeString = propertiesFile.getName().replaceAll(
"^" + getBundlename(propertiesFile) + "(_)?|\\.properties$", "");
Locale current = null;
if (localeString != null && !localeString.isEmpty())
{
final String[] splitted = localeString.split("_");
if (1 < splitted.length)
{
current = new Locale(splitted[0], splitted[1]);
}
else
{
current = new Locale(localeString);
}
}
else
{
current = Locale.getDefault();
}
return current;
}
/**
* Gets all the available Locales to the given resource bundle name in the given bundle package.
* For now it is only for properties files.
*
* @param bundlepackage
* The package that contains the properties files.
* @param bundlename
* The name of the resource bundle.
* @return a Map of File objects with the corresponding Locales to it.
*/
public static Map<File, Locale> getLocales(final String bundlepackage, final String bundlename)
{
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final File root = new File(loader.getResource(bundlepackage.replace('.', '/')).getFile());
final File[] files = root.listFiles(new ResourceBundleFilenameFilter(bundlename));
final Map<File, Locale> locales = new HashMap<>();
for (final File file : files)
{
final Locale current = getLocale(file);
locales.put(file, current);
}
return locales;
}
/**
* Gets the properties.
*
* @param componentClass
* the component class
* @param defaultClass
* the default class
* @param locale
* the locale
* @return the properties
* @throws Exception
* the exception
*/
public static Properties getLocalPropertiesFromClass(final Class<?> componentClass,
final Class<?> defaultClass, final Locale locale) throws Exception
{
// Try to find the properties file and the resource
Properties properties = null;
if (componentClass != null)
{
properties = PropertiesUtils.loadPropertiesFromClassObject(componentClass, locale);
}
else
{
properties = PropertiesUtils.loadPropertiesFromClassObject(defaultClass, locale);
}
return properties;
}
/**
* Finds all keys with the same key prefixes from the given Properties and saves them to a Map
* with the prefix as a key and holds a List with the whole keys the starts with the same key
* prefix.
*
* @param enProperties
* the en properties
* @return the matched prefix lists
*/
public static Map<String, List<String>> getMatchedPrefixLists(final Properties enProperties)
{
final Enumeration<?> e = enProperties.propertyNames();
final Map<String, List<String>> matchedPrefixes = new LinkedHashMap<>();
while (e.hasMoreElements())
{
final String key = (String)e.nextElement();
final int lastIndex = key.lastIndexOf(".");
String subKey = null;
if (0 < lastIndex)
{
subKey = key.substring(0, lastIndex);
}
else
{
subKey = key;
}
if (matchedPrefixes.containsKey(subKey))
{
final List<String> fullKeys = matchedPrefixes.get(subKey);
fullKeys.add(key);
}
else
{
final List<String> fullKeys = new ArrayList<>();
fullKeys.add(key);
matchedPrefixes.put(subKey, fullKeys);
}
}
return matchedPrefixes;
}
/**
* Finds the property parameters from the given propertyValue.
*
* @param propertyValue
* the property value
* @return the property parameters as a list.
*/
public static List<String> getPropertyParameters(final String propertyValue)
{
final List<String> parameterList = new ArrayList<>();
final Pattern pattern = Pattern.compile("\\{.*?\\}");
final Matcher matcher = pattern.matcher(propertyValue);
while (matcher.find())
{
final String parameter = matcher.group();
final String at = parameter.substring(1, parameter.length() - 1);
parameterList.add(at);
}
return parameterList;
}
/**
* Load properties.
*
* @param clazz
* the clazz
* @param name
* the package path with the file name
* @return the properties
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadProperties(final Class<?> clazz, final String name)
throws IOException
{
Properties properties = loadProperties(name);
if (properties == null)
{
final InputStream is = ClassExtensions.getResourceAsStream(clazz.getClass(), name);
if (is != null)
{
properties = new Properties();
properties.load(is);
}
}
return properties;
}
/**
* Load properties.
*
* @param clazz
* the clazz
* @param packagePath
* the package path without the file name
* @param fileName
* the file name
* @return the properties
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadProperties(final Class<?> clazz, final String packagePath,
final String fileName) throws IOException
{
return loadProperties(clazz, packagePath + fileName);
}
/**
* Load a Properties-object from the given File-object.
*
* @param propertiesFile
* the properties file
* @return the properties or null if the file is not found.
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadProperties(final File propertiesFile) throws IOException
{
Properties properties = null;
InputStream is = null;
if (propertiesFile.exists())
{
is = propertiesFile.toURI().toURL().openStream();
if (is != null)
{
properties = new Properties();
properties.load(is);
}
}
else
{
throw new FileNotFoundException(propertiesFile.getName() + " not found.");
}
return properties;
}
/**
* Gives a Properties-object from the given packagepath.
*
* @param packagePath
* The package-path and the name from the resource as a String.
* @return The Properties-object from the given packagepath.
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadProperties(final String packagePath) throws IOException
{
Properties properties = null;
final URL url = ClassExtensions.getResource(packagePath);
if (url != null)
{
properties = new Properties();
properties.load(url.openStream());
}
else
{
final InputStream is = ClassExtensions.getResourceAsStream(packagePath);
if (is != null)
{
properties = new Properties();
properties.load(is);
}
}
return properties;
}
/**
* Load properties.
*
* @param packagePath
* the package path without the file name
* @param fileName
* the file name
* @return the properties
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadProperties(String packagePath, String fileName) throws IOException
{
StringBuilder sb = new StringBuilder();
packagePath = FilenameUtils.normalize(packagePath);
final String slash = "/";
if (packagePath.startsWith(slash))
{
// remove first slash...
if (packagePath.endsWith(slash))
{
sb.append(packagePath.substring(1, packagePath.length()));
}
else
{
// append slash at the end...
sb.append(packagePath.substring(1, packagePath.length())).append(slash);
}
}
else
{
if (packagePath.endsWith(slash))
{
// remove first char...
sb.append(packagePath);
}
else
{
// remove first char...
sb.append(packagePath).append(slash);
}
}
packagePath = sb.toString().trim();
sb = new StringBuilder();
if (fileName.startsWith(slash))
{
sb.append(fileName.substring(1, fileName.length()));
}
fileName = sb.toString().trim();
return loadProperties(packagePath + fileName);
}
/**
* Load the properties file from the given class object. The filename from the properties file
* is the same as the simple name from the class object and it looks at the same path as the
* given class object. If locale is not null than the language will be added to the filename
* from the properties file.
*
* @param clazz
* the clazz
* @param locale
* the locale
* @return the properties
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static Properties loadPropertiesFromClassObject(final Class<?> clazz, final Locale locale)
throws IOException
{
if (null == clazz)
{
throw new IllegalArgumentException("Class object must not be null!!!");
}
StringBuilder propertiesName = new StringBuilder();
Properties properties = null;
String language = null;
String filename = null;
String pathAndFilename = null;
File propertiesFile = null;
String absoluteFilename = null;
final String packagePath = PackageUtils.getPackagePathWithSlash(clazz);
final List<String> missedFiles = new ArrayList<>();
if (null != locale)
{
propertiesName.append(clazz.getSimpleName());
language = locale.getLanguage();
if (null != language && !language.isEmpty())
{
propertiesName.append("_").append(language);
}
final String country = locale.getCountry();
if (null != country && !country.isEmpty())
{
propertiesName.append("_").append(country);
}
propertiesName.append(FileExtension.PROPERTIES.getExtension());
filename = propertiesName.toString().trim();
pathAndFilename = packagePath + filename;
URL url = ClassExtensions.getResource(clazz, filename);
if (url != null)
{
absoluteFilename = url.getFile();
}
else
{
missedFiles.add("File with filename '" + filename + "' does not exists.");
}
if (null != absoluteFilename)
{
propertiesFile = new File(absoluteFilename);
}
if (null != propertiesFile && propertiesFile.exists())
{
properties = PropertiesUtils.loadProperties(pathAndFilename);
}
else
{
propertiesName = new StringBuilder();
if (null != locale)
{
propertiesName.append(clazz.getSimpleName());
language = locale.getLanguage();
if (null != language && !language.isEmpty())
{
propertiesName.append("_").append(language);
}
propertiesName.append(FileExtension.PROPERTIES.getExtension());
filename = propertiesName.toString().trim();
pathAndFilename = packagePath + filename;
url = ClassExtensions.getResource(clazz, filename);
if (url != null)
{
absoluteFilename = url.getFile();
}
else
{
missedFiles.add("File with filename '" + filename + "' does not exists.");
}
if (null != absoluteFilename)
{
propertiesFile = new File(absoluteFilename);
}
if (null != propertiesFile && propertiesFile.exists())
{
properties = PropertiesUtils.loadProperties(pathAndFilename);
}
}
}
}
if (null == properties)
{
propertiesName = new StringBuilder();
propertiesName.append(clazz.getSimpleName()).append(
FileExtension.PROPERTIES.getExtension());
filename = propertiesName.toString().trim();
pathAndFilename = packagePath + filename;
final URL url = ClassExtensions.getResource(clazz, filename);
if (url != null)
{
absoluteFilename = url.getFile();
}
else
{
properties = PropertiesUtils.loadProperties(pathAndFilename);
missedFiles.add("File with filename '" + filename + "' does not exists.");
}
if (null != absoluteFilename)
{
propertiesFile = new File(absoluteFilename);
}
if (null != propertiesFile && propertiesFile.exists())
{
properties = PropertiesUtils.loadProperties(pathAndFilename);
}
}
if (properties == null)
{
for (final String string : missedFiles)
{
LOGGER.info(string);
}
}
return properties;
}
/**
* Converts the given xml file to the given properties file.
*
* @param properties
* the properties file. The xml file does not have to exist.
* @param xml
* the xml file with the properties to convert.
* @param comment
* the comment
* @throws FileNotFoundException
* the file not found exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static void toProperties(final File properties, final File xml, final String comment)
throws FileNotFoundException, IOException
{
toProperties(new FileOutputStream(properties), new FileInputStream(xml), comment);
}
/**
* Converts the given xml InputStream to the given properties OutputStream.
*
* @param properties
* the properties file. The xml file does not have to exist.
* @param xml
* the xml file with the properties to convert.
* @param comment
* the comment
* @throws FileNotFoundException
* the file not found exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static void toProperties(final OutputStream properties, final InputStream xml,
final String comment) throws FileNotFoundException, IOException
{
final Properties prop = new Properties();
prop.loadFromXML(xml);
prop.store(properties, comment);
}
/**
* Converts the given properties file to the given xml file.
*
* @param properties
* the properties file.
* @param xml
* the xml file to write in. The xml file does not have to exist.
* @param comment
* the comment
* @param encoding
* the encoding for the xml file.
* @throws FileNotFoundException
* the file not found exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static void toXml(final File properties, final File xml, final String comment,
final String encoding) throws FileNotFoundException, IOException
{
toXml(new FileInputStream(properties), new FileOutputStream(xml), comment, encoding);
}
/**
* Converts the given properties InputStream to the given xml OutputStream.
*
* @param properties
* the properties InputStream.
* @param xml
* the xml OutputStream to write in.
* @param comment
* the comment
* @param encoding
* the encoding for the xml file.
* @throws FileNotFoundException
* the file not found exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static void toXml(final InputStream properties, final OutputStream xml,
final String comment, final String encoding) throws FileNotFoundException, IOException
{
final Properties prop = new Properties();
prop.load(properties);
prop.storeToXML(xml, comment, encoding);
}
/**
* Private constructor.
*/
private PropertiesUtils()
{
}
}