/** * Copyright (C) 2009 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.services.resources.impl; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import org.exoplatform.commons.cache.future.FutureCache; import org.exoplatform.commons.cache.future.FutureExoCache; import org.exoplatform.commons.cache.future.Loader; import org.exoplatform.commons.utils.IOUtil; import org.exoplatform.commons.utils.MapResourceBundle; import org.exoplatform.commons.utils.PageList; import org.exoplatform.commons.utils.PropertyManager; import org.exoplatform.container.xml.InitParams; import org.exoplatform.services.cache.ExoCache; import org.exoplatform.services.log.Log; import org.exoplatform.services.resources.ExoResourceBundle; import org.exoplatform.services.resources.IdentityResourceBundle; import org.exoplatform.services.resources.LocaleConfig; import org.exoplatform.services.resources.LocaleConfigService; import org.exoplatform.services.resources.PropertiesClassLoader; import org.exoplatform.services.resources.Query; import org.exoplatform.services.resources.ResourceBundleData; import org.exoplatform.services.resources.ResourceBundleService; import org.picocontainer.Startable; /** * Created by The eXo Platform SAS Mar 9, 2007 */ public abstract class BaseResourceBundleService implements ResourceBundleService, Startable { protected Log log_; protected volatile List<String> classpathResources_; protected volatile String[] portalResourceBundleNames_; protected LocaleConfigService localeService_; protected ExoCache<String, ResourceBundle> cache_; private volatile FutureCache<String, ResourceBundle, ResourceBundleContext> futureCache_; private final Loader<String, ResourceBundle, ResourceBundleContext> loader_ = new Loader<String, ResourceBundle, ResourceBundleContext>() { /** * {@inheritDoc} */ public ResourceBundle retrieve(ResourceBundleContext context, String key) throws Exception { return context.get(key); } }; private volatile List<String> initResources_; @SuppressWarnings("unchecked") protected void initParams(InitParams params) { classpathResources_ = params.getValuesParam("classpath.resources").getValues(); // resources name can use for portlets List prnames = params.getValuesParam("portal.resource.names").getValues(); portalResourceBundleNames_ = new String[prnames.size()]; for (int i = 0; i < prnames.size(); i++) { portalResourceBundleNames_[i] = (String) prnames.get(i); } initResources_ = params.getValuesParam("init.resources").getValues(); } /** * Add new resources bundles */ public synchronized void addResourceBundle(BaseResourceBundlePlugin plugin) { List<String> classpathResources = plugin.getClasspathResources(); if (classpathResources != null && !classpathResources.isEmpty()) { List<String> result = new ArrayList<String>(classpathResources); if (classpathResources_ != null) { result.addAll(classpathResources_); } this.classpathResources_ = Collections.unmodifiableList(result); } List<String> portalResources = plugin.getPortalResources(); if (portalResources != null && !portalResources.isEmpty()) { List<String> result = new ArrayList<String>(portalResources); if (portalResourceBundleNames_ != null) { result.addAll(Arrays.asList(portalResourceBundleNames_)); } this.portalResourceBundleNames_ = (String[]) result.toArray(new String[result.size()]); } List<String> initResources = plugin.getInitResources(); if (initResources != null && !initResources.isEmpty()) { List<String> result = new ArrayList<String>(initResources); if (initResources_ != null) { result.addAll(initResources_); } this.initResources_ = Collections.unmodifiableList(result); } } /** * Loads all the "init" resource bundles * * @see org.picocontainer.Startable#start() */ public void start() { PageList pl = null; try { pl = findResourceDescriptions(new Query(null, null)); } catch (Exception e) { throw new RuntimeException("Cannot check if a resource already exists", e); } if (pl.getAvailable() > 0) return; // init resources List<String> initResources = initResources_; for (String resource : initResources) { initResources(resource, Thread.currentThread().getContextClassLoader()); } } /** * @see org.picocontainer.Startable#stop() */ public void stop() { } public ResourceBundle getResourceBundle(String[] name, Locale locale) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return getResourceBundle(name, locale, cl); } public ResourceBundle getResourceBundle(String name, Locale locale) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return getResourceBundle(name, locale, cl); } public String[] getSharedResourceBundleNames() { return portalResourceBundleNames_; } public ResourceBundleData createResourceBundleDataInstance() { return new ResourceBundleData(); } protected boolean isClasspathResource(String name) { if (classpathResources_ == null) return false; for (int i = 0; i < classpathResources_.size(); i++) { String pack = classpathResources_.get(i); if (name.startsWith(pack)) return true; } return false; } protected void initResources(String baseName, ClassLoader cl) { String name = baseName.replace('.', '/'); try { Collection<LocaleConfig> localeConfigs = localeService_.getLocalConfigs(); // String defaultLang = // localeService_.getDefaultLocaleConfig().getLanguage(); Locale defaultLocale = localeService_.getDefaultLocaleConfig().getLocale(); for (Iterator<LocaleConfig> iter = localeConfigs.iterator(); iter.hasNext();) { LocaleConfig localeConfig = iter.next(); // String language = localeConfig.getLanguage(); // String content = getResourceBundleContent(name, language, // defaultLang, cl); Locale locale = localeConfig.getLocale(); String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); String content = getResourceBundleContent(name, locale, defaultLocale, cl); if (content != null) { // save the content ResourceBundleData data = new ResourceBundleData(); data.setName(baseName); data.setLanguage(language); data.setCountry(country); data.setVariant(variant); data.setData(content); saveResourceBundle(data); } } } catch (Exception ex) { log_.error("Error while reading the resource bundle : " + baseName, ex); } } protected String getResourceBundleContent(String name, String language, String defaultLang, ClassLoader cl) throws Exception { String fileName = null; try { cl = new PropertiesClassLoader(cl, true); fileName = name + "_" + language + ".properties"; URL url = cl.getResource(fileName); if (url == null && defaultLang.equals(language)) { url = cl.getResource(name + ".properties"); } if (url != null) { InputStream is = url.openStream(); try { byte[] buf = IOUtil.getStreamContentAsBytes(is); return new String(buf, "UTF-8"); } finally { try { is.close(); } catch (IOException e) { // Do nothing } } } } catch (Exception e) { throw new Exception("Error while reading the file: " + fileName, e); } return null; } /** * This method is used for country support * * @param baseName * @param locale * @param defaultLocale * @param cl * @return */ protected String getResourceBundleContent(String baseName, Locale locale, Locale defaultLocale, ClassLoader cl) throws Exception { List<String> candidateFiles = new ArrayList<String>(); String language = locale.getLanguage(); String country = locale.getCountry().toUpperCase(); String variant = locale.getVariant(); String defaultLanguage = defaultLocale.getLanguage(); String defaultCountry = defaultLocale.getCountry().toUpperCase(); String defaultVariant = defaultLocale.getVariant(); if (variant != null && variant.length() > 0) { candidateFiles.add(baseName + "_" + language + "_" + country + "_" + variant + ".properties"); } if (country != null && country.length() > 0) { candidateFiles.add(baseName + "_" + language + "_" + country + ".properties"); } if (language != null && language.length() > 0) { candidateFiles.add(baseName + "_" + language + ".properties"); } if (defaultVariant != null && defaultVariant.length() > 0) { candidateFiles.add(baseName + "_" + defaultLanguage + "_" + defaultCountry + "_" + defaultVariant + ".properties"); } if (defaultCountry != null && defaultCountry.length() > 0) { candidateFiles.add(baseName + "_" + defaultLanguage + "_" + defaultCountry + ".properties"); } if (defaultLanguage != null && defaultLanguage.length() > 0) { candidateFiles.add(baseName + "_" + defaultLanguage + ".properties"); } candidateFiles.add(baseName + ".properties"); //fallback to en locale help to remove ant script that generates base properties file candidateFiles.add(baseName + "_" + Locale.ENGLISH.getLanguage() + ".properties"); cl = new PropertiesClassLoader(cl, true); String fileName = null; try { URL url = null; for (String candidateFile : candidateFiles) { url = cl.getResource(candidateFile); if (url != null) { fileName = candidateFile; break; } } if (url != null) { InputStream is = url.openStream(); try { byte[] buf = IOUtil.getStreamContentAsBytes(is); return new String(buf, "UTF-8"); } finally { try { is.close(); } catch (IOException e) { // Do nothing } } } } catch (Exception ex) { throw new Exception("Error while reading the file: " + fileName, ex); } return null; } /** * Invalidate an entry in the cache at this level. Normally this is called by the subclass. * * @param name the bundle name */ protected final void invalidate(String name) { cache_.remove(name); } public ResourceBundle getResourceBundle(String name, Locale locale, ClassLoader cl) { if (IdentityResourceBundle.MAGIC_LANGUAGE.equals(locale.getLanguage())) { return IdentityResourceBundle.getInstance(); } String country = locale.getCountry(); String variant = locale.getVariant(); String id; if (variant != null && variant.length() > 0) { id = name + "_" + locale.getLanguage() + "_" + country + "_" + variant; } else if (country != null && country.length() > 0) { id = name + "_" + locale.getLanguage() + "_" + locale.getCountry(); } else { id = name + "_" + locale.getLanguage(); } boolean isClasspathResource = isClasspathResource(name); boolean isCacheable = !isClasspathResource || !PropertyManager.isDevelopping(); if (isCacheable && isClasspathResource) { // Avoid naming collision id += "_" + cl.getClass() + "_" + System.identityHashCode(cl); } // Case 1: ResourceBundle of portlets, standard java API is used if (isClasspathResource) { ResourceBundleFromCPContext ctx = new ResourceBundleFromCPContext(name, locale, cl); ResourceBundle result; // Cache classpath resource bundle while running portal in non-dev mode if (isCacheable) { result = getFutureCache().get(ctx, id); } else { result = ctx.get(id); } if (ctx.e != null) { // Throw the RuntimeException if it occurs to remain compatible with the old behavior throw ctx.e; } else { return result; } } else { // Case 2: ResourceBundle of portal return getFutureCache().get(new GetResourceBundleFromDbContext(name, locale), id); } } public ResourceBundle getResourceBundle(String[] name, Locale locale, ClassLoader cl) { if (IdentityResourceBundle.MAGIC_LANGUAGE.equals(locale.getLanguage())) { return IdentityResourceBundle.getInstance(); } StringBuilder idBuf = new StringBuilder("merge:"); for (String n : name) idBuf.append(n).append("_"); idBuf.append(locale); String id = idBuf.toString(); return getFutureCache().get(new GetResourceBundleContext(name, locale, cl), id); } protected FutureCache<String, ResourceBundle, ResourceBundleContext> getFutureCache() { if (futureCache_ == null) { synchronized (this) { if (futureCache_ == null) { futureCache_ = new FutureExoCache<String, ResourceBundle, ResourceBundleContext>(loader_, cache_); } } } return futureCache_; } protected abstract ResourceBundle getResourceBundleFromDb(String id, ResourceBundle parent, Locale locale) throws Exception; /** * Generic class defining a context needed to get a ResourceBundle */ private abstract static class ResourceBundleContext { /** * Get the resource bundle corresponding to the context */ abstract ResourceBundle get(String id); } /** * The class defining the context required to load a ResourceBundle from classpath */ private class ResourceBundleFromCPContext extends ResourceBundleContext { private final String name; private final Locale locale; private final ClassLoader cl; private RuntimeException e; public ResourceBundleFromCPContext(String name, Locale locale, ClassLoader cl) { this.name = name; this.locale = locale; this.cl = cl; } /** * {@inheritDoc} */ @Override ResourceBundle get(String id) { ResourceBundle parent = null, result = null; try { Locale defaultLocale = localeService_.getDefaultLocaleConfig().getLocale(); String rootId = name + "_" + defaultLocale.getLanguage(); parent = getContent(rootId, null, defaultLocale); result = getContent(name, parent, defaultLocale); } catch (Exception e) { this.e = new RuntimeException(e); } if (result != null) { return result; } else { return parent; } } private ResourceBundle getContent(String _name, ResourceBundle parent, Locale defaultLocale) throws Exception { String content = getResourceBundleContent(_name.replace('.', '/'), locale, defaultLocale, cl); if (content != null) { ResourceBundleData data = new ResourceBundleData(); data.setName(_name); data.setLanguage(locale.getLanguage()); data.setCountry(locale.getCountry()); data.setVariant(locale.getVariant()); data.setData(content); return new ExoResourceBundle(data, parent); } return null; } } /** * The class defining the context required to load a ResourceBundle thanks to the method * <code>getResourceBundleFromDb(String id, ResourceBundle parent, Locale locale)</code> */ private class GetResourceBundleFromDbContext extends ResourceBundleContext { private final String name; private final Locale locale; public GetResourceBundleFromDbContext(String name, Locale locale) { this.name = name; this.locale = locale; } /** * {@inheritDoc} */ @Override ResourceBundle get(String id) { ResourceBundle res = null; try { String rootId = name + "_" + localeService_.getDefaultLocaleConfig().getLanguage(); ResourceBundle parent = getResourceBundleFromDb(rootId, null, locale); res = getResourceBundleFromDb(id, parent, locale); if (res == null) { res = parent; } } catch (Exception ex) { log_.error("Error: " + id, ex); } return res; } } /** * The class defining the context required to load a ResourceBundle thanks to the method * <code>getResourceBundle(String[] name, Locale locale, ClassLoader cl)</code> */ private class GetResourceBundleContext extends ResourceBundleContext { private final String[] name; private final Locale locale; private final ClassLoader cl; public GetResourceBundleContext(String[] name, Locale locale, ClassLoader cl) { this.name = name; this.locale = locale; this.cl = cl; } /** * {@inheritDoc} */ @Override ResourceBundle get(String id) { MapResourceBundle outputBundled = null; try { outputBundled = new MapResourceBundle(locale); for (int i = 0; i < name.length; i++) { ResourceBundle temp = getResourceBundle(name[i], locale, cl); if (temp != null) { outputBundled.merge(temp); continue; } log_.warn("Cannot load and merge the bundle: " + name[i]); } outputBundled.resolveDependencies(); } catch (Exception ex) { log_.error("Cannot load and merge the bundle: " + id, ex); } return outputBundled; } } }