blob: b971930defc9b5f956384288d477175913e90187 [file] [log] [blame]
/*
* 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);
}
}
}