diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2011-07-29 12:09:43 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2011-07-29 12:09:43 +0200 |
commit | 402f8958a403d94422f38d47cc39aba0f13e8aae (patch) | |
tree | e4bac52b9a2927d7e994d9961bacf6d880611db7 | |
parent | 15f5d562a3da55f3702500c1c855f3f9d7a52e2d (diff) | |
download | sonarqube-402f8958a403d94422f38d47cc39aba0f13e8aae.tar.gz sonarqube-402f8958a403d94422f38d47cc39aba0f13e8aae.zip |
SONAR-75 Improve i18n API
- The extension point LanguagePack is not required anymore
- No error logs when rule description is not available in all locales
- Increase code coverage and decrease complexity
43 files changed, 612 insertions, 932 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index bf045ccbcc7..97b8e06428c 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -34,7 +34,6 @@ import org.sonar.plugins.core.charts.XradarChart; import org.sonar.plugins.core.colorizers.JavaColorizerFormat; import org.sonar.plugins.core.duplicationsviewer.DuplicationsViewerDefinition; import org.sonar.plugins.core.hotspots.Hotspots; -import org.sonar.plugins.core.i18n.I18nManager; import org.sonar.plugins.core.metrics.UserManagedMetrics; import org.sonar.plugins.core.security.ApplyProjectRolesDecorator; import org.sonar.plugins.core.sensors.*; @@ -241,9 +240,6 @@ public class CorePlugin extends SonarPlugin { extensions.add(NewCoverageFileAnalyzer.class); extensions.add(NewCoverageAggregator.class); - // i18n - extensions.add(I18nManager.class); -// extensions.add(org.sonar.core.i18n.I18nManager.class); return extensions; } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/I18nManager.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/I18nManager.java deleted file mode 100644 index b3cd4db85ac..00000000000 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/I18nManager.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import org.apache.commons.collections.EnumerationUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.ServerExtension; -import org.sonar.api.i18n.I18n; -import org.sonar.api.i18n.LanguagePack; -import org.sonar.api.platform.PluginRepository; -import org.sonar.api.utils.SonarException; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.text.MessageFormat; -import java.util.*; - -public final class I18nManager implements I18n, ServerExtension { - - private static final Logger LOG = LoggerFactory.getLogger(I18nManager.class); - public static final String packagePathToSearchIn = "org/sonar/i18n"; - - private PluginRepository pluginRepository; - private LanguagePack[] languagePacks; - private Map<String, String> keyToBundles = Maps.newHashMap(); - private BundleClassLoader bundleClassLoader = new BundleClassLoader(); - private List<Locale> registeredLocales = Lists.newArrayList(); - - public I18nManager(PluginRepository pluginRepository, LanguagePack[] languagePacks) { - this.pluginRepository = pluginRepository; - this.languagePacks = languagePacks; - } - - public I18nManager(PluginRepository pluginRepository) { - this(pluginRepository, new LanguagePack[0]); - } - - public void start() { - doStart(InstalledPlugin.create(pluginRepository)); - } - - void doStart(List<InstalledPlugin> installedPlugins) { - LOG.info("Loading i18n bundles"); - Set<URI> alreadyLoadedResources = Sets.newHashSet(); - LanguagePack englishPack = findEnglishPack(); - - for (InstalledPlugin plugin : installedPlugins) { - // look first in the classloader of the English Language Pack - searchAndStoreBundleNames(plugin.key, englishPack.getClass().getClassLoader(), alreadyLoadedResources); - // then look in the classloader of the plugin - searchAndStoreBundleNames(plugin.key, plugin.classloader, alreadyLoadedResources); - } - - for (LanguagePack pack : languagePacks) { - if (!pack.equals(englishPack)) { - addLanguagePack(pack); - } - } - } - - protected LanguagePack findEnglishPack() { - LanguagePack englishPack = null; - for (LanguagePack pack : languagePacks) { - if (pack.getLocales().contains(Locale.ENGLISH)) { - englishPack = pack; - break; - } - } - if (englishPack == null) { - throw new SonarException("The I18n English Pack was not found."); - } - return englishPack; - } - - protected void addLanguagePack(LanguagePack languagePack) { - LOG.debug("Search for bundles in language pack : {}", languagePack); - for (String pluginKey : languagePack.getPluginKeys()) { - String bundleBaseName = buildBundleBaseName(pluginKey); - for (Locale locale : languagePack.getLocales()) { - String bundlePropertiesFile = new StringBuilder(bundleBaseName).append('_').append(locale.toString()).append(".properties").toString(); - ClassLoader classloader = languagePack.getClass().getClassLoader(); - LOG.info("Adding locale {} for bundleName : {} from classloader : {}", new Object[]{locale, bundleBaseName, classloader}); - bundleClassLoader.addResource(bundlePropertiesFile, classloader); - registeredLocales.add(locale); - } - } - } - - protected String buildBundleBaseName(String pluginKey) { - return packagePathToSearchIn + "/" + pluginKey; - } - - @SuppressWarnings("unchecked") - protected void searchAndStoreBundleNames(String pluginKey, ClassLoader classloader, Set<URI> alreadyLoadedResources) { - String bundleBaseName = buildBundleBaseName(pluginKey); - String bundleDefaultPropertiesFile = bundleBaseName + ".properties"; - try { - LOG.debug("Search for ResourceBundle base file '" + bundleDefaultPropertiesFile + "' in the classloader : " + classloader); - List<URL> resources = EnumerationUtils.toList(classloader.getResources(bundleDefaultPropertiesFile)); - if (resources.size() > 0) { - if (resources.size() > 1) { - LOG.debug("File '{}' found several times in the classloader : {}. Only the first one will be taken in account.", - bundleDefaultPropertiesFile, classloader); - } - - URL propertiesUrl = resources.get(0); - if (!alreadyLoadedResources.contains(propertiesUrl.toURI())) { - LOG.debug("Found the ResourceBundle base file : {} from classloader : {}", propertiesUrl, classloader); - LOG.debug("Add bundleName : {} from classloader : {}", bundleBaseName, classloader); - bundleClassLoader.addResource(bundleDefaultPropertiesFile, classloader); - alreadyLoadedResources.add(propertiesUrl.toURI()); - - Properties bundleContent = new Properties(); - InputStream input = null; - try { - input = propertiesUrl.openStream(); - bundleContent.load(input); - Enumeration<String> keysToAdd = (Enumeration<String>) bundleContent.propertyNames(); - while (keysToAdd.hasMoreElements()) { - String key = keysToAdd.nextElement(); - if (keyToBundles.containsKey(key)) { - LOG.debug("DUPLICATE KEY : Key '{}' defined in bundle '{}' is already defined in bundle '{}'. It is ignored.", - new Object[]{key, bundleBaseName, keyToBundles.get(key)}); - } else { - keyToBundles.put(key, bundleBaseName); - } - } - } finally { - IOUtils.closeQuietly(input); - } - } - } - } catch (Exception e) { - LOG.error("Fail to load '" + bundleDefaultPropertiesFile + "' in classloader : " + classloader, e); - throw new SonarException("Fail to load '" + bundleDefaultPropertiesFile + "' in classloader : " + classloader, e); - } - } - - public String message(final Locale locale, final String key, final String defaultText, final Object... objects) { - String translatedMessage = defaultText; - try { - if (isKeyForRuleDescription(key)) { - // Rule descriptions are in HTML files, not in regular bundles - translatedMessage = findRuleDescription(locale, key, defaultText); - } else { - translatedMessage = findStandardMessage(locale, key, defaultText, objects); - } - } catch (MissingResourceException e) { - if (translatedMessage == null) { - // when no translation has been found, the key is returned - return key; - } - } catch (Exception e) { - LOG.error("Exception when retrieving I18n string: ", e); - if (translatedMessage == null) { - // when no translation has been found, the key is returned - return key; - } - } - return translatedMessage; - } - - protected boolean isKeyForRuleDescription(String key) { - return StringUtils.startsWith(key, "rule.") && StringUtils.endsWith(key, ".description"); - } - - protected String findRuleDescription(final Locale locale, final String ruleDescriptionKey, final String defaultText) throws IOException { - String translation = defaultText; - String ruleNameKey = ruleDescriptionKey.replace(".description", ".name"); - - String bundleBaseName = keyToBundles.get(ruleNameKey); - if (bundleBaseName == null) { - handleMissingBundle(ruleNameKey, defaultText, bundleBaseName); - } else { - InputStream stream = extractRuleDescription(locale, ruleDescriptionKey, bundleBaseName); - if (stream == null && !Locale.ENGLISH.equals(locale)) { - // let's try in the ENGLISH bundles - stream = extractRuleDescription(Locale.ENGLISH, ruleDescriptionKey, bundleBaseName); - } - if (stream == null) { - // Definitely, no HTML file found for the description of this rule... - throw new MissingResourceException("MISSING RULE DESCRIPTION : HTML file not found in any bundle. Default value is returned.", - bundleBaseName, ruleDescriptionKey); - } - translation = IOUtils.toString(stream, "UTF-8"); - } - - return translation; - } - - private InputStream extractRuleDescription(final Locale locale, final String ruleDescriptionKey, String bundleBaseName) { - Locale localeToUse = defineLocaleToUse(locale); - String htmlFilePath = computeHtmlFilePath(bundleBaseName, ruleDescriptionKey, localeToUse); - ClassLoader classLoader = bundleClassLoader.getClassLoaderForBundle(bundleBaseName, localeToUse); - InputStream stream = classLoader.getResourceAsStream(htmlFilePath); - return stream; - } - - protected Locale defineLocaleToUse(final Locale locale) { - Locale localeToUse = locale; - if (!registeredLocales.contains(locale)) { - localeToUse = Locale.ENGLISH; - } - return localeToUse; - } - - String extractRuleKeyFromDescriptionProperty(String ruleDescriptionKey) { - // format is "rule.<plugin>.<key>.description" - String s = StringUtils.substringAfter(ruleDescriptionKey, "rule."); - return StringUtils.substringBetween(s, ".", ".description"); - } - - protected String computeHtmlFilePath(String bundleBaseName, String ruleDescriptionKey, Locale locale) { - String ruleName = extractRuleKeyFromDescriptionProperty(ruleDescriptionKey); - if (Locale.ENGLISH.equals(locale)) { - return bundleBaseName + "/" + ruleName + ".html"; - } else { - return bundleBaseName + "_" + locale.toString() + "/" + ruleName + ".html"; - } - } - - protected String findStandardMessage(final Locale locale, final String key, final String defaultText, final Object... objects) { - String translation = defaultText; - - String bundleBaseName = keyToBundles.get(key); - if (bundleBaseName == null) { - handleMissingBundle(key, defaultText, bundleBaseName); - } else { - try { - ResourceBundle bundle = ResourceBundle.getBundle(bundleBaseName, locale, bundleClassLoader); - - String value = bundle.getString(key); - if ("".equals(value)) { - if (translation == null) { - throw new MissingResourceException("VOID KEY : Key '" + key + "' (from bundle '" + bundleBaseName + "') returns a void value.", - bundleBaseName, key); - } - } else { - translation = value; - } - } catch (MissingResourceException e) { - if (translation == null) { - throw e; - } - } - } - - if (objects.length > 0) { - return MessageFormat.format(translation, objects); - } - return translation; - } - - protected void handleMissingBundle(final String key, final String defaultText, String bundleBaseName) { - if (defaultText == null) { - throw new MissingResourceException("UNKNOWN KEY : Key '" + key - + "' not found in any bundle, and no default value provided. The key is returned.", bundleBaseName, key); - } - } - - private static class BundleClassLoader extends URLClassLoader { - private Map<String, ClassLoader> resources = Maps.newHashMap(); - - public BundleClassLoader() { - super(new URL[]{}, null); - } - - public void addResource(String resourceName, ClassLoader classloader) { - resources.put(resourceName, classloader); - } - - public ClassLoader getClassLoaderForBundle(String bundleBaseName, Locale locale) { - StringBuilder resourceName = new StringBuilder(bundleBaseName); - if (locale != null && !locale.equals(Locale.ENGLISH)) { - resourceName.append("_"); - resourceName.append(locale); - } - resourceName.append(".properties"); - return resources.get(resourceName.toString()); - } - - @Override - public URL findResource(String name) { - if (resources.containsKey(name)) { - return resources.get(name).getResource(name); - } - return null; - } - } -} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/InstalledPlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/InstalledPlugin.java deleted file mode 100644 index abe4b3cf0de..00000000000 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/i18n/InstalledPlugin.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import com.google.common.collect.Lists; -import org.sonar.api.Plugin; -import org.sonar.api.platform.PluginMetadata; -import org.sonar.api.platform.PluginRepository; - -import java.util.List; - -class InstalledPlugin { - String key; - ClassLoader classloader; - - InstalledPlugin(String key, ClassLoader classloader) { - this.key = key; - this.classloader = classloader; - } - - static List<InstalledPlugin> create(PluginRepository pluginRepository) { - List<InstalledPlugin> result = Lists.newArrayList(); - for (PluginMetadata metadata : pluginRepository.getMetadata()) { - Plugin entryPoint = pluginRepository.getPlugin(metadata.getKey()); - result.add(new InstalledPlugin(metadata.getKey(), entryPoint.getClass().getClassLoader())); - } - return result; - } -} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/EnglishLanguagePack.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/EnglishLanguagePack.java deleted file mode 100644 index 0660fd60527..00000000000 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/EnglishLanguagePack.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import org.sonar.api.i18n.LanguagePack; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public class EnglishLanguagePack extends LanguagePack { - - @Override - public List<String> getPluginKeys() { - return Arrays.asList("test"); - } - - @Override - public List<Locale> getLocales() { - return Arrays.asList(Locale.ENGLISH); - } -}
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/FrenchLanguagePack.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/FrenchLanguagePack.java deleted file mode 100644 index e781f5a891f..00000000000 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/FrenchLanguagePack.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import org.sonar.api.i18n.LanguagePack; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public class FrenchLanguagePack extends LanguagePack { - - @Override - public List<String> getPluginKeys() { - return Arrays.asList("test"); - } - - @Override - public List<Locale> getLocales() { - return Arrays.asList(Locale.FRENCH); - } -}
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/I18nManagerTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/I18nManagerTest.java deleted file mode 100644 index 66c8ad17a31..00000000000 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/I18nManagerTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import com.google.common.collect.Lists; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.i18n.LanguagePack; -import org.sonar.api.platform.PluginRepository; - -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; - -public class I18nManagerTest { - - public static String ENGLISH_PACK_CLASS_NAME = "org.sonar.plugins.core.i18n.EnglishLanguagePack"; - public static String FRENCH_PACK_CLASS_NAME = "org.sonar.plugins.core.i18n.FrenchLanguagePack"; - public static String QUEBEC_PACK_CLASS_NAME = "org.sonar.plugins.core.i18n.QuebecLanguagePack"; - - private static URL classSource = I18nManagerTest.class.getProtectionDomain().getCodeSource().getLocation(); - private I18nManager manager; - - @Before - public void createManager() throws Exception { - List<InstalledPlugin> plugins = Lists.newArrayList(new InstalledPlugin("test", getClass().getClassLoader()), new InstalledPlugin( - "fake1", getClass().getClassLoader()), new InstalledPlugin("fake2", getClass().getClassLoader())); - - TestClassLoader englishPackClassLoader = new TestClassLoader(getClass().getClassLoader().getResource("I18n/EnglishPlugin.jar")); - LanguagePack englishPack = (LanguagePack) englishPackClassLoader.loadClass(ENGLISH_PACK_CLASS_NAME).newInstance(); - - TestClassLoader frenchPackClassLoader = new TestClassLoader(getClass().getClassLoader().getResource("I18n/FrenchPlugin.jar")); - LanguagePack frenchPack = (LanguagePack) frenchPackClassLoader.loadClass(FRENCH_PACK_CLASS_NAME).newInstance(); - - TestClassLoader quebecPackClassLoader = new TestClassLoader(getClass().getClassLoader().getResource("I18n/QuebecPlugin.jar")); - LanguagePack quebecPack = (LanguagePack) quebecPackClassLoader.loadClass(QUEBEC_PACK_CLASS_NAME).newInstance(); - - manager = new I18nManager(mock(PluginRepository.class), new LanguagePack[]{frenchPack, quebecPack, englishPack}); - manager.doStart(plugins); - } - - @Test - public void shouldTranslateWithoutRegionalVariant() { - List<String> sentence = Arrays.asList("it", "is", "cold"); - String result = ""; - for (String token : sentence) { - result += manager.message(Locale.FRENCH, token, token) + " "; - } - assertEquals("Il fait froid ", result); - } - - @Test - public void shouldTranslateWithRegionalVariant() { - // it & is are taken from the french language pack - // and cold is taken from the quebec language pack - List<String> sentence = Arrays.asList("it", "is", "cold"); - String result = ""; - for (String token : sentence) { - result += manager.message(Locale.CANADA_FRENCH, token, token) + " "; - } - assertEquals("Il fait frette ", result); - } - - @Test - public void shouldTranslateReturnsDefaultBundleValue() { - String result = manager.message(Locale.FRENCH, "only.english", "Default"); - assertEquals("Ketchup", result); - } - - @Test - public void shouldTranslateUnknownValue() { - String result = manager.message(Locale.FRENCH, "unknown", "Default value for Unknown"); - assertEquals("Default value for Unknown", result); - } - - @Test - public void shouldReturnKeyIfTranslationMissingAndNotDefaultProvided() throws Exception { - String result = manager.message(Locale.ENGLISH, "unknown.key", null); - assertEquals("unknown.key", result); - } - - @Test - public void testIsKeyForRuleDescription() throws Exception { - assertTrue(manager.isKeyForRuleDescription("rule.squid.ArchitecturalConstraint.description")); - assertFalse(manager.isKeyForRuleDescription("rule.squid.ArchitecturalConstraint.name")); - assertFalse(manager.isKeyForRuleDescription("another.key")); - } - - @Test - public void testDefineLocaleToUse() throws Exception { - assertThat(manager.defineLocaleToUse(Locale.CANADA_FRENCH), is(Locale.CANADA_FRENCH)); - // Locale not supported => get the English one - assertThat(manager.defineLocaleToUse(Locale.JAPAN), is(Locale.ENGLISH)); - } - - @Test - public void shouldExtractRuleKey() throws Exception { - assertThat(manager.extractRuleKeyFromDescriptionProperty("rule.squid.ArchitecturalConstraint.description"), is("ArchitecturalConstraint")); - assertThat(manager.extractRuleKeyFromDescriptionProperty("rule.checkstyle.com.puppycrawl.checkstyle.IllegalRegexp.description"), is("com.puppycrawl.checkstyle.IllegalRegexp")); - } - - @Test - public void testComputeHtmlFilePath() throws Exception { - assertThat(manager.computeHtmlFilePath("org/sonar/i18n/test", "rule.test.fakerule.description", Locale.FRENCH), - is("org/sonar/i18n/test_fr/fakerule.html")); - assertThat(manager.computeHtmlFilePath("org/sonar/i18n/test", "rule.test.fakerule.description", Locale.ENGLISH), - is("org/sonar/i18n/test/fakerule.html")); - } - - @Test - public void shouldReturnRuleDescriptionFromHTMLFile() throws Exception { - String result = manager.message(Locale.FRENCH, "rule.test.fakerule.description", "foo"); - assertThat(result, is("<h1>Règle bidon</h1>\nC'est la description de la règle bidon.")); - // Locale not supported => get the English translation - result = manager.message(Locale.JAPAN, "rule.test.fakerule.description", "foo"); - assertThat(result, is("<h1>Fake Rule</h1>\nThis is the description of the fake rule.")); - } - - @Test - public void shouldReturnEnglishRuleDescriptionFromMissingHTMLFileInFrench() throws Exception { - String result = manager.message(Locale.FRENCH, "rule.test.anotherfakerule.description", "foo"); - assertThat(result, is("<h1>Another Fake Rule</h1>\nThis is the description of the fake rule.")); - - } - - public static class TestClassLoader extends URLClassLoader { - - public TestClassLoader(URL url) { - super(new URL[]{url, classSource}, Thread.currentThread().getContextClassLoader()); - } - - protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - Class c = findLoadedClass(name); - if (c == null) { - if (name.equals(ENGLISH_PACK_CLASS_NAME) || name.equals(QUEBEC_PACK_CLASS_NAME) || name.equals(FRENCH_PACK_CLASS_NAME)) { - c = findClass(name); - } else { - return super.loadClass(name, resolve); - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - -} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/QuebecLanguagePack.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/QuebecLanguagePack.java deleted file mode 100644 index c8ed96a4edb..00000000000 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/i18n/QuebecLanguagePack.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.core.i18n; - -import org.sonar.api.i18n.LanguagePack; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public class QuebecLanguagePack extends LanguagePack { - - @Override - public List<String> getPluginKeys() { - return Arrays.asList("test"); - } - - @Override - public List<Locale> getLocales() { - return Arrays.asList(Locale.CANADA_FRENCH); - } -}
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin.jar b/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin.jar Binary files differdeleted file mode 100755 index f593384a2ab..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin.jar +++ /dev/null diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/META-INF/MANIFEST.MF b/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/META-INF/MANIFEST.MF deleted file mode 100644 index a45029d3531..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0
-Created-By: 1.6.0_17 (Apple Inc.)
-
diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test.properties b/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test.properties deleted file mode 100644 index 207a12a76a8..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test.properties +++ /dev/null @@ -1,6 +0,0 @@ -it=It -is=is -cold=cold -only.english=Ketchup -rule.test.fakerule.name=Fake Rule -rule.test.anotherfakerule.name=Another Fake Rule
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/anotherfakerule.html b/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/anotherfakerule.html deleted file mode 100644 index 60539d00c95..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/anotherfakerule.html +++ /dev/null @@ -1,2 +0,0 @@ -<h1>Another Fake Rule</h1> -This is the description of the fake rule.
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/fakerule.html b/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/fakerule.html deleted file mode 100644 index 7e7d84dea8b..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/EnglishPlugin/org/sonar/i18n/test/fakerule.html +++ /dev/null @@ -1,2 +0,0 @@ -<h1>Fake Rule</h1> -This is the description of the fake rule.
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin.jar b/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin.jar Binary files differdeleted file mode 100755 index 383c767d26d..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin.jar +++ /dev/null diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/META-INF/MANIFEST.MF b/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/META-INF/MANIFEST.MF deleted file mode 100644 index a45029d3531..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0
-Created-By: 1.6.0_17 (Apple Inc.)
-
diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr.properties b/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr.properties deleted file mode 100644 index a1d8766e962..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr.properties +++ /dev/null @@ -1,4 +0,0 @@ -it=Il -is=fait -cold=froid -rule.test.fakerule.name=R\u00e8gle bidon diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr/fakerule.html b/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr/fakerule.html deleted file mode 100644 index 94c0d0869f8..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/FrenchPlugin/org/sonar/i18n/test_fr/fakerule.html +++ /dev/null @@ -1,2 +0,0 @@ -<h1>Règle bidon</h1> -C'est la description de la règle bidon.
\ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin.jar b/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin.jar Binary files differdeleted file mode 100644 index 101df34ba6d..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin.jar +++ /dev/null diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/META-INF/MANIFEST.MF b/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/META-INF/MANIFEST.MF deleted file mode 100644 index a45029d3531..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0
-Created-By: 1.6.0_17 (Apple Inc.)
-
diff --git a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/org/sonar/i18n/test_fr_CA.properties b/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/org/sonar/i18n/test_fr_CA.properties deleted file mode 100644 index 38b5b84ccdf..00000000000 --- a/plugins/sonar-core-plugin/src/test/resources/I18n/QuebecPlugin/org/sonar/i18n/test_fr_CA.properties +++ /dev/null @@ -1,2 +0,0 @@ -cold=frette - diff --git a/plugins/sonar-i18n-en-plugin/pom.xml b/plugins/sonar-i18n-en-plugin/pom.xml index 9e523b507f0..c25857f1b1d 100644 --- a/plugins/sonar-i18n-en-plugin/pom.xml +++ b/plugins/sonar-i18n-en-plugin/pom.xml @@ -51,7 +51,7 @@ <pluginName>I18n English Pack</pluginName> <pluginClass>org.sonar.plugins.i18n.en.EnglishPackPlugin</pluginClass> <pluginDescription> - <![CDATA[Language pack for English.]]></pluginDescription> + <![CDATA[Language pack for English]]></pluginDescription> </configuration> </plugin> </plugins> diff --git a/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPack.java b/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPack.java deleted file mode 100644 index 89dfc435365..00000000000 --- a/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPack.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.i18n.en; - -import org.sonar.api.i18n.LanguagePack; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public class EnglishPack extends LanguagePack { - - public List<String> getPluginKeys() { - return Arrays.asList("core", "design", "squidjava", "checkstyle", "findbugs", "pmd"); - } - - public List<Locale> getLocales() { - return Arrays.asList(Locale.ENGLISH); - } -} diff --git a/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPackPlugin.java b/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPackPlugin.java index 6bbe6f605e9..1cd2c84fb8f 100644 --- a/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPackPlugin.java +++ b/plugins/sonar-i18n-en-plugin/src/main/java/org/sonar/plugins/i18n/en/EnglishPackPlugin.java @@ -21,12 +21,12 @@ package org.sonar.plugins.i18n.en; import org.sonar.api.SonarPlugin; -import java.util.Arrays; +import java.util.Collections; import java.util.List; -public class EnglishPackPlugin extends SonarPlugin { +public final class EnglishPackPlugin extends SonarPlugin { public List getExtensions() { - return Arrays.asList(EnglishPack.class); + return Collections.emptyList(); } } diff --git a/sonar-core/src/main/java/org/sonar/core/i18n/I18nManager.java b/sonar-core/src/main/java/org/sonar/core/i18n/I18nManager.java index a99ba3397bf..45f31b36853 100644 --- a/sonar-core/src/main/java/org/sonar/core/i18n/I18nManager.java +++ b/sonar-core/src/main/java/org/sonar/core/i18n/I18nManager.java @@ -20,50 +20,145 @@ package org.sonar.core.i18n; import com.google.common.collect.Maps; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.api.ServerExtension; import org.sonar.api.i18n.I18n; import org.sonar.api.platform.PluginMetadata; import org.sonar.api.platform.PluginRepository; +import org.sonar.api.utils.SonarException; +import java.io.IOException; +import java.io.InputStream; import java.text.MessageFormat; -import java.util.Locale; -import java.util.Map; -import java.util.ResourceBundle; +import java.util.*; public class I18nManager implements I18n, ServerExtension { + private static Logger LOG = LoggerFactory.getLogger(I18nManager.class); + + public static final String ENGLISH_PACK_PLUGIN_KEY = "i18nen"; + public static final String BUNDLE_PACKAGE = "org.sonar.i18n."; private PluginRepository pluginRepository; - private Map<String, ClassLoader> bundleToClassloader; + private Map<String, ClassLoader> bundleToClassloaders; + private Map<String, String> propertyToBundles; public I18nManager(PluginRepository pluginRepository) { this.pluginRepository = pluginRepository; } - I18nManager(Map<String, ClassLoader> bundleToClassloader) { - this.bundleToClassloader = bundleToClassloader; + I18nManager(Map<String, ClassLoader> bundleToClassloaders) { + this.bundleToClassloaders = bundleToClassloaders; } public void start() { - ClassLoader coreClassLoader = pluginRepository.getPlugin("i18nen").getClass().getClassLoader(); + initClassloaders(); + initProperties(); + } + + private void initClassloaders() { + if (bundleToClassloaders == null) { + ClassLoader coreClassLoader = pluginRepository.getPlugin(ENGLISH_PACK_PLUGIN_KEY).getClass().getClassLoader(); + bundleToClassloaders = Maps.newHashMap(); + for (PluginMetadata metadata : pluginRepository.getMetadata()) { + if (!metadata.isCore() && !ENGLISH_PACK_PLUGIN_KEY.equals(metadata.getBasePlugin())) { + // plugin but not a language pack + // => plugins embedd only their own bundles with all locales + ClassLoader classLoader = pluginRepository.getPlugin(metadata.getKey()).getClass().getClassLoader(); + bundleToClassloaders.put(BUNDLE_PACKAGE + metadata.getKey(), classLoader); - bundleToClassloader = Maps.newHashMap(); - for (PluginMetadata metadata : pluginRepository.getMetadata()) { - if (!metadata.isCore() && !"i18nen".equals(metadata.getBasePlugin())) { - ClassLoader classLoader = pluginRepository.getPlugin(metadata.getKey()).getClass().getClassLoader(); - bundleToClassloader.put(metadata.getKey(), classLoader); + } else if (metadata.isCore()) { + // bundles of core plugins are defined into language packs. All language packs are supposed + // to share the same classloader (english pack classloader) + bundleToClassloaders.put(BUNDLE_PACKAGE + metadata.getKey(), coreClassLoader); + } + } + } + bundleToClassloaders = Collections.unmodifiableMap(bundleToClassloaders); + } - } else if (metadata.isCore()) { - bundleToClassloader.put(metadata.getKey(), coreClassLoader); + private void initProperties() { + propertyToBundles = Maps.newHashMap(); + for (Map.Entry<String, ClassLoader> entry : bundleToClassloaders.entrySet()) { + try { + String bundleKey = entry.getKey(); + ResourceBundle bundle = ResourceBundle.getBundle(bundleKey, Locale.ENGLISH, entry.getValue()); + Enumeration<String> keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + propertyToBundles.put(key, bundleKey); + } + } catch (MissingResourceException e) { + // ignore } } + propertyToBundles = Collections.unmodifiableMap(propertyToBundles); + LOG.debug(String.format("Loaded %d properties from English bundles", propertyToBundles.size())); } public String message(Locale locale, String key, String defaultValue, Object... parameters) { - String bundle = keyToBundle(key); - ResourceBundle resourceBundle = ResourceBundle.getBundle(bundle, locale, bundleToClassloader.get(bundle)); - String value = resourceBundle.getString(key); - if (value==null) { + String bundleKey = propertyToBundles.get(key); + ResourceBundle resourceBundle = getBundle(bundleKey, locale); + return message(resourceBundle, key, defaultValue, parameters); + } + + /** + * Results are not kept in cache. + */ + String messageFromFile(Locale locale, String filename, String relatedProperty) { + ClassLoader classloader = getClassLoaderForProperty(relatedProperty); + String result = null; + if (classloader != null) { + String bundleBase = propertyToBundles.get(relatedProperty); + String filePath = bundleBase.replace('.', '/'); + if (locale != Locale.ENGLISH) { + filePath += "_" + locale.toString(); + } + filePath += "/" + filename; + InputStream input = classloader.getResourceAsStream(filePath); + if (input != null) { + 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; + } + + ResourceBundle getBundle(String bundleKey, Locale locale) { + try { + ClassLoader classloader = bundleToClassloaders.get(bundleKey); + if (classloader != null) { + return ResourceBundle.getBundle(bundleKey, locale, classloader); + } + } catch (MissingResourceException e) { + // ignore + } + return null; + } + + + ClassLoader getClassLoaderForProperty(String propertyKey) { + String bundleKey = propertyToBundles.get(propertyKey); + return (bundleKey != null ? bundleToClassloaders.get(bundleKey) : null); + } + + String message(ResourceBundle resourceBundle, String key, String defaultValue, Object... parameters) { + String value = null; + if (resourceBundle != null) { + try { + value = resourceBundle.getString(key); + } catch (MissingResourceException e) { + // ignore + } + } + if (value == null) { value = defaultValue; } if (value != null && parameters.length > 0) { @@ -72,11 +167,11 @@ public class I18nManager implements I18n, ServerExtension { return value; } - String keyToBundle(String key) { - String pluginKey = StringUtils.substringBefore(key, "."); - if (bundleToClassloader.containsKey(pluginKey)) { - return pluginKey; + String extractBundleFromKey(String key) { + String bundleKey = BUNDLE_PACKAGE + StringUtils.substringBefore(key, "."); + if (bundleToClassloaders.containsKey(bundleKey)) { + return bundleKey; } - return "core"; + return BUNDLE_PACKAGE + "core"; } } diff --git a/sonar-core/src/main/java/org/sonar/core/i18n/RuleI18nManager.java b/sonar-core/src/main/java/org/sonar/core/i18n/RuleI18nManager.java new file mode 100644 index 00000000000..764871806cc --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/i18n/RuleI18nManager.java @@ -0,0 +1,111 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.i18n; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.ServerComponent; + +import java.util.Locale; + +public class RuleI18nManager implements ServerComponent { + + private I18nManager i18nManager; + + public RuleI18nManager(I18nManager i18nManager) { + this.i18nManager = i18nManager; + } + + public String getName(String repositoryKey, String ruleKey, Locale locale) { + return message(repositoryKey, ruleKey, locale, ".name", ruleKey); + } + + public String getDescription(String repositoryKey, String ruleKey, Locale locale) { + String relatedProperty = "rule." + repositoryKey + "." + ruleKey + ".name"; + + // TODO add cache + String description = i18nManager.messageFromFile(locale, ruleKey + ".html", relatedProperty); + if (description == null && !Locale.ENGLISH.equals(locale)) { + description = i18nManager.messageFromFile(Locale.ENGLISH, ruleKey + ".html", relatedProperty); + } + return StringUtils.defaultString(description, ""); + } + + public String getParamDescription(String repositoryKey, String ruleKey, String paramKey, Locale locale) { + return message(repositoryKey, ruleKey, locale, ".param." + paramKey, ""); + } + + private String message(String repositoryKey, String ruleKey, Locale locale, String suffix, String defaultValue) { + String propertyKey = new StringBuilder().append("rule.").append(repositoryKey).append(".").append(ruleKey).append(suffix).toString(); + return i18nManager.message(locale, propertyKey, defaultValue); + } + +// static class RuleKey { +// private String repositoryKey; +// private String key; +// +// RuleKey(String repositoryKey, String key) { +// this.repositoryKey = repositoryKey; +// this.key = key; +// } +// +// public String getRepositoryKey() { +// return repositoryKey; +// } +// +// public String getKey() { +// return key; +// } +// +// @Override +// public boolean equals(Object o) { +// if (this == o) return true; +// if (o == null || getClass() != o.getClass()) return false; +// +// RuleKey ruleKey = (RuleKey) o; +// +// if (!key.equals(ruleKey.key)) return false; +// if (!repositoryKey.equals(ruleKey.repositoryKey)) return false; +// +// return true; +// } +// +// @Override +// public int hashCode() { +// int result = repositoryKey.hashCode(); +// result = 31 * result + key.hashCode(); +// return result; +// } +// +// @Override +// public String toString() { +// return new StringBuilder().append(repositoryKey).append(":").append(key).toString(); +// } +// } +// +// static class RuleMessages { +// private String name; +// private String description; +// +// RuleMessages(String name, String description) { +// this.name = name; +// this.description = description; +// } +// } +} diff --git a/sonar-core/src/test/java/org/sonar/core/i18n/I18nManagerTest.java b/sonar-core/src/test/java/org/sonar/core/i18n/I18nManagerTest.java index af57b768738..b2ef460c993 100644 --- a/sonar-core/src/test/java/org/sonar/core/i18n/I18nManagerTest.java +++ b/sonar-core/src/test/java/org/sonar/core/i18n/I18nManagerTest.java @@ -20,25 +20,145 @@ package org.sonar.core.i18n; import com.google.common.collect.Maps; +import org.hamcrest.CoreMatchers; import org.hamcrest.core.Is; +import org.junit.Before; import org.junit.Test; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Locale; import java.util.Map; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; +import static org.sonar.core.i18n.I18nManager.BUNDLE_PACKAGE; public class I18nManagerTest { + private I18nManager manager; + private ClassLoader coreClassLoader; + private ClassLoader sqaleClassLoader; + + @Before + public void init() { + coreClassLoader = newCoreClassLoader(); + sqaleClassLoader = newSqaleClassLoader(); + Map<String, ClassLoader> bundleToClassLoaders = Maps.newHashMap(); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "core", coreClassLoader); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "checkstyle", coreClassLoader); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "sqale", sqaleClassLoader); + manager = new I18nManager(bundleToClassLoaders); + manager.start(); + } + @Test - public void shouldExtractBundleKey() { - Map<String,ClassLoader> bundleToClassLoaders = Maps.newHashMap(); - bundleToClassLoaders.put("core", getClass().getClassLoader()); - bundleToClassLoaders.put("checkstyle", getClass().getClassLoader()); - bundleToClassLoaders.put("sqale", getClass().getClassLoader()); + public void shouldExtractPluginFromKey() { + Map<String, ClassLoader> bundleToClassLoaders = Maps.newHashMap(); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "core", getClass().getClassLoader()); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "checkstyle", getClass().getClassLoader()); + bundleToClassLoaders.put(BUNDLE_PACKAGE + "sqale", getClass().getClassLoader()); I18nManager i18n = new I18nManager(bundleToClassLoaders); + i18n.start(); + + assertThat(i18n.extractBundleFromKey("by"), Is.is(BUNDLE_PACKAGE + "core")); + assertThat(i18n.extractBundleFromKey("violations_drilldown.page"), Is.is(BUNDLE_PACKAGE + "core")); + assertThat(i18n.extractBundleFromKey("checkstyle.rule1.name"), Is.is(BUNDLE_PACKAGE + "checkstyle")); + assertThat(i18n.extractBundleFromKey("sqale.console.page"), Is.is(BUNDLE_PACKAGE + "sqale")); + } + + @Test + public void shouldFindKeysInEnglishLanguagePack() { + assertThat(manager.message(Locale.ENGLISH, "checkstyle.rule1.name", null), Is.is("Rule one")); + assertThat(manager.message(Locale.ENGLISH, "by", null), Is.is("By")); + assertThat(manager.message(Locale.ENGLISH, "sqale.page", null), Is.is("Sqale page title")); + + assertThat(manager.message(Locale.FRENCH, "checkstyle.rule1.name", null), Is.is("Rule un")); + assertThat(manager.message(Locale.FRENCH, "by", null), Is.is("Par")); + assertThat(manager.message(Locale.FRENCH, "sqale.page", null), Is.is("Titre de la page Sqale")); + } + + @Test + public void shouldUseDefaultLocale() { + assertThat(manager.message(Locale.CHINA, "checkstyle.rule1.name", null), Is.is("Rule one")); + assertThat(manager.message(Locale.CHINA, "by", null), Is.is("By")); + assertThat(manager.message(Locale.CHINA, "sqale.page", null), Is.is("Sqale page title")); + } + + @Test + public void shouldUseLanguagePack() { + assertThat(manager.message(Locale.FRENCH, "checkstyle.rule1.name", null), Is.is("Rule un")); + assertThat(manager.message(Locale.FRENCH, "by", null), Is.is("Par")); + assertThat(manager.message(Locale.FRENCH, "sqale.page", null), Is.is("Titre de la page Sqale")); + } + + @Test + public void shouldReturnDefaultValueIfMissingKey() { + assertThat(manager.message(Locale.ENGLISH, "foo.unknown", "default"), Is.is("default")); + assertThat(manager.message(Locale.FRENCH, "foo.unknown", "default"), Is.is("default")); + } + + @Test + public void shouldAcceptEmptyLabels() { + assertThat(manager.message(Locale.ENGLISH, "empty", "default"), Is.is("")); + assertThat(manager.message(Locale.FRENCH, "empty", "default"), Is.is("")); + } + + @Test + public void shouldFormatMessageWithParameters() { + assertThat(manager.message(Locale.ENGLISH, "with.parameters", null, "one", "two"), Is.is("First is one and second is two")); + } + + @Test + public void shouldUseDefaultLocaleIfMissingValueInLocalizedBundle() { + assertThat(manager.message(Locale.FRENCH, "only.in.english", null), Is.is("Missing in French bundle")); + assertThat(manager.message(Locale.CHINA, "only.in.english", null), Is.is("Missing in French bundle")); + } + + @Test + public void shouldGetClassLoaderByProperty() { + assertThat(manager.getClassLoaderForProperty("foo.unknown"), nullValue()); + assertThat(manager.getClassLoaderForProperty("by"), Is.is(coreClassLoader)); + assertThat(manager.getClassLoaderForProperty("sqale.page"), Is.is(sqaleClassLoader)); + } + + @Test + public void shouldFindEnglishFile() { + String html = manager.messageFromFile(Locale.ENGLISH, "ArchitectureRule.html", "checkstyle.rule1.name" /* any property in the same bundle */); + assertThat(html, Is.is("This is the architecture rule")); + } + + @Test + public void shouldNotFindFile() { + String html = manager.messageFromFile(Locale.ENGLISH, "UnknownRule.html", "checkstyle.rule1.name" /* any property in the same bundle */); + assertThat(html, nullValue()); + } + + @Test + public void shouldFindFrenchFile() { + String html = manager.messageFromFile(Locale.FRENCH, "ArchitectureRule.html", "checkstyle.rule1.name" /* any property in the same bundle */); + assertThat(html, Is.is("Règle d'architecture")); + } + + @Test + public void shouldNotFindMissingLocale() { + String html = manager.messageFromFile(Locale.CHINA, "ArchitectureRule.html", "checkstyle.rule1.name" /* any property in the same bundle */); + assertThat(html, nullValue()); + } + + + private URLClassLoader newSqaleClassLoader() { + return newClassLoader("/org/sonar/core/i18n/sqalePlugin/"); + } + + private URLClassLoader newCoreClassLoader() { + return newClassLoader("/org/sonar/core/i18n/englishPack/", "/org/sonar/core/i18n/frenchPack/"); + } - assertThat(i18n.keyToBundle("by"), Is.is("core")); - assertThat(i18n.keyToBundle("violations_drilldown.page"), Is.is("core")); - assertThat(i18n.keyToBundle("checkstyle.rule1.name"), Is.is("checkstyle")); - assertThat(i18n.keyToBundle("sqale.console.page"), Is.is("sqale")); + private URLClassLoader newClassLoader(String... resourcePaths) { + URL[] urls = new URL[resourcePaths.length]; + for (int index = 0; index < resourcePaths.length; index++) { + urls[index] = getClass().getResource(resourcePaths[index]); + } + return new URLClassLoader(urls); } } diff --git a/sonar-core/src/test/java/org/sonar/core/i18n/RuleI18nManagerTest.java b/sonar-core/src/test/java/org/sonar/core/i18n/RuleI18nManagerTest.java new file mode 100644 index 00000000000..17608330075 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/i18n/RuleI18nManagerTest.java @@ -0,0 +1,90 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.i18n; + +import org.hamcrest.core.Is; +import org.junit.Test; + +import java.util.Locale; + +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.isNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class RuleI18nManagerTest { + @Test + public void shouldGetName() { + I18nManager i18n = mock(I18nManager.class); + RuleI18nManager ruleI18n = new RuleI18nManager(i18n); + + ruleI18n.getName("checkstyle", "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", Locale.ENGLISH); + + String propertyKey = "rule.checkstyle.com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.name"; + verify(i18n).message(Locale.ENGLISH, propertyKey, "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck" /* default value is the key */); + verifyNoMoreInteractions(i18n); + } + + @Test + public void shouldGetParamDescription() { + I18nManager i18n = mock(I18nManager.class); + RuleI18nManager ruleI18n = new RuleI18nManager(i18n); + + ruleI18n.getParamDescription("checkstyle", "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", "pattern", Locale.ENGLISH); + + String propertyKey = "rule.checkstyle.com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.param.pattern"; + verify(i18n).message(Locale.ENGLISH, propertyKey, "" /* default value must be blank */); + verifyNoMoreInteractions(i18n); + } + + @Test + public void shouldGetDescriptionFromFile() { + I18nManager i18n = mock(I18nManager.class); + RuleI18nManager ruleI18n = new RuleI18nManager(i18n); + + ruleI18n.getDescription("checkstyle", "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", Locale.ENGLISH); + + String propertyKeyForName = "rule.checkstyle.com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.name"; + verify(i18n).messageFromFile(Locale.ENGLISH, "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.html", propertyKeyForName); + verifyNoMoreInteractions(i18n); + } + + @Test + public void shoudlReturnBlankIfMissingDescription() { + I18nManager i18n = mock(I18nManager.class); + RuleI18nManager ruleI18n = new RuleI18nManager(i18n); + + assertThat(ruleI18n.getDescription("checkstyle", "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", Locale.ENGLISH), Is.is("")); + } + + @Test + public void shouldUseEnglishIfMissingLocale() { + I18nManager i18n = mock(I18nManager.class); + RuleI18nManager ruleI18n = new RuleI18nManager(i18n); + + ruleI18n.getDescription("checkstyle", "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", Locale.FRENCH); + + String propertyKeyForName = "rule.checkstyle.com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.name"; + verify(i18n).messageFromFile(Locale.FRENCH, "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.html", propertyKeyForName); + verify(i18n).messageFromFile(Locale.ENGLISH, "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.html", propertyKeyForName); + verifyNoMoreInteractions(i18n); + } +} diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle.properties new file mode 100644 index 00000000000..10fa9295c44 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle.properties @@ -0,0 +1 @@ +checkstyle.rule1.name=Rule one diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle/ArchitectureRule.html b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle/ArchitectureRule.html new file mode 100644 index 00000000000..a7cad9049d7 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/checkstyle/ArchitectureRule.html @@ -0,0 +1 @@ +This is the architecture rule
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/core.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/core.properties new file mode 100644 index 00000000000..de205d651cb --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/englishPack/org/sonar/i18n/core.properties @@ -0,0 +1,4 @@ +by=By +empty= +with.parameters=First is {0} and second is {1} +only.in.english=Missing in French bundle
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr.properties new file mode 100644 index 00000000000..b2fc8f9651f --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr.properties @@ -0,0 +1 @@ +checkstyle.rule1.name=Rule un
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr/ArchitectureRule.html b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr/ArchitectureRule.html new file mode 100644 index 00000000000..9b12ae071ce --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/checkstyle_fr/ArchitectureRule.html @@ -0,0 +1 @@ +Règle d'architecture
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/core_fr.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/core_fr.properties new file mode 100644 index 00000000000..e9ced4039ae --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/frenchPack/org/sonar/i18n/core_fr.properties @@ -0,0 +1,2 @@ +by=Par +empty=
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale.properties new file mode 100644 index 00000000000..a8ea9c0553e --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale.properties @@ -0,0 +1 @@ +sqale.page=Sqale page title
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale_fr.properties b/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale_fr.properties new file mode 100644 index 00000000000..471d015a11a --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/i18n/sqalePlugin/org/sonar/i18n/sqale_fr.properties @@ -0,0 +1 @@ +sqale.page=Titre de la page Sqale
\ No newline at end of file diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/i18n/LanguagePack.java b/sonar-plugin-api/src/main/java/org/sonar/api/i18n/LanguagePack.java deleted file mode 100644 index 5b167fd6cad..00000000000 --- a/sonar-plugin-api/src/main/java/org/sonar/api/i18n/LanguagePack.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.api.i18n; - -import org.sonar.api.BatchExtension; -import org.sonar.api.ServerExtension; - -import java.util.Collection; -import java.util.Locale; - -/** - * - * EXPERIMENTAL - this feature will be fully implemented in version 2.10 - * - * - * @since 2.9 - */ -public abstract class LanguagePack implements ServerExtension, BatchExtension { - - @Override - public String toString() { - return new StringBuilder("Language Pack (") - .append(getPluginKeys().toString()) - .append(getLocales().toString()) - .append(')').toString(); - } - - /** - * @return the pluginKeys - */ - public abstract Collection<String> getPluginKeys(); - - /** - * @return the locales - */ - public abstract Collection<Locale> getLocales(); -} diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 7b5ef8fac7f..6835c7f69dd 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -41,6 +41,8 @@ import org.sonar.core.components.DefaultMetricFinder; import org.sonar.core.components.DefaultModelFinder; import org.sonar.core.components.DefaultRuleFinder; import org.sonar.core.components.DefaultUserFinder; +import org.sonar.core.i18n.I18nManager; +import org.sonar.core.i18n.RuleI18nManager; import org.sonar.core.notifications.DefaultNotificationManager; import org.sonar.jpa.dao.DaoFacade; import org.sonar.jpa.dao.MeasuresDao; @@ -187,6 +189,8 @@ public final class Platform { servicesContainer.as(Characteristics.CACHE).addComponent(RulesConsole.class); servicesContainer.as(Characteristics.CACHE).addComponent(JRubyI18n.class); servicesContainer.as(Characteristics.CACHE).addComponent(DefaultUserFinder.class); + servicesContainer.as(Characteristics.CACHE).addComponent(I18nManager.class); + servicesContainer.as(Characteristics.CACHE).addComponent(RuleI18nManager.class); // Notifications servicesContainer.as(Characteristics.CACHE).addComponent(NotificationService.class); diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 473bc679f65..e0ea1b1004f 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -302,13 +302,34 @@ public final class JRubyFacade { return component; } - public String getI18nMessage(String rubyLocale, String key, String defaultValue, Object... parameters) { + public String getMessage(String rubyLocale, String key, String defaultValue, Object... parameters) { if (i18n == null) { i18n = getContainer().getComponent(JRubyI18n.class); } return i18n.message(rubyLocale, key, defaultValue, parameters); } + public String getRuleName(String rubyLocale, String repositoryKey, String key) { + if (i18n == null) { + i18n = getContainer().getComponent(JRubyI18n.class); + } + return i18n.getRuleName(rubyLocale, repositoryKey, key); + } + + public String getRuleDescription(String rubyLocale, String repositoryKey, String key) { + if (i18n == null) { + i18n = getContainer().getComponent(JRubyI18n.class); + } + return i18n.getRuleDescription(rubyLocale, repositoryKey, key); + } + + public String getRuleParamDescription(String rubyLocale, String repositoryKey, String key, String paramKey) { + if (i18n == null) { + i18n = getContainer().getComponent(JRubyI18n.class); + } + return i18n.getRuleParamDescription(rubyLocale, repositoryKey, key, paramKey); + } + public ReviewsNotificationManager getReviewsNotificationManager() { return getContainer().getComponent(ReviewsNotificationManager.class); } diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyI18n.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyI18n.java index 72a1d0e39a0..e7d9804e3f3 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyI18n.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyI18n.java @@ -23,6 +23,7 @@ import com.google.common.collect.Maps; import org.apache.commons.lang.StringUtils; import org.sonar.api.ServerComponent; import org.sonar.api.i18n.I18n; +import org.sonar.core.i18n.RuleI18nManager; import java.util.Locale; import java.util.Map; @@ -33,10 +34,12 @@ import java.util.Map; public final class JRubyI18n implements ServerComponent { private I18n i18n; - private Map<String,Locale> localesByRubyKey = Maps.newHashMap(); + private Map<String, Locale> localesByRubyKey = Maps.newHashMap(); + private RuleI18nManager ruleI18nManager; - public JRubyI18n(I18n i18n) { + public JRubyI18n(I18n i18n, RuleI18nManager ruleI18nManager) { this.i18n = i18n; + this.ruleI18nManager = ruleI18nManager; } Locale getLocale(String rubyKey) { @@ -55,7 +58,7 @@ public final class JRubyI18n implements ServerComponent { static Locale toLocale(String rubyKey) { Locale locale; String[] fields = StringUtils.split(rubyKey, "-"); - if (fields.length==1) { + if (fields.length == 1) { locale = new Locale(fields[0]); } else { locale = new Locale(fields[0], fields[1]); @@ -66,4 +69,16 @@ public final class JRubyI18n implements ServerComponent { public String message(String rubyLocale, String key, String defaultValue, Object... parameters) { return i18n.message(getLocale(rubyLocale), key, defaultValue, parameters); } + + public String getRuleName(String rubyLocale, String repositoryKey, String key) { + return ruleI18nManager.getName(repositoryKey, key, toLocale(rubyLocale)); + } + + public String getRuleDescription(String rubyLocale, String repositoryKey, String key) { + return ruleI18nManager.getDescription(repositoryKey, key, toLocale(rubyLocale)); + } + + public String getRuleParamDescription(String rubyLocale, String repositoryKey, String ruleKey, String paramKey) { + return ruleI18nManager.getParamDescription(repositoryKey, ruleKey, paramKey, toLocale(rubyLocale)); + } } diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb index 0c06549b0fd..9e8ff5e686b 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb @@ -97,12 +97,7 @@ class ApplicationController < ActionController::Base # i18n def message(key, options={}) - default = options[:default] - params = options[:params] - if params.nil? - params=[] - end - Java::OrgSonarServerUi::JRubyFacade.getInstance().getI18nMessage(I18n.locale, key, default, params.to_java) + Api::Utils.message(key, options) end end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/api/utils.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/api/utils.rb index 7ff974147af..8f5d33436a6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/api/utils.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/api/utils.rb @@ -67,6 +67,6 @@ class Api::Utils if params.nil? params=[] end - Java::OrgSonarServerUi::JRubyFacade.getInstance().getI18nMessage(I18n.locale, key, default, params.to_java) + Java::OrgSonarServerUi::JRubyFacade.getInstance().getMessage(I18n.locale, key, default, params.to_java) end end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/rule.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/rule.rb index 0fbb2e78823..6724dcbeac8 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/rule.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/rule.rb @@ -26,6 +26,10 @@ class Rule < ActiveRecord::Base has_many :active_rules belongs_to :parent, :class_name => 'Rule', :foreign_key => 'parent_id' + def repository_key + plugin_name + end + def parameters rules_parameters end @@ -57,41 +61,31 @@ class Rule < ActiveRecord::Base def <=>(rule) name<=>rule.name end - - def name(translate=true) - default_string = read_attribute(:name) - return default_string unless translate - - rule_plugin_name = read_attribute(:plugin_name) - rule_plugin_rule_key = read_attribute(:plugin_rule_key) - - return nil if (rule_plugin_name.nil? or rule_plugin_rule_key.nil?) - - i18n_key = 'rule.' + rule_plugin_name + '.' + rule_plugin_rule_key + '.name' - result = Api::Utils.message(i18n_key, :default => default_string) - result + + def name + @l10n_name ||= + begin + result = Java::OrgSonarServerUi::JRubyFacade.getInstance().getRuleName(I18n.locale, repository_key, plugin_rule_key) + result = read_attribute(:name) unless result + result + end end - + def name=(value) - write_attribute(:name, value) + write_attribute(:name, value) end - - def description(translate=true) - default_string = read_attribute(:description) - return default_string unless translate - - rule_plugin_name = read_attribute(:plugin_name) - rule_plugin_rule_key = read_attribute(:plugin_rule_key) - - return nil if (rule_plugin_name.nil? or rule_plugin_rule_key.nil?) - - i18n_key = 'rule.' + rule_plugin_name + '.' + rule_plugin_rule_key + '.description' - result = Api::Utils.message(i18n_key, :default => default_string) - result + + def description + @l10n_description ||= + begin + result = Java::OrgSonarServerUi::JRubyFacade.getInstance().getRuleDescription(I18n.locale, repository_key, plugin_rule_key) + result = read_attribute(:description) unless result + result + end end def description=(value) - write_attribute(:description, value) + write_attribute(:description, value) end def config_key @@ -142,7 +136,7 @@ class Rule < ActiveRecord::Base else json['priority'] = priority_text end - json['params'] = parameters.collect{|parameter| parameter.to_hash_json(active_rule)} unless parameters.empty? + json['params'] = parameters.collect { |parameter| parameter.to_hash_json(active_rule) } unless parameters.empty? json end @@ -152,7 +146,7 @@ class Rule < ActiveRecord::Base xml.key(key) xml.config_key(config_key) xml.plugin(plugin_name) - xml.description {xml.cdata!(description)} + xml.description { xml.cdata!(description) } active_rule = nil if profile active_rule = profile.active_by_rule_id(id) @@ -197,10 +191,10 @@ class Rule < ActiveRecord::Base if remove_blank(options[:plugins]) plugins = options[:plugins] unless options[:language].blank? - plugins = plugins & java_facade.getRuleRepositoriesByLanguage(options[:language]).collect{ |repo| repo.getKey() } + plugins = plugins & java_facade.getRuleRepositoriesByLanguage(options[:language]).collect { |repo| repo.getKey() } end elsif !options[:language].blank? - plugins = java_facade.getRuleRepositoriesByLanguage(options[:language]).collect{ |repo| repo.getKey() } + plugins = java_facade.getRuleRepositoriesByLanguage(options[:language]).collect { |repo| repo.getKey() } end if plugins @@ -220,8 +214,7 @@ class Rule < ActiveRecord::Base end includes=(options[:include_parameters] ? :rules_parameters : nil) - rules = Rule.find(:all, :order => 'rules.name', :include => includes, - :conditions => [conditions.join(" AND "), values]) + rules = Rule.find(:all, :include => includes, :conditions => [conditions.join(" AND "), values]).sort_by { |rule| rule.name } filter(rules, options) end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/rules_parameter.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/rules_parameter.rb index efe684b8c58..360b4f988af 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/rules_parameter.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/rules_parameter.rb @@ -1,22 +1,22 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar 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. - # - # Sonar 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 {library}; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2011 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar 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. +# +# Sonar 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 {library}; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# class RulesParameter < ActiveRecord::Base validates_presence_of :name, :param_type @@ -29,35 +29,30 @@ class RulesParameter < ActiveRecord::Base PARAM_TYPE_INTEGER_LIST = "i{}"; PARAM_TYPE_BOOLEAN = "b"; PARAM_TYPE_REGEXP = "r"; - + belongs_to :rule def is_set_type - return param_type.at(1) == "[" && param_type.ends_with?( "]" ) + return param_type.at(1) == "[" && param_type.ends_with?("]") end - + def get_allowed_tokens - return param_type[2,param_type.length-3].split( "," ) + return param_type[2, param_type.length-3].split(",") end - def description(translate=true) - default_string = read_attribute(:description) - return default_string unless translate - - rule_plugin_name = rule.plugin_name - rule_plugin_rule_key = rule.plugin_rule_key - - return nil if (rule_plugin_name.nil? or rule_plugin_rule_key.nil?) - - i18n_key = 'rule.' + rule_plugin_name + '.' + rule_plugin_rule_key + '.param.' + read_attribute(:name) - result = Api::Utils.message(i18n_key, :default => default_string) - result + def description + @l10n_description ||= + begin + result = Java::OrgSonarServerUi::JRubyFacade.getInstance().getRuleParamDescription(I18n.locale, rule.repository_key, rule.plugin_rule_key, name()) + result = read_attribute(:description) unless result + result + end end - + def description=(value) - write_attribute(:description, value) + write_attribute(:description, value) end - + def readable_param_type return "String" if param_type == PARAM_TYPE_STRING return "Set of string (, as delimiter)" if param_type == PARAM_TYPE_STRING_LIST @@ -67,55 +62,55 @@ class RulesParameter < ActiveRecord::Base return "Regular expression" if param_type == PARAM_TYPE_REGEXP return "Set of values (, as delimiter)" if is_set_type end - + def input_box_size return 15 if param_type == PARAM_TYPE_STRING or param_type == PARAM_TYPE_STRING_LIST or param_type == PARAM_TYPE_REGEXP return 8 if param_type == PARAM_TYPE_INTEGER or param_type == PARAM_TYPE_INTEGER_LIST return 4 if param_type == PARAM_TYPE_BOOLEAN if is_set_type - size = ( param_type.length / 2 ).to_i + size = (param_type.length / 2).to_i size = 64 if size > 64 return size end end def validate_value(attribute, errors, value) - return if attribute.nil? or attribute.length == 0 - if is_set_type - provided_tokens = attribute.split( "," ) - allowed_tokens = get_allowed_tokens - provided_tokens.each do |provided_token| - if !allowed_tokens.include?(provided_token) - errors.add( "#{value}", "Invalid value '" + provided_token + "'. Must be one of : " + allowed_tokens.join(", ") ) - end - end - elsif param_type == RulesParameter::PARAM_TYPE_INTEGER - begin - Kernel.Integer(attribute) - rescue - errors.add( "#{value}", "Invalid value '" + attribute + "'. Must be an integer." ) - end - elsif param_type == RulesParameter::PARAM_TYPE_INTEGER_LIST - provided_numbers = attribute.split( "," ) - provided_numbers.each do |provided_number| - begin - Kernel.Integer(provided_number) - rescue - errors.add("#{value}", "Invalid value '" + provided_number + "'. Must be an integer." ) - return - end - end - elsif param_type == RulesParameter::PARAM_TYPE_BOOLEAN - if attribute != "true" && attribute != "false" - errors.add( "#{value}", "Invalid value '" + attribute + "'. Must be one of : true,false" ) - end - elsif param_type == RulesParameter::PARAM_TYPE_REGEXP - begin - Regexp.new(attribute) - rescue - errors.add( "#{value}", "Invalid regular expression '" + attribute + "'.") - end - end + return if attribute.nil? or attribute.length == 0 + if is_set_type + provided_tokens = attribute.split(",") + allowed_tokens = get_allowed_tokens + provided_tokens.each do |provided_token| + if !allowed_tokens.include?(provided_token) + errors.add("#{value}", "Invalid value '" + provided_token + "'. Must be one of : " + allowed_tokens.join(", ")) + end + end + elsif param_type == RulesParameter::PARAM_TYPE_INTEGER + begin + Kernel.Integer(attribute) + rescue + errors.add("#{value}", "Invalid value '" + attribute + "'. Must be an integer.") + end + elsif param_type == RulesParameter::PARAM_TYPE_INTEGER_LIST + provided_numbers = attribute.split(",") + provided_numbers.each do |provided_number| + begin + Kernel.Integer(provided_number) + rescue + errors.add("#{value}", "Invalid value '" + provided_number + "'. Must be an integer.") + return + end + end + elsif param_type == RulesParameter::PARAM_TYPE_BOOLEAN + if attribute != "true" && attribute != "false" + errors.add("#{value}", "Invalid value '" + attribute + "'. Must be one of : true,false") + end + elsif param_type == RulesParameter::PARAM_TYPE_REGEXP + begin + Regexp.new(attribute) + rescue + errors.add("#{value}", "Invalid regular expression '" + attribute + "'.") + end + end end def to_hash_json(active_rule) @@ -130,7 +125,7 @@ class RulesParameter < ActiveRecord::Base def to_xml(active_rule, xml) xml.param do xml.name(name) - xml.description {xml.cdata!(description)} + xml.description { xml.cdata!(description) } if active_rule active_parameter = active_rule.active_param_by_param_id(id) xml.value(active_parameter.value) if active_parameter diff --git a/sonar-server/src/test/java/org/sonar/server/ui/JRubyI18nTest.java b/sonar-server/src/test/java/org/sonar/server/ui/JRubyI18nTest.java index fe150e2b754..c03934a3940 100644 --- a/sonar-server/src/test/java/org/sonar/server/ui/JRubyI18nTest.java +++ b/sonar-server/src/test/java/org/sonar/server/ui/JRubyI18nTest.java @@ -22,6 +22,7 @@ package org.sonar.server.ui; import org.hamcrest.core.Is; import org.junit.Test; import org.sonar.api.i18n.I18n; +import org.sonar.core.i18n.RuleI18nManager; import java.util.Locale; @@ -39,7 +40,7 @@ public class JRubyI18nTest { @Test public void shouldCacheLocales() { - JRubyI18n i18n = new JRubyI18n(mock(I18n.class)); + JRubyI18n i18n = new JRubyI18n(mock(I18n.class), mock(RuleI18nManager.class)); assertThat(i18n.getLocalesByRubyKey().size(), Is.is(0)); i18n.getLocale("fr"); |