/** * 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.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; 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 ResourceBundle#getBundle(String, 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. *

* * @author Hanspeter Dünnenberger */ 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 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()); 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 = 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, baseFileName, name); } } else { File dir = new File(jarUrl.getFile()); dir = dir.getParentFile(); if (dir.isDirectory()) { for (String name : dir.list()) { addMatchingNameOnce(resourcePath, baseName, bundleNames, baseFileName, name); } } } } } catch (Exception e) { e.printStackTrace(); } Collections.sort(bundleNames, new Comparator() { public int compare(String o1, String o2) { int rc = 0; if (baseName.equals(o1)) { rc = 1; } else if (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 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 = name.indexOf(baseName); if (prefixed > -1 && name.endsWith(baseFileName)) { String toAdd = resourcePath + name.substring(0, prefixed) + baseName; if (!bundleNames.contains(toAdd)) { bundleNames.add(toAdd); } } } }