001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * Sonar is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019 */
020package org.sonar.core.i18n;
021
022import com.google.common.collect.Maps;
023import org.apache.commons.io.IOUtils;
024import org.apache.commons.lang.StringUtils;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027import org.sonar.api.ServerExtension;
028import org.sonar.api.i18n.I18n;
029import org.sonar.api.platform.PluginMetadata;
030import org.sonar.api.platform.PluginRepository;
031import org.sonar.api.utils.SonarException;
032
033import java.io.IOException;
034import java.io.InputStream;
035import java.text.MessageFormat;
036import java.util.*;
037
038public class I18nManager implements I18n, ServerExtension {
039  private static final Logger LOG = LoggerFactory.getLogger(I18nManager.class);
040
041  public static final String ENGLISH_PACK_PLUGIN_KEY = "l10nen";
042  public static final String BUNDLE_PACKAGE = "org.sonar.l10n.";
043
044  private PluginRepository pluginRepository;
045  private Map<String, ClassLoader> bundleToClassloaders;
046  private Map<String, String> propertyToBundles;
047  private ClassLoader languagePackClassLoader;
048  private Map<String, Map<Locale, String>> fileContentCache = Maps.newHashMap();
049
050  public I18nManager(PluginRepository pluginRepository) {
051    this.pluginRepository = pluginRepository;
052  }
053
054  I18nManager(Map<String, ClassLoader> bundleToClassloaders) {
055    this.bundleToClassloaders = bundleToClassloaders;
056  }
057
058  public void start() {
059    initClassloaders();
060    initProperties();
061  }
062
063  private void initClassloaders() {
064    if (bundleToClassloaders == null) {
065      languagePackClassLoader = pluginRepository.getPlugin(ENGLISH_PACK_PLUGIN_KEY).getClass().getClassLoader();
066      bundleToClassloaders = Maps.newHashMap();
067      for (PluginMetadata metadata : pluginRepository.getMetadata()) {
068        if (!metadata.isCore() && !ENGLISH_PACK_PLUGIN_KEY.equals(metadata.getBasePlugin())) {
069          // plugin but not a language pack
070          // => plugins embedd only their own bundles with all locales
071          ClassLoader classLoader = pluginRepository.getPlugin(metadata.getKey()).getClass().getClassLoader();
072          bundleToClassloaders.put(BUNDLE_PACKAGE + metadata.getKey(), classLoader);
073
074        } else if (metadata.isCore()) {
075          // bundles of core plugins are defined into language packs. All language packs are supposed
076          // to share the same classloader (english pack classloader)
077          bundleToClassloaders.put(BUNDLE_PACKAGE + metadata.getKey(), languagePackClassLoader);
078        }
079      }
080    }
081    bundleToClassloaders = Collections.unmodifiableMap(bundleToClassloaders);
082  }
083
084  private void initProperties() {
085    propertyToBundles = Maps.newHashMap();
086    for (Map.Entry<String, ClassLoader> entry : bundleToClassloaders.entrySet()) {
087      try {
088        String bundleKey = entry.getKey();
089        ResourceBundle bundle = ResourceBundle.getBundle(bundleKey, Locale.ENGLISH, entry.getValue());
090        Enumeration<String> keys = bundle.getKeys();
091        while (keys.hasMoreElements()) {
092          String key = keys.nextElement();
093          propertyToBundles.put(key, bundleKey);
094        }
095      } catch (MissingResourceException e) {
096        // ignore
097      }
098    }
099    propertyToBundles = Collections.unmodifiableMap(propertyToBundles);
100    LOG.debug(String.format("Loaded %d properties from English bundles", propertyToBundles.size()));
101  }
102
103  public String message(Locale locale, String key, String defaultValue, Object... parameters) {
104    String bundleKey = propertyToBundles.get(key);
105    ResourceBundle resourceBundle = getBundle(bundleKey, locale);
106    return message(resourceBundle, key, defaultValue, parameters);
107  }
108
109  /**
110   * Only the given locale is searched. Contrary to java.util.ResourceBundle, no strategy for locating the bundle is implemented in
111   * this method.
112   */
113  String messageFromFile(Locale locale, String filename, String relatedProperty, boolean keepInCache) {
114    Map<Locale, String> fileCache = fileContentCache.get(filename);
115    if (fileCache != null && fileCache.containsKey(locale)) {
116      return fileCache.get(locale);
117    }
118
119    ClassLoader classloader = getClassLoaderForProperty(relatedProperty);
120    String result = null;
121    if (classloader != null) {
122      String bundleBase = propertyToBundles.get(relatedProperty);
123      String filePath = bundleBase.replace('.', '/');
124      if (!"en".equals(locale.getLanguage())) {
125        filePath += "_" + locale.getLanguage();
126      }
127      filePath += "/" + filename;
128      InputStream input = classloader.getResourceAsStream(filePath);
129      if (input != null) {
130        try {
131          result = IOUtils.toString(input, "UTF-8");
132
133        } catch (IOException e) {
134          throw new SonarException("Fail to load file: " + filePath, e);
135        } finally {
136          IOUtils.closeQuietly(input);
137        }
138      }
139
140      if (keepInCache) {
141        if (fileCache == null) {
142          fileCache = Maps.newHashMap();
143          fileContentCache.put(filename, fileCache);
144        }
145        // put null value for negative caching.
146        fileCache.put(locale, result);
147      }
148    }
149    return result;
150  }
151
152  Set<String> getPropertyKeys() {
153    return propertyToBundles.keySet();
154  }
155
156  ResourceBundle getBundle(String bundleKey, Locale locale) {
157    try {
158      ClassLoader classloader = bundleToClassloaders.get(bundleKey);
159      if (classloader != null) {
160        return ResourceBundle.getBundle(bundleKey, locale, classloader);
161      }
162    } catch (MissingResourceException e) {
163      // ignore
164    }
165    return null;
166  }
167
168
169  ClassLoader getClassLoaderForProperty(String propertyKey) {
170    String bundleKey = propertyToBundles.get(propertyKey);
171    return (bundleKey != null ? bundleToClassloaders.get(bundleKey) : null);
172  }
173
174  String message(ResourceBundle resourceBundle, String key, String defaultValue, Object... parameters) {
175    String value = null;
176    if (resourceBundle != null) {
177      try {
178        value = resourceBundle.getString(key);
179      } catch (MissingResourceException e) {
180        // ignore
181      }
182    }
183    if (value == null) {
184      value = defaultValue;
185    }
186    if (value != null && parameters.length > 0) {
187      return MessageFormat.format(value, parameters);
188    }
189    return value;
190  }
191
192  String extractBundleFromKey(String key) {
193    String bundleKey = BUNDLE_PACKAGE + StringUtils.substringBefore(key, ".");
194    if (bundleToClassloaders.containsKey(bundleKey)) {
195      return bundleKey;
196    }
197    return BUNDLE_PACKAGE + "core";
198  }
199
200  ClassLoader getLanguagePackClassLoader() {
201    return languagePackClassLoader;
202  }
203
204  Map<String, Map<Locale, String>> getFileContentCache() {
205    return fileContentCache;
206  }
207}