/* * 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.cocoon.i18n; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.component.ComponentException; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.avalon.framework.thread.ThreadSafe; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.store.Store; import org.apache.cocoon.util.NetUtils; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * This is the XMLResourceBundleFactory, the method for getting and creating * XMLResourceBundles. * * @author <a href="mailto:mengelhart@earthtrip.com">Mike Engelhart</a> * @author <a href="mailto:neeme@one.lv">Neeme Praks</a> * @author <a href="mailto:oleg@one.lv">Oleg Podolsky</a> * @author <a href="mailto:kpiroumian@apache.org">Konstantin Piroumian</a> * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a> * @version $Id$ */ public class XMLResourceBundleFactory extends AbstractLogEnabled implements BundleFactory, Serviceable, Configurable, Disposable, ThreadSafe { /** * Root directory to all bundle names */ protected String directory; /** * Reload check interval in milliseconds. * Defaults to 60000 (1 minute), use <code>-1</code> to * disable reloads and <code>0</code> to check for modifications * on each catalogue request. */ protected long interval; /** * Service Manager */ protected ServiceManager manager; /** * Source resolver */ protected SourceResolver resolver; /** * Store of the loaded bundles */ protected Store cache; // // Lifecycle // public void service(ServiceManager manager) throws ServiceException { this.manager = manager; this.resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE); } /** * Configure the component. * * @param configuration the configuration */ public void configure(Configuration configuration) throws ConfigurationException { this.directory = configuration.getChild(ConfigurationKeys.ROOT_DIRECTORY).getValue(""); String cacheRole = configuration.getChild(ConfigurationKeys.STORE_ROLE).getValue(Store.TRANSIENT_STORE); try { this.cache = (Store) this.manager.lookup(cacheRole); } catch (ServiceException e) { throw new ConfigurationException("Unable to lookup store '" + cacheRole + "'"); } this.interval = configuration.getChild(ConfigurationKeys.RELOAD_INTERVAL).getValueAsLong(60000L); if (getLogger().isDebugEnabled()) { getLogger().debug("Bundle directory '" + this.directory + "'"); getLogger().debug("Store role '" + cacheRole + "'"); } } /** * Disposes this component. */ public void dispose() { this.manager.release(this.resolver); this.manager.release(this.cache); this.resolver = null; this.cache = null; this.manager = null; } // // BundleFactory Interface // /** * Returns the root directory to all bundles. * * @return the directory path */ protected String getDirectory() { return this.directory; } /** * Select a bundle based on the bundle name and the locale name. * * @param name bundle name * @param locale locale name * @return the bundle * @exception ComponentException if a bundle is not found */ public Bundle select(String name, String locale) throws ComponentException { return select(getDirectory(), name, locale); } /** * Select a bundle based on the bundle name and the locale. * * @param name bundle name * @param locale locale * @return the bundle * @exception ComponentException if a bundle is not found */ public Bundle select(String name, Locale locale) throws ComponentException { return select(getDirectory(), name, locale); } /** * Select a bundle based on the catalogue base location, bundle name, * and the locale name. * * @param directory catalogue base location (URI) * @param name bundle name * @param localeName locale name * @return the bundle * @exception ComponentException if a bundle is not found */ public Bundle select(String directory, String name, String localeName) throws ComponentException { return select(directory, name, new Locale(localeName, localeName)); } /** * Select a bundle based on the catalogue base location, bundle name, * and the locale. * * @param directory catalogue base location (URI) * @param name bundle name * @param locale locale * @return the bundle * @exception ComponentException if a bundle is not found */ public Bundle select(String directory, String name, Locale locale) throws ComponentException { return select(new String[] { directory }, name, locale); } /** * Select a bundle based on the catalogue base location, bundle name, * and the locale. * * @param directories catalogue base location (URI) * @param name bundle name * @param locale locale * @return the bundle * @exception ComponentException if a bundle is not found */ public Bundle select(String[] directories, String name, Locale locale) throws ComponentException { Bundle bundle = _select(directories, 0, name, locale); if (bundle == null) { throw new ComponentException(name, "Unable to locate resource: " + name); } return bundle; } public void release(Bundle bundle) { // Do nothing } // // Implementation // /** * Select a bundle based on bundle name and locale. * * @param directories catalogue location(s) * @param name bundle name * @param locale locale * @return the bundle */ private XMLResourceBundle _select(String[] directories, int index, String name, Locale locale) throws ComponentException { if (getLogger().isDebugEnabled()) { getLogger().debug("Selecting from: " + name + ", locale: " + locale + ", directory: " + directories[index]); } final String cacheKey = "XRB" + getCacheKey(directories, index, name, locale); XMLResourceBundle bundle = selectCached(cacheKey); if (bundle == null) { synchronized (this) { bundle = selectCached(cacheKey); if (bundle == null) { boolean localeAvailable = (locale != null && !locale.getLanguage().equals("")); index++; // Find parent bundle first XMLResourceBundle parent = null; if (localeAvailable && index == directories.length) { // all directories have been searched with this locale, // now start again with the first directory and the parent locale parent = _select(directories, 0, name, getParentLocale(locale)); } else if (index < directories.length) { // there are directories left to search for with this locale parent = _select(directories, index, name, locale); } // Create this bundle (if source exists) and pass parent to it. final String sourceURI = getSourceURI(directories[index - 1], name, locale); bundle = create(sourceURI, locale, parent); updateCache(cacheKey, bundle); } } } return bundle; } /** * Constructs new bundle. * * <p> * If there is a problem loading the bundle, created bundle will be empty. * * @param sourceURI source URI of the XML resource bundle * @param locale locale of the bundle * @param parent parent bundle, if any * @return the bundle */ protected XMLResourceBundle create(String sourceURI, Locale locale, XMLResourceBundle parent) { if (getLogger().isDebugEnabled()) { getLogger().debug("Creating bundle <" + sourceURI + ">"); } XMLResourceBundle bundle = new XMLResourceBundle(sourceURI, locale, parent); bundle.enableLogging(getLogger()); bundle.reload(this.manager, this.resolver, this.interval); return bundle; } /** * Returns the next locale up the parent hierarchy. * E.g. the parent of new Locale("en","us","mac") would be * new Locale("en", "us", ""). * * @param locale the locale * @return the parent locale */ protected Locale getParentLocale(Locale locale) { Locale newloc; if (locale.getVariant().length() == 0) { if (locale.getCountry().length() == 0) { newloc = new Locale("", "", ""); } else { newloc = new Locale(locale.getLanguage(), "", ""); } } else { newloc = new Locale(locale.getLanguage(), locale.getCountry(), ""); } return newloc; } /** * Creates a cache key for the bundle. * @return the cache key */ protected String getCacheKey(String[] directories, int index, String name, Locale locale) throws ComponentException { StringBuffer cacheKey = new StringBuffer(); if (index < directories.length) { cacheKey.append(":"); cacheKey.append(getSourceURI(directories[index], name, locale)); index++; cacheKey.append(getCacheKey(directories, index, name, locale)); } else if ((locale != null && !locale.getLanguage().equals(""))) { cacheKey.append(getCacheKey(directories, 0, name, getParentLocale(locale))); } return cacheKey.toString(); } /** * Maps a bundle name and locale to a bundle source URI. * If you need a different mapping, then just override this method. * * @param base the base URI for the catalogues * @param name the name of the catalogue * @param locale the locale of the bundle * @return the source URI for the bundle */ protected String getSourceURI(String base, String name, Locale locale) throws ComponentException { // If base is null default to the current location if (base == null) { base = ""; } // Resolve base URI Source src = null; Map parameters = Collections.EMPTY_MAP; StringBuffer sb = new StringBuffer(); try { src = this.resolver.resolveURI(base); // Deparameterize base URL before adding catalogue name String uri = NetUtils.deparameterize(src.getURI(), parameters = new HashMap(7)); // Append trailing slash sb.append(uri); if (!uri.endsWith("/")) { sb.append('/'); } } catch (IOException e) { throw new ComponentException("Cannot resolve catalogue base URI <" + base + ">", name, e); } finally { this.resolver.release(src); } // Append catalogue name sb.append(name); // Append catalogue locale if (locale != null) { if (!locale.getLanguage().equals("")) { sb.append("_"); sb.append(locale.getLanguage()); } if (!locale.getCountry().equals("")) { sb.append("_"); sb.append(locale.getCountry()); } if (!locale.getVariant().equals("")) { sb.append("_"); sb.append(locale.getVariant()); } } sb.append(".xml"); // Reconstruct complete bundle URI with parameters String uri = NetUtils.parameterize(sb.toString(), parameters); if (getLogger().isDebugEnabled()) { getLogger().debug("Resolved name: " + name + ", locale: " + locale + " --> " + uri); } return uri; } /** * Selects a bundle from the cache, and reloads it if needed. * * @param cacheKey caching key of the bundle * @return the cached bundle; null, if not found */ protected XMLResourceBundle selectCached(String cacheKey) { XMLResourceBundle bundle = (XMLResourceBundle) this.cache.get(cacheKey); if (bundle != null && this.interval != -1) { // Reload this bundle and all parent bundles, as necessary for (XMLResourceBundle b = bundle; b != null; b = (XMLResourceBundle) b.parent) { b.reload(this.manager, this.resolver, this.interval); } } return bundle; } /** * Stores bundle in the cache. * * @param cacheKey caching key of the bundle * @param bundle bundle to be placed in the cache */ protected void updateCache(String cacheKey, XMLResourceBundle bundle) { try { this.cache.store(cacheKey, bundle); } catch (IOException e) { getLogger().error("Bundle <" + bundle.getSourceURI() + ">: unable to store.", e); } } }