/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program 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 3 of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.core.i18n;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.io.IOUtils;
import org.picocontainer.Startable;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.i18n.I18n;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.SonarException;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginRepository;
@ScannerSide
@ServerSide
@ComputeEngineSide
public class DefaultI18n implements I18n, Startable {
private static final Logger LOG = Loggers.get(DefaultI18n.class);
private static final String BUNDLE_PACKAGE = "org.sonar.l10n.";
private final PluginRepository pluginRepository;
private final ResourceBundle.Control control;
private final System2 system2;
// the following fields are available after startup
private ClassLoader classloader;
private Map<String, String> propertyToBundles;
public DefaultI18n(PluginRepository pluginRepository, System2 system2) {
this.pluginRepository = pluginRepository;
this.system2 = system2;
// SONAR-2927
this.control = new ResourceBundle.Control() {
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
Preconditions.checkNotNull(baseName);
Locale defaultLocale = Locale.ENGLISH;
return locale.equals(defaultLocale) ? null : defaultLocale;
}
};
}
@Override
public void start() {
doStart(new I18nClassloader(pluginRepository));
}
@VisibleForTesting
void doStart(ClassLoader classloader) {
this.classloader = classloader;
this.propertyToBundles = new HashMap<>();
// org.sonar.l10n.core bundle is provided by sonar-core module
initPlugin("core");
Collection<PluginInfo> infos = pluginRepository.getPluginInfos();
for (PluginInfo plugin : infos) {
initPlugin(plugin.getKey());
}
LOG.debug("Loaded {} properties from l10n bundles", propertyToBundles.size());
}
private void initPlugin(String pluginKey) {
try {
String bundleKey = BUNDLE_PACKAGE + pluginKey;
ResourceBundle bundle = ResourceBundle.getBundle(bundleKey, Locale.ENGLISH, this.classloader, control);
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
propertyToBundles.put(key, bundleKey);
}
} catch (MissingResourceException e) {
// ignore
}
}
@Override
public void stop() {
if (classloader instanceof Closeable) {
IOUtils.closeQuietly((Closeable) classloader);
}
classloader = null;
propertyToBundles = null;
}
@Override
@CheckForNull
public String message(Locale locale, String key, @Nullable String defaultValue, Object... parameters) {
String bundleKey = propertyToBundles.get(key);
String value = null;
if (bundleKey != null) {
try {
ResourceBundle resourceBundle = ResourceBundle.getBundle(bundleKey, locale, classloader, control);
value = resourceBundle.getString(key);
} catch (MissingResourceException e1) {
// ignore
}
}
if (value == null) {
value = defaultValue;
}
return formatMessage(value, parameters);
}
@Override
public String age(Locale locale, long durationInMillis) {
DurationLabel.Result duration = DurationLabel.label(durationInMillis);
return message(locale, duration.key(), null, duration.value());
}
@Override
public String age(Locale locale, Date fromDate, Date toDate) {
return age(locale, toDate.getTime() - fromDate.getTime());
}
@Override
public String ageFromNow(Locale locale, Date date) {
return age(locale, system2.now() - date.getTime());
}
/**
* Format date for the given locale. JVM timezone is used.
*/
@Override
public String formatDateTime(Locale locale, Date date) {
return DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT, locale).format(date);
}
@Override
public String formatDate(Locale locale, Date date) {
return DateFormat.getDateInstance(DateFormat.DEFAULT, locale).format(date);
}
@Override
public String formatDouble(Locale locale, Double value) {
NumberFormat format = DecimalFormat.getNumberInstance(locale);
format.setMinimumFractionDigits(1);
format.setMaximumFractionDigits(1);
return format.format(value);
}
@Override
public String formatInteger(Locale locale, Integer value) {
return NumberFormat.getNumberInstance(locale).format(value);
}
/**
* Only the given locale is searched. Contrary to java.util.ResourceBundle, no strategy for locating the bundle is implemented in
* this method.
*/
String messageFromFile(Locale locale, String filename, String relatedProperty) {
String result = null;
String bundleBase = propertyToBundles.get(relatedProperty);
if (bundleBase == null) {
// this property has no translation
return null;
}
String filePath = bundleBase.replace('.', '/');
if (!"en".equals(locale.getLanguage())) {
filePath += "_" + locale.getLanguage();
}
filePath += "/" + filename;
InputStream input = classloader.getResourceAsStream(filePath);
if (input != null) {
result = readInputStream(filePath, input);
}
return result;
}
private static String readInputStream(String filePath, InputStream input) {
String result;
try {
result = IOUtils.toString(input, "UTF-8");
} catch (IOException e) {
throw new SonarException("Fail to load file: " + filePath, e);
} finally {
IOUtils.closeQuietly(input);
}
return result;
}
public Set<String> getPropertyKeys() {
return propertyToBundles.keySet();
}
public Locale getEffectiveLocale(Locale locale) {
Locale bundleLocale = ResourceBundle.getBundle(BUNDLE_PACKAGE + "core", locale, this.classloader, this.control).getLocale();
locale.getISO3Language();
return bundleLocale.getLanguage().isEmpty() ? Locale.ENGLISH : bundleLocale;
}
@CheckForNull
private static String formatMessage(@Nullable String message, Object... parameters) {
if (message == null || parameters.length == 0) {
return message;
}
return MessageFormat.format(message.replaceAll("'", "''"), parameters);
}
}