/** * Copyright 2009 by dueni.ch * * 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 ch.dueni.util; import java.io.File; import java.net.URL; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; /** * MultiplePropertiesResourceBundle is an abstract base implementation to allow to * combine a ResourceBundle from multiple properties files whereas these properties files must end * with the same name - the base-name for these combined ResourceBundle. *

* A concrete implementation must subclass this class and provide a default constructor in which * super("base-name"); or super("package.name","base-name"); must be * called depending on if your properties files are located in default or a specific package. *

* *
 * public class ExampleResourceBundle extends MultiplePropertiesResourceBundle {
 * 	public ExampleResourceBundle() {
 * 		super("example");
 * 	}
 * }
 * 
 * or 
 * 
 * public class ExampleResourceBundle extends MultiplePropertiesResourceBundle {
 * 	public ExampleResourceBundle() {
 * 		super("my.package", "example");
 * 	}
 * }
 * 
* *

* For each Locale that you need to support you also must provide Locale variants of your java * ResourceBindle class as shown below. Creating an empty subclass of the above class does the job - * the separate class is needed to let {@link java.util.ResourceBundle#getBundle(String, java.util.Locale)} find and * cache your bundle with the right Locale. *

* *
 * public class ExampleResourceBundle_de extends ExampleResourceBundle {
 * }
 * 
* *

File name rules

*

* To allow automatic detection of the multiple properties files, for each filename you must provide * a general properties file without any Locale extension in the name (e.g. * additional-example.properties as a variant to examples.properties) - otherwise that properties * file name will not be used to load as PropertyResourceBundle. Let's assume we used the base-name * "example" and the following list of properties files are reachable: *

* *

* Only example and additional-example will be used as base-names to load this * MultiplePropertiesResourceBundle - another-example.properties is missing * and therefore another-example is not detected as a valid base-name. *

*

* It is also supported to provide additional properties files with a jar file. To make sure that * jar file is recognized as properties file provider it must contain a file "base-name".properties * (e.g. example.properties). This marker file may be empty and is only used to find all resource * paths containing properties files of interest (with matching base-name). In fact, every path * location containing properties files to be combined into one MultiplePropertiesResourceBundle * must contain that "base-name".properties file. *

*

* The load order of the properties files is base-named file first and then the additional * properties files in natural sort order of the file name. Properties files loaded later may * override previously loaded properties. *

* * @author Hanspeter Dünnenberger * *

* Here was appended additional constructor which don't receive parameters if you would like to load * ANY *.properties file which you define in resources folder you should extend this class with * constructor without arguments. Then this class expects that it find one main (default) property * file with name resources.properties but it load also any other .properties file from this * folder without constraints of its name. Other features from this library will be still working. * You can not use '.' and '_' in property file name because then filenames wrongly will parse * and you'll receive NullPointerException while creating ResourceBandle. *

* @author MichaƂ Rowicki */ public abstract class MultiplePropertiesResourceBundle extends ResourceBundle { private static final String CLASS = MultiplePropertiesResourceBundle.class.getName(); /** private Logger instance */ private static final Logger LOG = Logger.getLogger(CLASS); /** * The base name for the ResourceBundles to load in. */ private String baseName; /** * The package name where the properties files should be. */ private String packageName; /** * A Map containing the combined resources of all parts building this * MultiplePropertiesResourceBundle. */ private Map combined; /** * Construct a MultiplePropertiesResourceBundle for the all properties. */ protected MultiplePropertiesResourceBundle() { this(null, ""); } /** * Construct a MultiplePropertiesResourceBundle for the passed in base-name. * * @param baseName * the base-name that must be part of the properties file names. */ protected MultiplePropertiesResourceBundle(String baseName) { this(null, baseName); } /** * Construct a MultiplePropertiesResourceBundle for the passed in base-name. * * @param packageName * the package name where the properties files should be. * @param baseName * the base-name that must be part of the properties file names. */ protected MultiplePropertiesResourceBundle(String packageName, String baseName) { this.packageName = packageName; this.baseName = baseName; } @Override public Object handleGetObject(String key) { if (key == null) { throw new NullPointerException(); } loadBundlesOnce(); return combined.get(key); } @Override public Enumeration getKeys() { loadBundlesOnce(); ResourceBundle parent = this.parent; return new ResourceBundleEnumeration(combined.keySet(), (parent != null) ? parent.getKeys() : null); } /** * Load the resources once. */ private void loadBundlesOnce() { if (combined == null) { combined = new HashMap(128); List bundleNames = findBaseNames(baseName); for (String bundleName : bundleNames) { ResourceBundle bundle = ResourceBundle.getBundle(bundleName, getLocale(), new UTF8Control()); Enumeration keys = bundle.getKeys(); String key = null; while (keys.hasMoreElements()) { key = keys.nextElement(); combined.put(key, bundle.getObject(key)); } } } } /** * Return a Set with the real base-names of the multiple properties based resource bundles that * contribute to the full set of resources. * * @param baseName * the base-name that must be part of the properties file names. * @return a List with the real base-names. */ private List findBaseNames(final String baseName) { final String METHOD = "findBaseNames"; boolean isLoggable = LOG.isLoggable(Level.FINE); ClassLoader cl = Thread.currentThread().getContextClassLoader(); List bundleNames = new ArrayList(); try { String baseFileName = getBaseName(baseName) + ".properties"; String endFileName = baseName + ".properties"; String resourcePath = getResourcePath(); String resourceName = resourcePath + baseFileName; if (isLoggable) { LOG.logp(Level.FINE, CLASS, METHOD, "Looking for files named '" + resourceName + "'"); } Enumeration names = cl.getResources(resourceName); while (names.hasMoreElements()) { URL jarUrl = names.nextElement(); if (isLoggable) { LOG.logp(Level.FINE, CLASS, METHOD, "inspecting: " + jarUrl); } if ("jar".equals(jarUrl.getProtocol())) { String path = jarUrl.getFile(); String filename = path.substring(0, path.length() - resourceName.length() - 2); if (filename.startsWith("file:")) { filename = filename.substring(5); } JarFile jar = new JarFile(filename); for (Enumeration entries = jar.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String name = entry.getName(); addMatchingNameOnce("", baseName, bundleNames, endFileName, name); } jar.close(); } else { File dir = new File(jarUrl.getFile()); dir = dir.getParentFile(); if (dir.isDirectory()) { for (String name : dir.list()) { addMatchingNameOnce(resourcePath, baseName, bundleNames, endFileName, name); } } } } } catch (Exception e) { e.printStackTrace(); } Collections.sort(bundleNames, new Comparator() { public int compare(String o1, String o2) { int rc = 0; if (getBaseName(baseName).equals(o1)) { rc = -1; } else if (getBaseName(baseName).equals(o2)) { rc = 1; } else { rc = o1.compareTo(o2); } return rc; } }); if (isLoggable) { LOG.logp(Level.FINE, CLASS, METHOD, "Combine ResourceBundles named: " + bundleNames); } return bundleNames; } private String getBaseName(String baseName) { if (baseName.isEmpty()) { return "resources"; } else { return baseName; } } private String getResourcePath() { String result = ""; if (packageName != null) { result = packageName.replaceAll("\\.", "/") + "/"; } return result; } private void addMatchingNameOnce(String resourcePath, String baseName, List bundleNames, String baseFileName, String name) { int prefixed; if (baseName.isEmpty()) { prefixed = name.indexOf("_"); if (prefixed == -1) { prefixed = name.indexOf("."); } } else { prefixed = name.indexOf(baseName); } if (prefixed > -1 && name.endsWith(baseFileName)) { String toAdd = resourcePath + name.substring(0, prefixed) + baseName; if (!bundleNames.contains(toAdd)) { bundleNames.add(toAdd); } } } }