Author: tchemit Date: 2010-03-07 17:02:33 +0100 (Sun, 07 Mar 2010) New Revision: 1702 Log: - reformat code (80 car per line max) - Evolution #343: Integrates class org.nuiton.i18n.I18n from nuiton-utils - Evolution #344: Introduces I18nInitializer api Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18n.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nBundleBridge.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nLanguage.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nStore.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleUtil.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ClassPathI18nInitializer.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/DefaultI18nInitializer.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/I18nInitializer.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/UserI18nInitializer.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/package.html trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/I18nStoreTest.java trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBundleScopeTest.java trunk/nuiton-i18n-api/src/test/resources/META-INF/ trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-definition.properties trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-en_GB.properties trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-fr_FR.properties Removed: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBunsleScopeTest.java Modified: trunk/nuiton-i18n-api/pom.xml trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/CountryEnum.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nFileReader.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nUtil.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/LanguageEnum.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundle.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleEntry.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleFactory.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleScope.java trunk/nuiton-i18n-api/src/main/java/org/nuiton/util/LocaleConverter.java trunk/nuiton-i18n-api/src/test/java/org/nuiton/util/LocaleConverterTest.java trunk/nuiton-i18n-api/src/test/resources/log4j.properties Modified: trunk/nuiton-i18n-api/pom.xml =================================================================== --- trunk/nuiton-i18n-api/pom.xml 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/pom.xml 2010-03-07 16:02:33 UTC (rev 1702) @@ -10,7 +10,7 @@ <parent> <groupId>org.nuiton</groupId> <artifactId>i18n</artifactId> - <version>1.0.2-SNAPSHOT</version> + <version>1.1-SNAPSHOT</version> </parent> <groupId>org.nuiton.i18n</groupId> @@ -29,10 +29,21 @@ </dependency> <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <scope>test</scope> + </dependency> + </dependencies> Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/CountryEnum.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/CountryEnum.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/CountryEnum.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -16,20 +16,23 @@ * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* */ + * ##%* + */ package org.nuiton.i18n; /** * Une énumération pour représenter le pays d'une locale * <p/> - * <a href="http://www.iso.org/iso/country_codes"><code>ISO 3166-1:1998 (ICS n° 01.140.20)</code></a>. + * <a href="http://www.iso.org/iso/country_codes"><code>ISO 3166-1:1998 (ICS n° + * 01.140.20)</code></a>. * <p/> - * <a href="http://www.iso.org/iso/french_country_names_and_code_elements">la liste des codes</a> + * <a href="http://www.iso.org/iso/french_country_names_and_code_elements">la + * liste des codes</a> * <p/> * Chaque pays est repésenté ainsi : * <pre>A2, // A3 Number Country name</pre> * - * @author chemit + * @author tchemit <chemit@codelutin.com> */ public enum CountryEnum { @@ -272,14 +275,17 @@ ZM, // ZMB 894 ZAMBIA ZW; // ZWE 716 ZIMBABWE - public static CountryEnum valueOf(String country, CountryEnum defaultValue) { + public static CountryEnum valueOf(String country, + CountryEnum defaultValue) { CountryEnum countryValue = null; try { - countryValue = CountryEnum.valueOf(country.toUpperCase()); + countryValue = valueOf(country.toUpperCase()); } catch (IllegalArgumentException e) { - System.err.println("unfound country " + country + ", will use default one : " + defaultValue); + System.err.println("unfound country " + country + + ", will use default one : " + defaultValue); } catch (NullPointerException e) { - System.err.println("unfound country " + country + ", will use default one : " + defaultValue); + System.err.println("unfound country " + country + + ", will use default one : " + defaultValue); } return countryValue == null ? defaultValue : countryValue; } Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18n.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18n.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18n.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,345 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.init.ClassPathI18nInitializer; +import org.nuiton.i18n.init.DefaultI18nInitializer; +import org.nuiton.i18n.init.I18nInitializer; + +import java.util.Arrays; +import java.util.Locale; + +/** + * New generation I18n class. + * <p/> + * <b>Note:</b> This class replace the previous one in project {@code + * nuiton-utils}. + * <p/> + * This class is a facility for internationalization. To use it in your soft, + * you can either : <ul> <li> import the org.nuiton.i18n.I18n class, <li> init + * the translation support with the init(String language) or init(String + * language, String country), init(Localelocale) static methods in your main, ( + * eg: I18n.init("fr","FR") ) <li> call the translate static method for each + * sentence, ( eg: I18n._("hello you !") ) <li> create a resource file for each + * language following the naming convention given in the + * java.util.ResourceBundle javadoc and translate all the sentence. </ul> + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class I18n { + + /** Logger */ + private static final Log log = LogFactory.getLog(I18n.class); + + public static final String ISO_8859_1_ENCONDING = "ISO-8859-1"; + + public static final String UTF_8_ENCONDING = "UTF-8"; + + public static final String DEFAULT_ENCODING = ISO_8859_1_ENCONDING; + + public static final Locale DEFAULT_LOCALE = Locale.UK; + + /** I18n initializer */ + protected static I18nInitializer initializer; + + /** I18n store of languages */ + protected static I18nStore store; + + /** Filtre a appliquer avant de retourner les chaines */ + protected static I18nFilter filter; + + /** + * Sets the initializer to use to init the I18n system. + * <p/> + * You must inovke this method before any invocation of methods {@code + * init(XXX)}. + * + * @param initializer the initializer to set. + * @see DefaultI18nInitializer + */ + public static void setInitializer(I18nInitializer initializer) { + I18n.initializer = initializer; + } + + /** + * Change le filtre des chaines traduites + * + * @param filter l'objet filtre a utiliser + */ + public static void setFilter(I18nFilter filter) { + I18n.filter = filter; + } + + /** Initialise la librairie avec encoding par defaut et locale par defaut */ + public static void init() { + init(null); + } + + /** + * Initialise la librairie + * + * @param language une chaine representant la langue à utiliser fr, en, ... + * @param country une chaine representant le pays à utiliser FR, GB, ... + */ + public static void init(String language, String country) { + init(I18nUtil.newLocale(language, country)); + } + + /** + * Initialize the library for given <code>locale</code> with {@link + * #DEFAULT_ENCODING}. + * + * @param locale language to use + */ + public static void init(Locale locale) { + if (initializer == null) { + initializer = new ClassPathI18nInitializer(); + } + + if (locale == null) { + // use default locale + locale = I18nUtil.newLocale(null, null); + } + + // let the store use this locale + getStore().setLanguage(locale); + } + + /** + * Retourne la chaine traduite si possible. + * + * @param message la chaine a traduire + * @return la traduction si possible ou la chaine passee en parametre + * sinon. + */ + public static String _(String message) { + + // if the key to translate is null, just return null + if (message == null) { + return null; + } + + I18nLanguage language = getCurrentLanguage(); + String result = message; + if (language != null) { + result = language.translate(message); + } + return applyFilter(result); + } + + /** + * Retourne la chaine traduite si possible. + * + * @param message message formate avec la meme syntaxe que {@link + * String#format} + * @param args les parametres pour le message. + * @return la traduction si possible ou la chaine passee en parametre + * sinon. + */ + public static String _(String message, Object... args) { + + // if the key to translate is null, just return null + if (message == null) { + return null; + } + + I18nLanguage language = getCurrentLanguage(); + String result = message; + if (language != null) { + result = language.translate(message); + } + try { + return applyFilter(String.format(result, args)); + } catch (Exception eee) { + try { + return applyFilter(String.format(message, args)); + } catch (Exception zzz) { + log.warn( + _("nuitonutil.error.i18n.untranslated.message", message), + zzz); + return applyFilter(message); + } + } + } + + /** + * Retourne la chaine passée en argument. + * <p/> + * Utile surtout pour collecter les chaines et ne pas les traduires à leur + * apparition. + * <p/> + * Par exemple : + * <pre>String key = "nuitonutils.key"; + * String result = _(key)</pre> + * fonctionnera, mais la chaine n'aura pas été marquée comme devant être + * internationalisé. + * <p/> + * Tres utile par exemple, pour crée des objets non internationnalisé, et + * devant être traduit seulement à leur lecture suivant la locale du lecteur + * et non du créateur. + * + * @param message message formate avec la meme syntaxe que {@link + * String#format(String, Object...)} + * @param args les parametres pour le message. + * @return le message passe en argument mais formatté avec les parametres + */ + public static String n_(String message, Object... args) { + try { + return String.format(message, args); + } catch (Exception eee) { + log.warn( + _("nuitonutil.error.i18n.unformated.message", message, Arrays.toString(args)), + eee); + return message; + } + } + + /** + * Retourne la chaine passé en argument. + * <p/> + * Utile surtout pour collecter les chaines et ne pas les traduires à leur + * apparition. + * <p/> + * Par exemple : + * <pre>String key = "nuitonutils.key"; + * String result = _(key)</pre> + * fonctionnera, mais la chaine n'aura pas été marquée comme devant être + * internationalisé. + * <p/> + * Tres utile par exemple, pour crée des objets non internationnalisé, et + * devant être traduit seulement à leur lecture suivant la locale du lecteur + * et non du créateur. + * + * @param message la chaine à traduire + * @return la chaine passée en argument. + */ + public static String n_(String message) { + return message; + } + + /** + * close i18n caches, says the store if exists + * <p/> + * This method should be called to reset all caches (languages, + * bundles,...) + */ + public static void close() { + if (store != null) { + store.close(); + store = null; + } + } + + public static I18nInitializer getInitializer() { + return initializer; + } + + /** + * Get the i18n store. + * <p/> + * If store is not init, then instanciate it. + * + * @return the instanciated i18n store + */ + public static synchronized I18nStore getStore() { + + if (store == null) { + store = new I18nStore(DEFAULT_LOCALE, initializer); + } + return store; + } + + /** + * Applique le filtre s'il y en a un + * + * @param message le message qui devrait etre retourne avant application du + * filtre. + * @return le message filtre + */ + protected static String applyFilter(String message) { + if (getFilter() != null) { + return getFilter().applyFilter(message); + } + return message; + } + + /** + * @return the current language of the store, or {@code null} if store is + * not init. + */ + protected static I18nLanguage getCurrentLanguage() { + I18nLanguage language = store == null ? null : store.getLanguage(); + return language; + } + + protected static I18nFilter getFilter() { + return filter; + } + + /** + * @return the current store + * @deprecated since 1.1, only keep for compatibility + */ + @Deprecated + public static I18nStore getLoader() { + return getStore(); + } + + /** + * Parse a list of {@link Locale} seperated by comma. + * <p/> + * Example : fr_FR,en_GB + * + * @param str the string representation of locale separated by comma + * @return list of available locales + * @throws IllegalArgumentException ia a locale is not valid + * @deprecated since 1.1, prefer use the {@link I18nUtil#parseLocales(String)} + */ + @Deprecated + public static Locale[] parseLocales(String str) + throws IllegalArgumentException { + return I18nUtil.parseLocales(str); + } + + /** + * @param str the text representation of the locale + * @return the locale, or default locale if could not parse the given one + * @deprecated since 1.1, prefer use the {@link I18nUtil#newLocale(String)} + */ + @Deprecated + public static Locale newLocale(String str) { + return I18nUtil.newLocale(str); + } + + /** + * @param language the language of the locale + * @param country the country of the locale + * @return the required locale + * @deprecated since 1.1, prefer use the {@link I18nUtil#newLocale(String,String)} + */ + @Deprecated + public static Locale newLocale(String language, String country) { + return I18nUtil.newLocale(language, country); + } +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18n.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nBundleBridge.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nBundleBridge.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nBundleBridge.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,51 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ + +/* * +* BundleBridge.java +* +* Created: 6 sept. 06 +* +* @author Arnaud Thimel <thimel@codelutin.com> +* @version $Revision$ +* +* Mise a jour: $Date$ +* par : $Author: tchemit $ +*/ + +package org.nuiton.i18n; + +import java.util.Enumeration; +import java.util.ResourceBundle; + +public class I18nBundleBridge extends ResourceBundle { + + @Override + public Enumeration<String> getKeys() { + throw new UnsupportedOperationException(); + } + + @Override + public Object handleGetObject(String key) { + return I18n._(key); + } + +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nBundleBridge.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nFileReader.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nFileReader.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nFileReader.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -31,40 +31,48 @@ package org.nuiton.i18n; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.nio.charset.Charset; import java.util.Properties; import java.util.regex.Pattern; -/** Classe assurant la lecture et les possibles traitement nécessaires à I18n. */ +/** + * Classe assurant la lecture et les possibles traitement nécessaires à I18n. + * + * @author tchemit <chemit@codelutin.com> + * @deprecated since 1.1, no more use of this class. + */ +@Deprecated public class I18nFileReader extends Properties { protected static final Pattern commentPattern = Pattern.compile("[^\\\\]#"); protected static final Pattern splitPattern = Pattern.compile("[^\\\\]="); + private static final long serialVersionUID = 3611718334066783394L; public void load(InputStream inStream, String encodingTo) throws IOException { Charset charsetTo = Charset.forName(encodingTo); - BufferedReader readerFile; - readerFile = new BufferedReader(new InputStreamReader(inStream, charsetTo)); - String lineFile; - StringBuilder builderFile; - builderFile = new StringBuilder(); - while ((lineFile = readerFile.readLine()) != null) { - builderFile.append(lineFile).append('\n'); + BufferedReader readerFile = + new BufferedReader(new InputStreamReader(inStream, charsetTo)); + try { + String lineFile; + StringBuilder builderFile; + builderFile = new StringBuilder(); + while ((lineFile = readerFile.readLine()) != null) { + builderFile.append(lineFile).append('\n'); + } + load(new ByteArrayInputStream(builderFile.toString().getBytes())); + } finally { + readerFile.close(); } - readerFile.close(); - super.load(new ByteArrayInputStream(builderFile.toString().getBytes())); +// super.load(new ByteArrayInputStream(builderFile.toString().getBytes())); } protected String interpretBackslashes(String message) { int backslashIndex = -1; - while ((backslashIndex = message.indexOf("\\", backslashIndex + 1)) != -1) { + while ((backslashIndex = + message.indexOf("\\", backslashIndex + 1)) != -1) { if (message.length() >= backslashIndex + 1) { char charNextToBackslash = message.charAt(backslashIndex + 1); char replacementChar; @@ -91,7 +99,9 @@ replacementChar = '\\'; break; } - message = message.substring(0, backslashIndex) + replacementChar + message.substring(backslashIndex + 2); + message = message.substring(0, backslashIndex) + + replacementChar + + message.substring(backslashIndex + 2); } } return message; @@ -124,7 +134,8 @@ replacementString = "\\:"; break; } - message = message.substring(0, charIndex) + replacementString + message.substring(charIndex + 1); + message = message.substring(0, charIndex) + replacementString + + message.substring(charIndex + 1); } } return message; Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nLanguage.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nLanguage.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nLanguage.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,253 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.bundle.I18nBundleEntry; + +import java.io.*; +import java.util.Enumeration; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.Properties; + +/** + * Represents a language in i18n system with all his translations. + * <p/> + * To obtain a translation, use the method {@link #translate(String)} + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class I18nLanguage { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + private static final Log log = LogFactory.getLog(I18nLanguage.class); + + /** toutes les traductions pour cette langue */ + protected Properties resource; + + /** la locale de la langue */ + protected final Locale locale; + + /** Indique le chemin du fichier dans lequel ecrire les entrees non trouvees */ + protected String recordFilePath; + + public I18nLanguage(Locale l) { + this(l, null); + } + + public I18nLanguage(Locale locale, String recordFilePath) { + this.locale = locale; + this.recordFilePath = recordFilePath; + } + + /** + * charge les traductions de la languea partir d'une liste donnee de + * fichiers de traduction. + * + * @param bundleEntries the used bundles entries to load + */ + public void load(I18nBundleEntry[] bundleEntries) { + + // use a recursive properties + // inspired from nuiton-utils:org.nuiton.util.RecursiveProperties + // thanks to Arnaud Thimel + resource = new Properties() { + private static final long serialVersionUID = 1L; + + @Override + public String getProperty(String key) { + String result = super.getProperty(key); + if (result == null) { + return null; + } + //Ex : result="My name is ${myName}." + int pos = result.indexOf("${", 0); + //Ex : pos=11 + while (pos != -1) { + int posEnd = result.indexOf("}", pos + 1); + //Ex : posEnd=19 + if (posEnd != -1) { + String value = + getProperty(result.substring(pos + 2, posEnd)); + // Ex : getProperty("myName"); + if (value != null) { + // Ex : value="Thimel" + result = result.substring(0, pos) + value + + result.substring(posEnd + 1); + // Ex : result="My name is " + "Thimel" + "." + pos = result.indexOf("${", pos + value.length()); + // Ex : pos=-1 + } else { + // Ex : value=null + pos = result.indexOf("${", posEnd + 1); + // Ex : pos=-1 + } + // Ex : pos=-1 + } + } + return result; + } + }; + + // load resources + + try { + for (I18nBundleEntry e : bundleEntries) { + e.load(resource); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Translate takes a sentence and returns its translation if found, the very + * same string otherwise. + * + * @param sentence sentence to translate + * @return translated sentence + */ + public String translate(String sentence) { + if (resource == null) { + recordNotFound(sentence); + return sentence; + } + try { + String result = resource.getProperty(sentence); + if (null != result && !"".equals(result)) { + return result; + } + recordNotFound(sentence); + return sentence; + } catch (MissingResourceException eee) { + log.warn("Resource " + sentence + " unavailable", eee); + return sentence; + } catch (Exception eee) { + log.error("Unexpected error while translating : ", eee); + return sentence; + } + } + + protected void recordNotFound(String key) { + if (recordFilePath != null && key != null && !"".equals(key)) { + File f = new File(recordFilePath); + Properties recordProps = new Properties(); + try { + if (f.exists()) { + FileInputStream fis = new FileInputStream(f); + try { + recordProps.load(fis); + } finally { + fis.close(); + } + } + recordProps.put(key, ""); + FileOutputStream fos = new FileOutputStream(f); + try { + recordProps.store(fos, "Adding the key : " + key); + } finally { + fos.close(); + } + } catch (FileNotFoundException e) { + if (log.isErrorEnabled()) { + log.error(e); + } + } catch (IOException e) { + if (log.isErrorEnabled()) { + log.error(e); + } + } + } + } + + /** + * Untranslate takes a translated sentence and returns the original one if + * found, the very same string otherwise. + * + * @param sentence sentence to untranslate + * @return untranslated sentence + */ + public String untranslate(String sentence) { + if (resource == null) { + return sentence; + } + try { + Enumeration<?> e = resource.propertyNames(); + // Look for the given sentence through all translations + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + String translation = resource.getProperty(key); + // If found returns the corresponding key + if (sentence.equals(translation)) { + return key; + } + } + } catch (MissingResourceException eee) { + // Well, this can't happen... + } + // No such translated sentence in our resourceBundle + return sentence; + } + + public Locale getLocale() { + return locale; + } + + public int size() { + return resource == null ? 0 : resource.size(); + } + + public void close() { + if (resource != null) { + if (log.isInfoEnabled()) { + log.info("closing " + this); + } + resource.clear(); + resource = null; + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + @Override + public boolean equals(Object o) { + return this == o || + o instanceof I18nLanguage && + locale.equals(((I18nLanguage) o).locale); + } + + @Override + public int hashCode() { + return locale.hashCode(); + } + + @Override + public String toString() { + return "I18nLanguage <locale: " + locale + ", nbStences:" + size() + '>'; + } +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nLanguage.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nStore.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nStore.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nStore.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,230 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.bundle.I18nBundle; +import org.nuiton.i18n.bundle.I18nBundleEntry; +import org.nuiton.i18n.bundle.I18nBundleUtil; +import org.nuiton.i18n.init.I18nInitializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Represents the store of languages of the i18n system. + * <p/> + * Replace the {@code org.nuiton.i18n.I18nLoader} class from project + * http://maven-site.nuiton.org/nuiton-utils + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class I18nStore { + + /** Logger */ + private static final Log log = LogFactory.getLog(I18nStore.class); + + /** le language actuellement utilise */ + protected I18nLanguage language; + + /** le cache de languages deja charges */ + protected List<I18nLanguage> languages; + + /** le cache de bundles deja charges */ + protected I18nBundle[] bundles; + + /** la locale par defaut a utiliser */ + protected Locale defaultLocale; + + /** le resolver de bundles */ + protected I18nInitializer resolver; + + public I18nStore(Locale defaultLocale, I18nInitializer resolver) { + this.defaultLocale = defaultLocale; + this.resolver = resolver; + } + + /** @return current language loaded or null, if no language was load */ + public I18nLanguage getLanguage() { + return language; + } + + /** @return le cache de language avec instanciation paresseuse */ + public List<I18nLanguage> getLanguages() { + if (languages == null) { + languages = new ArrayList<I18nLanguage>(); + } + return languages; + } + + public Locale getDefaultLocale() { + return defaultLocale; + } + + public boolean isEmpty() { + boolean isEmpty = I18nBundleUtil.isEmpty(getBundles()); + return isEmpty; + } + + /** @return array of all locales loaded */ + public Locale[] getLocales() { + Locale[] result = I18nBundleUtil.getLocales(getBundles()); + return result; + } + + public I18nBundle[] getBundles() { + checkInit(); + return bundles; + } + + public I18nBundle[] getBundles(Locale l) { + I18nBundle[] result = I18nBundleUtil.getBundles(l, getBundles()); + return result; + } + + public I18nBundleEntry[] getBundleEntries() { + I18nBundleEntry[] result = I18nBundleUtil.getBundleEntries(getBundles()); + return result; + } + + public I18nBundleEntry[] getBundleEntries(Locale l) { + I18nBundleEntry[] result = + I18nBundleUtil.getBundleEntries(l, getDefaultLocale(), + getBundles()); + return result; + } + + protected void init() { + + if (isInit()) { + // already init + return; + } + + if (resolver == null) { + throw new NullPointerException( + "resolver can not be null in " + + "org.nuiton.i18n.I18nStore.init method"); + } + + try { + bundles = resolver.resolvBundles(); + } catch (Exception e) { + throw new RuntimeException( + "Could not init store for reason " + e.getMessage(), e); + } + + if (log.isInfoEnabled()) { + log.info(bundles.length + " bundle(s) found, [" + + getBundleEntries().length + " file(s)]."); + } + } + + /** + * Set a new language in store, given a locale. + * + * @param locale la locale du language requis + */ + protected synchronized void setLanguage(Locale locale) { + init(); + if (log.isDebugEnabled()) { + log.debug("locale: " + locale); + } + I18nLanguage result = getLanguage(locale); + if (result == null) { + result = addLanguage(locale); + } else { + if (log.isDebugEnabled()) { + log.debug("using cached language : " + result); + } + } + language = result; + //TC-20090702 the selected langue is the default locale, usefull for + // objects dealing with the default locale (swing widgets,...) + Locale.setDefault(locale); + } + + /** + * Close store and release cache ofg language. + * <p/> + * Current language will be also clean. + */ + protected void close() { + if (languages != null) { + if (log.isInfoEnabled()) { + log.info("will close " + languages.size() + " language(s)."); + } + for (I18nLanguage l : languages) { + l.close(); + } + languages.clear(); + languages = null; + } + if (bundles != null) { + bundles = null; + } + language = null; + } + + /** + * @param locale la locale du language recherche + * @return le language trouve dans le cache, ou null. + */ + protected I18nLanguage getLanguage(Locale locale) { + + if (!(languages == null || languages.isEmpty())) { + for (I18nLanguage l : languages) { + if (locale.equals(l.getLocale())) { + return l; + } + } + } + return null; + } + + protected I18nLanguage addLanguage(Locale locale) { + I18nLanguage result; + result = new I18nLanguage(locale); + I18nBundleEntry[] entries = getBundleEntries(locale); + result.load(entries); + + if (log.isInfoEnabled()) { + log.info(result + ", nbEntries: " + entries.length + + ", nbSentences: " + result.size() + "."); + } + getLanguages().add(result); + return result; + } + + protected boolean isInit() { + return bundles != null; + } + + protected void checkInit() { + if (!isInit()) { + throw new IllegalStateException( + "should call init method on " + I18nStore.class); + } + } +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nStore.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nUtil.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nUtil.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/I18nUtil.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -20,52 +20,60 @@ */ package org.nuiton.i18n; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.util.LocaleConverter; + import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.logging.Logger; +import java.net.URLClassLoader; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.zip.ZipFile; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.nuiton.util.LocaleConverter; -/** - * - * @author chemit - */ +/** @author tchemit <chemit@codelutin.com> */ public class I18nUtil { /** to use log facility, just put in your code: log.info(\"...\"); */ private static final Log log = LogFactory.getLog(I18nUtil.class); + public static final String ISO_8859_1_ENCONDING = "ISO-8859-1"; + public static final String UTF_8_ENCONDING = "UTF-8"; + public static final String DEFAULT_ENCODING = ISO_8859_1_ENCONDING; + public static final Locale DEFAULT_LOCALE = Locale.UK; /** * Parse a list of {@link Locale} seperated by comma. - * + * <p/> * Example : fr_FR,en_GB * * @param str the string representation of locale separated by comma * @return list of available locales * @throws IllegalArgumentException ia a locale is not valid */ - public static Locale[] parseLocales(String str) throws IllegalArgumentException { - List<Locale> result = new java.util.ArrayList<Locale>(); + public static Locale[] parseLocales(String str) throws + IllegalArgumentException { + List<Locale> result = new ArrayList<Locale>(); String[] bundlesToUse = str.split(","); + LocaleConverter converter = new LocaleConverter(); for (int i = 0, j = bundlesToUse.length; i < j; i++) { String s = bundlesToUse[i].trim(); // on devrait verifier que le bundle existe try { - Locale l = (Locale) new LocaleConverter().convert(Locale.class, s); + Locale l = (Locale) converter.convert(Locale.class, s); result.add(l); } catch (Exception e) { - throw new IllegalArgumentException("bundle " + s + " is not a valid locale,e"); + throw new IllegalArgumentException("bundle " + s + + " is not a valid locale,e"); } } return result.toArray(new Locale[result.size()]); @@ -79,7 +87,8 @@ try { return (Locale) new LocaleConverter().convert(Locale.class, str); } catch (Exception e) { - Logger.getLogger("org.nuiton.i18n.I18n").warning("could not load locale '" + str + " for reason : " + e.getMessage()); + log.warn("could not load locale '" + str + " for reason : " + + e.getMessage()); // use default locale return DEFAULT_LOCALE; } @@ -88,8 +97,10 @@ public static Locale newLocale(String language, String country) { if (language == null) { // get user locale - language = System.getProperty("user.language", DEFAULT_LOCALE.getLanguage()); - country = System.getProperty("user.country", DEFAULT_LOCALE.getCountry()); + language = System.getProperty("user.language", + DEFAULT_LOCALE.getLanguage()); + country = System.getProperty("user.country", + DEFAULT_LOCALE.getCountry()); } return newLocale(language + (country == null ? "" : '_' + country)); } @@ -99,12 +110,14 @@ * * @param url the url to seek * @param directory the directory to find - * @return <code>true</code> if directory was found, <code>false</code> otherwise. - * @throws java.io.IOException if any io pb + * @return {@code true} if directory was found, {@code false} otherwise. + * @throws IOException if any io pb */ - public static boolean containsDirectDirectory(URL url, String directory) throws IOException { + public static boolean containsDirectDirectory(URL url, String directory) + throws IOException { String fileName = url.getFile(); - // TODO deal with encoding in windows, this is very durty, but it works... + // TODO deal with encoding in windows, this is very durty, + // TODO but it works... File file = new File(fileName.replaceAll("%20", " ")); if (!file.exists()) { return false; @@ -163,12 +176,14 @@ * ordre ne doit être supposé sur les fichiers. * * @param repository repertoire dans lequel on recherche les fichiers - * @param pattern le nom du fichier a extraire du fichier du repertoire doit - * correspondre au pattern (repertoire + nom compris). si le - * pattern est null, tous les fichiers trouvé sont retourné. + * @param pattern le nom du fichier a extraire du fichier du repertoire + * doit correspondre au pattern (repertoire + nom + * compris). si le pattern est null, tous les fichiers + * trouvé sont retourné. * @return la liste des urls correspondant au pattern */ - static public List<URL> getURLsFromDirectory(File repository, String pattern) { + static public List<URL> getURLsFromDirectory(File repository, + String pattern) { try { if (log.isTraceEnabled()) { log.trace("search '" + pattern + "' in " + repository); @@ -184,19 +199,21 @@ String name = file.getAbsolutePath(); if (log.isTraceEnabled()) { - log.trace("directory: " + repository + " name: " + name); + log.trace("directory: " + repository + " name: " + + name); } // cas de recursivite : repertoire dans un repertoire if (file.exists() && file.isDirectory()) { urlList.addAll(getURLsFromDirectory(file, - pattern)); + pattern)); // si le fichier du repertoire n'est pas un repertoire // on verifie s'il correspond au pattern } else if (pattern == null || name.matches(pattern)) { URL url = file.toURI().toURL(); if (log.isTraceEnabled()) { - log.trace("directory: " + repository + " url: " + url); + log.trace("directory: " + repository + " url: " + + url); } urlList.add(url); } @@ -207,8 +224,147 @@ } return urlList; } catch (MalformedURLException eee) { - throw new IllegalArgumentException("Erreur lors de la conversion de l'url " + repository + " (pattern " + pattern + ") " + eee.getMessage(), eee); + throw new IllegalArgumentException( + "Erreur lors de la conversion de l'url " + repository + + " (pattern " + pattern + ") " + eee.getMessage(), eee); //throw new ResourceException("Le fichier n'a pu être converti en URL", eee); } } + + /** + * Returns the all urls to be used in a {@link URLClassLoader}. + * <p/> + * If classloader has only one url and the url is a jar, try to load in + * manifest class-path. + * + * @param loader the classloader (if null will use system one) + * @return all the url found in the classloader + */ + static public URL[] getDeepURLs(URLClassLoader loader) { + Stack<URL> urlToTreate = new Stack<URL>(); + List<URL> urlTreated = new ArrayList<URL>(); + + // first get the urls from classloader + URL[] result = getURLs(loader); + + urlToTreate.addAll(Arrays.asList(result)); + while (!urlToTreate.isEmpty()) { + URL currentUrl = urlToTreate.pop(); + // save the url + urlTreated.add(currentUrl); + if (isJar(currentUrl.toString())) { + // jar invocation + try { + URL[] newArrayURLs = + getClassPathURLsFromJarManifest( + currentUrl); + if (newArrayURLs == null) { + continue; + } + List<URL> newURLs = Arrays.asList(newArrayURLs); + for (URL newURL : newURLs) { + if (!urlTreated.contains(newURL) && + !urlToTreate.contains(newURL)) { + urlToTreate.add(newURL); + } + } + } catch (Exception e) { + if (log.isDebugEnabled()) { + // this is not a such error, but some jar can not be + log.debug("error with url" + currentUrl + + " for reason : " + e.getMessage()); + } + } + } + } + return urlTreated.toArray(new URL[urlToTreate.size()]); + } + + /** + * Recupere la liste des urls d'un {@link URLClassLoader}. + * <p/> + * Note : Un cas particulier est positionné pour JBoss qui utilise la method + * getAllURLs. + * + * @param classLoader le class loader a scanner + * @return les urls du classloade. + */ + static public URL[] getURLs(URLClassLoader classLoader) { + if (classLoader == null) { + classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + } + Method m; + try { + // Essai de récupération de la méthode getAllURLs() de + // RepositoryClassLoader (JBoss) + m = classLoader.getClass().getMethod("getAllURLs"); + } catch (Exception e) { + m = null; + } + URL[] result; + if (m == null) { + result = classLoader.getURLs(); + } else { + try { + result = (URL[]) m.invoke(classLoader); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return result; + } + + static public URL[] getClassPathURLsFromJarManifest(URL jarURL) + throws IOException, URISyntaxException { + JarFile jar = null; + URL[] result; + try { + String jarPath = jarURL.toURI().getPath(); + File jarFile = new File(jarPath); + if (log.isDebugEnabled()) { + log.debug("class-path jar to scan " + jarPath); + } + jar = new JarFile(jarFile); + File container = jarFile.getParentFile(); + Manifest mf = jar.getManifest(); + String classPath = null; + if (mf != null && mf.getMainAttributes() != null) { + classPath = mf.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + } + String[] paths; + if (classPath != null) { + paths = classPath.split(" "); + } else { + paths = new String[0]; + } + result = new URL[paths.length + 1]; + result[0] = jarURL; + File path; + for (int i = 0; i < paths.length; i++) { + String s = paths[i]; + // test de l'existence d'un protocole dans le path (genre file:...) + if (s.indexOf(':') != -1) { + result[i + 1] = new URL(s); + continue; + } + + if (s.startsWith(".") || !s.startsWith("/")) { + // relative url + path = new File(container, s); + } else { + path = new File(s); + } + if (log.isDebugEnabled()) { + log.debug(path); + } + result[i + 1] = path.toURI().toURL(); + } + jar.close(); + } finally { + if (jar != null) { + jar.close(); + } + } + return result; + } } Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/LanguageEnum.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/LanguageEnum.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/LanguageEnum.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -16,16 +16,19 @@ * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* */ + * ##%* + */ package org.nuiton.i18n; /** * Une énumération pour représenter le langue d'une locale définie dans la norme - * <a href="http://www.iso.org/iso/iso_catalogue/catalogue_ics/catalogue_detail_ics.htm?csnumber=22109&ICS1=1&ICS2=140&ICS3=20"><code>ISO 639-1:1998 (ICS n° 01.140.20)</code></a>. + * <a href="http://www.iso.org/iso/iso_catalogue/catalogue_ics/catalogue_detail_ics.htm?csnumber=22109&ICS1=1&ICS2=140&ICS3=20"><code>ISO + * 639-1:1998 (ICS n° 01.140.20)</code></a>. * <p/> - * <a href="http://www.loc.gov/standards/iso639-2/php/French_list.php">la liste des codes</a> + * <a href="http://www.loc.gov/standards/iso639-2/php/French_list.php">la liste + * des codes</a> * - * @author chemit + * @author tchemit <chemit@codelutin.com> */ public enum LanguageEnum { @@ -169,14 +172,17 @@ zh, // Chinese zu; // Zulu - public static LanguageEnum valueOf(String language, LanguageEnum defaultValue) { + public static LanguageEnum valueOf(String language, + LanguageEnum defaultValue) { LanguageEnum languageValue = null; try { - languageValue = LanguageEnum.valueOf(language.toLowerCase()); + languageValue = valueOf(language.toLowerCase()); } catch (IllegalArgumentException e) { - System.err.println("Unfound language " + language + ", will use default one " + defaultValue); + System.err.println("Unfound language " + language + + ", will use default one " + defaultValue); } catch (NullPointerException e) { - System.err.println("Unfound language " + language + ", will use default one " + defaultValue); + System.err.println("Unfound language " + language + + ", will use default one " + defaultValue); } return languageValue == null ? defaultValue : languageValue; } Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundle.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundle.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundle.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* -* *##% + * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -16,43 +16,49 @@ * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* */ + * ##%* + */ package org.nuiton.i18n.bundle; -import java.util.Iterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Locale; /** * Class to represent a i18n Bundle. * <p/> - * A bundle is defined by a resource prefix (eg /tmp/bundle.properties), and a list of locale implemented entries. + * A bundle is defined by a resource prefix (eg /tmp/bundle.properties), and a + * list of locale implemented entries. * <p/> * The property {@link #bundlePrefix} is the equals order property. * <p/> * The property {@link #entries} contains all entries defined for this bundle. * <p/> - * The method {@link #getEntries(java.util.Locale)} filter entries for a given locale, including scope inclusive property. + * The method {@link #getEntries(Locale)} filter entries for a given locale, + * including scope inclusive property. * <p/> - * The method {@link #getEntries(I18nBundleScope)} filter entries for a givne scope, with no inclusive logi. + * The method {@link #getEntries(I18nBundleScope)} filter entries for a given + * scope, with no inclusive logi. * <p/> - * Thoses filter methods return result in the order defines in {@link I18nBundleEntry}, e.g + * Thoses filter methods return result in the order defines in {@link + * I18nBundleEntry}, e.g * <pre> * XXX.properties * XXX-fr.properties * XXX-fr_FR.properties * </pre> - * In that way, we can load resource in the good order : load before more general scope to more specialized. + * In that way, we can load resource in the good order : load before more + * general scope to more specialized. * - * @author chemit + * @author tchemit <chemit@codelutin.com> * @see I18nBundleScope * @see I18nBundleEntry */ -public class I18nBundle implements Iterable<I18nBundleEntry>{ +public class I18nBundle implements Iterable<I18nBundleEntry> { /** to use log facility, just put in your code: log.info(\"...\"); */ static final Log log = LogFactory.getLog(I18nBundle.class); @@ -77,7 +83,8 @@ * The order of result respect {@link I18nBundleEntry} order. * * @param locale the required locale - * @return the array of entries matching extacly the locale or one of the lesser scope one. + * @return the array of entries matching extacly the locale or one of the + * lesser scope one. */ public I18nBundleEntry[] getEntries(Locale locale) { I18nBundleScope scope = I18nBundleScope.valueOf(locale); @@ -85,8 +92,11 @@ List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); for (I18nBundleEntry entry : entries) { I18nBundleScope i18nBundleScope = entry.getScope(); - // load from general to the max scope and always if there is only one bundle entry found - if ((i18nBundleScope == scope || i18nBundleScope.ordinal() < scope.ordinal()) && entry.matchLocale(locale, scope)) { + // load from general to the max scope and always if there is + // only one bundle entry found + if ((i18nBundleScope == scope || + i18nBundleScope.ordinal() < scope.ordinal()) && + entry.matchLocale(locale, scope)) { result.add(entry); } } @@ -94,7 +104,8 @@ } /** - * Obtain the entries for a given <code>scope</code> <ith no incluvie logic. + * Obtain the entries for a given <code>scope</code> with no incluvie + * logic. * <p/> * The order of result respect {@link I18nBundleEntry} order. * @@ -105,7 +116,8 @@ List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); for (I18nBundleEntry entry : entries) { I18nBundleScope i18nBundleScope = entry.getScope(); - // load from general to the max scope and always if there is only one bundle entry found + // load from general to the max scope and always if there is + // only one bundle entry found if (i18nBundleScope == scope) { result.add(entry); } @@ -121,7 +133,8 @@ @Override public String toString() { String s = super.toString(); - return "<" + s.substring(s.lastIndexOf(".") + 1) + ", bundlePrefix:" + bundlePrefix + ", size:" + size() + ">"; + return "<" + s.substring(s.lastIndexOf(".") + 1) + ", bundlePrefix:" + + bundlePrefix + ", size:" + size() + ">"; } protected List<I18nBundleEntry> getEntries() { Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleEntry.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleEntry.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleEntry.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -16,53 +16,53 @@ * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* */ + * ##%* + */ package org.nuiton.i18n.bundle; -import java.io.IOException; -import java.io.InputStream; +import org.nuiton.i18n.I18nUtil; + +import java.io.*; import java.net.URL; +import java.nio.charset.Charset; import java.util.Locale; import java.util.Map.Entry; import java.util.Properties; -import org.nuiton.i18n.I18nUtil; -import org.nuiton.i18n.I18nFileReader; /** * A class to represent an entry in a bundle. * <p/> * The object matches exactly one resource file in a given scope. * <p/> - * The object has three properties : - * <ul> - * <li> {@link #path} : the path to resource file where to find transaltion for the entry. - * <li> {@link #locale} : the locale of the entry - * <li> {link #scope} ; the scope of the entry - * </ul> + * The object has three properties : <ul> <li> {@link #path} : the path to + * resource file where to find transaltion for the entry. <li> {@link #locale} : + * the locale of the entry <li> {link #scope} ; the scope of the entry </ul> * This object defines a equals order base on property {@link #path}. * <p/> * This object is {@link Comparable}, the order relation is defined like this : - * <ul> - * <li> sort first on {@link #scope}, in the scope order (see {@link I18nBundleScope}), - * <li> if scopes are equals, sort on {@link #locale} string representation. - * </ul> + * <ul> <li> sort first on {@link #scope}, in the scope order (see {@link + * I18nBundleScope}), <li> if scopes are equals, sort on {@link #locale} string + * representation. </ul> * - * @author chemit + * @author tchemit <chemit@codelutin.com> * @see I18nBundleScope */ public class I18nBundleEntry implements Comparable<I18nBundleEntry> { /** path to resource file */ protected URL path; + /** local of the entry, can be null if general scope */ protected Locale locale; + /** scope of the entry */ protected I18nBundleScope scope; /** * Constructor if an bundle entry. * <p/> - * It is defined by a <code>path</code> of the resource file, a scope and a locale. + * It is defined by a <code>path</code> of the resource file, a scope and a + * locale. * * @param path the path of the resource file fo the bundle entry * @param locale the given locale of the bundle entry @@ -89,13 +89,12 @@ /** * Method to match or not a bundle entry for a given scope and locale. * <p/> - * We use the inclusive property of scope, means that we accept all entries on the path - * to the generalest entry for a givne locale. + * We use the inclusive property of scope, means that we accept all entries + * on the path to the generalest entry for a givne locale. * * @param locale the locale to match * @param scope the highest scope to match - * @return <code>true</code> if the entry match the scope and locale - * * + * @return <code>true</code> if the entry match the scope and locale * */ public boolean matchLocale(Locale locale, I18nBundleScope scope) { if (this.locale == null) { @@ -108,12 +107,13 @@ } // match full locale, or at least a language return this.locale.equals(locale) || - (this.scope.ordinal() < scope.ordinal() && locale.getLanguage().equals(this.locale.getLanguage())); + this.scope.ordinal() < scope.ordinal() && + locale.getLanguage().equals(this.locale.getLanguage()); } /** - * For a given language, load the resource file of this entry into the <code>resource</code> - * properties object. + * For a given language, load the resource file of this entry into the + * <code>resource</code> properties object. * * @param resource the save of resources already loaded * @throws IOException if any pb while reading resource file @@ -122,18 +122,48 @@ InputStream inputStream = null; StringBuilder sb = new StringBuilder(); try { - I18nFileReader fileReader = new I18nFileReader(); +// I18nFileReader fileReader = new I18nFileReader(); + Properties fileReader = new Properties() { + + private static final long serialVersionUID = 1L; + + public void load(InputStream inStream) throws IOException { + String charset = I18nUtil.ISO_8859_1_ENCONDING; + Charset charsetTo = Charset.forName(charset); + + BufferedReader readerFile = new BufferedReader( + new InputStreamReader(inStream, charsetTo)); + try { + String lineFile; + StringBuilder builderFile; + builderFile = new StringBuilder(); + while ((lineFile = readerFile.readLine()) != null) { + builderFile.append(lineFile).append('\n'); + } + super.load(new ByteArrayInputStream( + builderFile.toString().getBytes())); + } finally { + readerFile.close(); + } + } + }; + inputStream = getPath().openStream(); //String encoding = language.getEncoding(); if (I18nBundle.log.isDebugEnabled()) { sb.append(getPath()).append("\n"); } - // TC 20081117 always use ISO_8859_1_ENCONDING, since java does it like this. - fileReader.load(inputStream, I18nUtil.ISO_8859_1_ENCONDING); + // TC 20081117 always use ISO_8859_1_ENCONDING, since java does + // it like this. + fileReader.load(inputStream); +// fileReader.load(inputStream, I18nUtil.ISO_8859_1_ENCONDING); if (I18nBundle.log.isDebugEnabled()) { for (Entry<Object, Object> entry : fileReader.entrySet()) { - sb.append(I18nUtil.ISO_8859_1_ENCONDING).append(" : ").append(entry).append("\n"); + sb.append(I18nUtil.ISO_8859_1_ENCONDING); + sb.append(" : "); + sb.append(entry); + sb.append("\n"); } } for (Entry<Object, Object> entry : fileReader.entrySet()) { @@ -151,7 +181,9 @@ } //resource.putAll(fileReader); if (I18nBundle.log.isDebugEnabled()) { - sb.append("nbSentences : ").append(fileReader.size()).append("\n"); + sb.append("nbSentences : "); + sb.append(fileReader.size()); + sb.append("\n"); sb.append("====================================="); } fileReader.clear(); @@ -178,7 +210,9 @@ @Override public boolean equals(Object o) { - return this == o || o instanceof I18nBundleEntry && path.equals(((I18nBundleEntry) o).path); + return this == o || + o instanceof I18nBundleEntry && + path.equals(((I18nBundleEntry) o).path); } @Override @@ -189,6 +223,7 @@ @Override public String toString() { String s = super.toString(); - return "<" + s.substring(s.lastIndexOf(".") + 1) + ", locale:" + locale + ", scope " + scope + ", path:" + path + ">"; + return "<" + s.substring(s.lastIndexOf(".") + 1) + ", locale:" + + locale + ", scope " + scope + ", path:" + path + ">"; } } Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleFactory.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleFactory.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleFactory.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ /* * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -20,230 +20,95 @@ */ package org.nuiton.i18n.bundle; -import java.io.File; -import java.io.FileInputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.I18nUtil; +import org.nuiton.i18n.init.I18nInitializer; + import java.io.InputStream; -import java.net.MalformedURLException; import java.net.URL; -import java.net.URLClassLoader; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Properties; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.nuiton.i18n.I18nUtil; /** - * Classe qui est responsable de la detection et construction - * de {@link I18nBundle}. - * + * Classe qui est responsable de la detection et construction de {@link + * I18nBundle}. + * <p/> * On retrouve aussi ici des méthodes utiles de parcours de bundles. * - * @author chemit - * + * @author tchemit <chemit@codelutin.com> * @since 1.0.6 + * @deprecated since 1.1, prefer use the {@link I18nBundleUtil} class instead. */ -public class I18nBundleFactory { +@Deprecated +public class I18nBundleFactory extends I18nBundleUtil { /** to use log facility, just put in your code: log.info(\"...\"); */ private static final Log log = LogFactory.getLog(I18nBundleFactory.class); - /** pattern to find all i18n bundles in classloader class path */ - public static final String SEARCH_BUNDLE_PATTERN = ".*i18n/.+\\.properties"; - public static final String DIRECTORY_SEARCH_BUNDLE_PATTERN = "i18n"; + protected static String UNIQUE_BUNDLE_PATH = "/META-INF/"; + public static String UNIQUE_BUNDLE_DEF = "%1$s-definition.properties"; + public static String UNIQUE_BUNDLE_ENTRY = "%1$s-%2$s.properties"; + public static String BUNDLE_DEF_LOCALES = "locales"; + public static String BUNDLES_FOR_LOCALE = "bundles."; /** - * Récuperation de toutes les locales connus par un ensemble de bundles. - * - * @param bundles les bundles a parcourir - * @return la liste des locales rencontrées - */ - public static Locale[] getLocales(I18nBundle... bundles) { - Set<Locale> result = new java.util.HashSet<Locale>(); - for (I18nBundle i18nBundle : bundles) { - for (I18nBundleEntry entry : i18nBundle.getEntries()) { - Locale o = entry.getLocale(); - if (o != null) { - result.add(o); - } - } - } - return result.toArray(new Locale[result.size()]); - } - - /** - * Récuperation des noms de bundle par un ensemble de bundles. - * - * @param bundles les bundles a parcourir - * @return la liste des noms de bundle rencontrées - */ - public static String[] getBundleNames(I18nBundle... bundles) { - List<String> result = new ArrayList<String>(); - for (I18nBundle i18nBundle : bundles) { - result.add(i18nBundle.getBundlePrefix()); - } - return result.toArray(new String[result.size()]); - } - - /** - * Filtrage des bundles qui correspondante à la locale donnée. - * - * @param l la locale à filtrer - * @param bundles les bundles a parcourir - * @return les bundles qui correspondent à la locale donnée. - */ - public static I18nBundle[] getBundles(Locale l, I18nBundle... bundles) { - List<I18nBundle> result = new ArrayList<I18nBundle>(); - for (I18nBundle i18nBundle : bundles) { - if (i18nBundle.matchLocale(l)) { - result.add(i18nBundle); - } - } - return result.toArray(new I18nBundle[result.size()]); - } - - /** - * Récupération de toutes les entrées de bundles pour les bundles données. - * - * @param bundles les bundles a parcourir - * @return toutes les entrées de bundles. - */ - public static I18nBundleEntry[] getBundleEntries(I18nBundle... bundles) { - List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); - for (I18nBundle i18nBundle : bundles) { - List<I18nBundleEntry> list = i18nBundle.getEntries(); - if (!list.isEmpty()) { - result.addAll(list); - } - } - return result.toArray(new I18nBundleEntry[result.size()]); - } - - /** - * Filtrage des entrées de bundles pour une locale donnée. - * - * On essaye de trouver les meilleurs entrées possibles (possibilité de - * promotion). - * - * Note: Cette méthode doit être utilisé pour trouver toutes les entrées à - * charger par le système i18n pour une locale donnée. - * - * Note: Par defaut, on n'effectue pas les promotions générales - * ({@link #getBundleEntries(Locale, Locale, boolean, I18nBundle[])} - * - * @param l la locale à filtrer - * @param defaultLocale la locale à utiliser pour les promotions - * @param bundles les bundles a parcourir - * @return les entrées de bundles filtrés. - */ - public static I18nBundleEntry[] getBundleEntries(Locale l, Locale defaultLocale, I18nBundle... bundles) { - return getBundleEntries(l, defaultLocale, false, bundles); - } - - /** - * Filtrage des entrées de bundles pour une locale donnée. - * - * On essaye de trouver les meilleurs entrées possibles (possibilité de - * promotion). - * - * Note: Cette méthode doit être utilisé pour trouver toutes les entrées à - * charger par le système i18n pour une locale donnée. - * - * @param l la locale à filtrer - * @param defaultLocale la locale à utiliser pour les promotions - * @param promuteGeneral un drapeau pour indiquer si l'on autorise le - * chargement de la locale par defaut si pour un bundle donne on a - * pas trouve de traductions pour la locale donnee. - * @param bundles les bundles a parcourir - * @return les entrées de bundles filtrés. - */ - public static I18nBundleEntry[] getBundleEntries(Locale l, Locale defaultLocale, boolean promuteGeneral, I18nBundle... bundles) { - - List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); - for (I18nBundle i18nBundle : bundles) { - I18nBundleEntry[] entries = i18nBundle.getEntries(l); - if (entries.length == 0) { - //no entry found for the bundle, try pomotion - entries = promuteBundle(i18nBundle, l, defaultLocale, promuteGeneral); - } - result.addAll(Arrays.asList(entries)); - } - return result.toArray(new I18nBundleEntry[result.size()]); - } - - /** - * Teste si un ensemble de bundles contient au moins une entrée. - * - * @param bundles les bundles a parcourir - * @return <code>true</code> si aucune entree trouvee, <code>false</code> - * autrement. - */ - public static boolean isEmpty(I18nBundle... bundles) { - for (I18nBundle i18nBundle : bundles) { - if (!i18nBundle.getEntries().isEmpty()) { - // on a trouve au moins une entree - return false; - } - } - return true; - } - - /** - * Recherche la liste des url de toutes les resources i18n, i.e les urls - * des fichiers de traduction en mode uniqueBundleName. - * + * Recherche la liste des url de toutes les resources i18n, i.e les urls des + * fichiers de traduction en mode uniqueBundleName. + * <p/> * On va d'abord rechercher un fichier /META-INF/unqiueBundleName-definition.properties - * - * Dans ce fichier il y a une entree locales qui contient les locales du bundle - * + * <p/> + * Dans ce fichier il y a une entree locales qui contient les locales du + * bundle + * <p/> * Ensuite pour chaque locale on recupere l'url du fichier : - * + * <p/> * /META-INF/uniqueBundleName-locale.properties - * + * <p/> * Exemple : - * - * <code> - * fichier de définition : /META-INF/monAppli-definition.properties + * <p/> + * <code> fichier de définition : /META-INF/monAppli-definition.properties * locales=fr_fr,es_ES - * - * fichiers de traduction - * /META-INF/monAppli-fr_FR.properties + * <p/> + * fichiers de traduction /META-INF/monAppli-fr_FR.properties * /META-INF/monAppli-es_ES.properties - * + * <p/> * </code> * * @param uniqueBundleName le nom de l'unique bundle a charger * @return la liste des urls de bundle i18n + * @deprecated since 1.1, prefer use the {@link I18nInitializer} api + * instead. */ + @Deprecated public static URL[] getURLs(String uniqueBundleName) { + String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, + uniqueBundleName); + URL[] urls; - String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, uniqueBundleName); - URL[] urls = null; - try { - URL defURL = I18nBundleFactory.class.getResource(UNIQUE_BUNDLE_PATH + definitionFileName); + URL defURL = I18nBundleFactory.class.getResource( + UNIQUE_BUNDLE_PATH + definitionFileName); Properties p = loadUniqueNameDefFile(uniqueBundleName); String localesAsStr = p.getProperty(BUNDLE_DEF_LOCALES); Locale[] locales = I18nUtil.parseLocales(localesAsStr); - List<URL> lUrls = new java.util.ArrayList<URL>(1); + List<URL> lUrls = new ArrayList<URL>(1); String prefixURL = defURL.toString(); - prefixURL = prefixURL.substring(0, prefixURL.length() - definitionFileName.length()); + prefixURL = prefixURL.substring(0, prefixURL.length() - + definitionFileName.length()); //FIXME on devrait tester que la resource est disponible ? for (Locale l : locales) { - String url = prefixURL + String.format(UNIQUE_BUNDLE_ENTRY, uniqueBundleName, l.toString()); + String url = prefixURL + String.format(UNIQUE_BUNDLE_ENTRY, + uniqueBundleName, + l.toString()); log.info("detected bundle properties file : " + url); URL u = new URL(url); // //FIXME on devrait tester que la resource est disponible ? @@ -261,373 +126,37 @@ } } catch (Exception ex) { - log.warn("could not load unique bundle " + uniqueBundleName + " for reason " + ex.getMessage(), ex); + log.warn("could not load unique bundle " + uniqueBundleName + + " for reason " + ex.getMessage(), ex); urls = null; } return urls; } + /** + * @param uniqueBundleName the unique bundle name + * @return the properties file loaded + * @deprecated since 1.1, prefer use the {@link I18nInitializer} api + * instead. + */ + @Deprecated public static Properties loadUniqueNameDefFile(String uniqueBundleName) { - String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, uniqueBundleName); + String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, + uniqueBundleName); Properties p = new Properties(); try { - URL defURL = I18nBundleFactory.class.getResource(UNIQUE_BUNDLE_PATH + definitionFileName); + URL defURL = I18nBundleFactory.class.getResource( + UNIQUE_BUNDLE_PATH + definitionFileName); log.info("definition i18n file : " + defURL); InputStream stream = defURL.openStream(); p.load(stream); stream.close(); } catch (Exception ex) { - log.warn("could not load unique bundle " + uniqueBundleName + " for reason " + ex.getMessage(), ex); + log.warn("could not load unique bundle " + uniqueBundleName + + " for reason " + ex.getMessage(), ex); } return p; } - /** - * Recherche la liste des url de toutes les resources i18n, i.e les urls - * des fichiers de traduction. - * - * @param urls des urls de resources i18n deja calcule, à ajouter au resultat sans traitement particulier - * @return la liste des urls de bundle i18n - */ - public static URL[] getURLs(URL... urls) { - - try { - // on calcule toutes les urls utilisable dans le classloader donnee - List<URL> urlToSeek = new ArrayList<URL>(); - urlToSeek.addAll(Arrays.asList(urls)); - - // on va maintenant supprimer toutes les urls qui ne respectent pas - // le pattern i18n : il faut que la resource contienne un repertoire i18n - // ce simple test permet de restreindre la recherche des resources - // i18n qui est tres couteuse - int size = urlToSeek.size(); - for (Iterator<URL> it = urlToSeek.iterator(); it.hasNext();) { - URL url = it.next(); - if (!I18nUtil.containsDirectDirectory(url, DIRECTORY_SEARCH_BUNDLE_PATTERN)) { - if (log.isDebugEnabled()) { - log.debug("skip url with no " + DIRECTORY_SEARCH_BUNDLE_PATTERN + " directory : " + url); - } - it.remove(); - } - } - - if (log.isDebugEnabled()) { - log.debug("detect " + urlToSeek.size() + " i18n capable url (out of " + size + ")"); - } - - List<URL> listURLs = new java.util.ArrayList<URL>(); - - for (URL url : urlToSeek) { - // on recherche tous les fichiers de traduction pour cet url - - List<URL> result = null; - - if (log.isDebugEnabled()) { - log.debug("seek in : " + url); - } - - String fileName = url.getFile(); - // TODO deal with encoding in windows, this is very durty, but it - // works... - File file = new File(fileName.replaceAll("%20", " ")); - - if (I18nUtil.isJar(fileName)) { - // cas ou le ichier du classLoader est un fichier jar - if (log.isDebugEnabled()) { - log.debug("jar to search " + file); - } - result = getURLsFromJar(url, file); - - } else if (file.isDirectory()) { - // cas ou le ichier du classLoader est un repertoire - if (log.isDebugEnabled()) { - log.debug("directory to search " + file); - } - // on traite le cas ou il peut y avoir des repertoire dans ce - // repertoire - result = getURLsFromDirectory(url, file); - } - if (result != null && !result.isEmpty()) { - listURLs.addAll(result); - } - - } - return listURLs.toArray(new URL[listURLs.size()]); - } catch (Exception eee) { - log.warn("Unable to find urls for urls : " + urls + " for reason " + eee.getMessage(), eee); - return new URL[0]; - } - } - - /** - * Detecte les bundles i18n a partir des urls des fichiers de traduction - * donnes. - * - * Tous les entrées de bundles sont triees dans l'ordre des scopes i18n. - * - * @param urls les urls des fichiers de traductions - * @return la liste des bundle i18n construits à partir des fichiers de - * traduction donnes. - */ - public static List<I18nBundle> detectBundles(URL... urls) { - - List<String> bundleNames = new ArrayList<String>(); - List<I18nBundle> bundles = new ArrayList<I18nBundle>(); - - for (URL url : urls) { - - if (addBundleEntry(url, I18nBundleScope.FULL, bundleNames, bundles)) { - // found a full bundle - continue; - } - if (addBundleEntry(url, I18nBundleScope.LANGUAGE, bundleNames, bundles)) { - // found a language bundle - continue; - } - // must be a general bundle with no locale defined - addBundleEntry(url, I18nBundleScope.GENERAL, bundleNames, bundles); - } - bundleNames.clear(); - - // once for all, sort entries from general to full - for (I18nBundle bundle : bundles) { - java.util.Collections.sort(bundle.getEntries()); - } - - return bundles; - } - - protected static boolean addBundleEntry(URL url, I18nBundleScope scope, List<String> bundleNames, List<I18nBundle> bundles) { - String path = url.toString(); - Matcher matcher = scope.getMatcher(path); - if (!matcher.matches()) { - // no match at this scope - return false; - } - // create a new bundle entry - I18nBundleEntry entry = new I18nBundleEntry(url, scope.getLocale(matcher), scope); - if (log.isDebugEnabled()) { - log.debug("bundle (" + bundles.size() + ") : " + entry); - } - // get the associated bundle - I18nBundle bundle = addBundle(scope.getBundlePrefix(matcher), bundleNames, bundles); - // add entry to bundle - bundle.addEntry(entry); - return true; - } - - protected static I18nBundle addBundle(String bundleName, List<String> bundleNames, List<I18nBundle> bundles) { - I18nBundle bundle; - int index = bundleNames.indexOf(bundleName); - if (index > -1) { - bundle = bundles.get(index); - } else { - bundle = new I18nBundle(bundleName); - if (log.isDebugEnabled()) { - log.debug("bundle (" + bundles.size() + ") : " + bundle); - } - bundles.add(bundle); - bundleNames.add(bundleName); - } - return bundle; - } - - /** - * Obtain some rescue entries for a given locale. - * <p/> - * Note: <b>Calling this method implies there is no entry matched by the common method - * {@link #getBundleEntries(Locale, Locale, I18nBundle[])} return a empty array. - * - * @param bundle the bundle to promute - * @param l the locale required - * @param defaultLocale the default locale to used for promotion - * @param promuteGeneral a flag to authorize promotion to default locale - * @return the table of entries promuted for the given locale - */ - protected static I18nBundleEntry[] promuteBundle(I18nBundle bundle, Locale l, Locale defaultLocale, boolean promuteGeneral) { - - I18nBundleScope scope = I18nBundleScope.valueOf(l); - - if (log.isDebugEnabled()) { - log.debug('[' + bundle.getBundlePrefix() + "] did not find matching entries for locale " + l + ". Try to detect best entries..."); - } - - if (bundle.size() == 0) { - // there is no entry to take... - log.warn("PROMUTE NO ENTRY FOUND"); - return new I18nBundleEntry[0]; - } - - if (bundle.size() == 1) { - // there is one entry take it,what ever... - I18nBundleEntry entry = bundle.getEntries().get(0); - log.warn("PROMUTE" + l + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - return new I18nBundleEntry[]{entry}; - } - - List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); - - switch (scope) { - case FULL: - promuteFull(bundle, l, defaultLocale, result, promuteGeneral); - break; - case LANGUAGE: - promuteLanguage(bundle, l, defaultLocale, result, promuteGeneral); - break; - case GENERAL: - if (promuteGeneral) { - promuteGeneral(bundle, l, defaultLocale, result); - } - break; - } - return result.toArray(new I18nBundleEntry[result.size()]); - } - - protected static void promuteFull(I18nBundle bundle, Locale locale, Locale defaultLocale, List<I18nBundleEntry> result, boolean promuteGeneral) { - if (bundle.size() == 0) { - return; - } - // try with a another FULL matching locale ? - for (I18nBundleEntry entry : bundle.getEntries()) { - I18nBundleScope i18nBundleScope = entry.getScope(); - // load from general to the max scope and always if there is only one bundle entry found - if (i18nBundleScope == I18nBundleScope.FULL && - !entry.getLocale().getCountry().equals(locale.getCountry()) && - entry.getLocale().getLanguage().equals(locale.getLanguage())) { - log.warn(locale + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - result.add(entry); - // we take the first one, this is a resuce!!! - break; - } - } - if (result.isEmpty()) { - // full promotion failed,trylanguage promotion - promuteLanguage(bundle, locale, defaultLocale, result, promuteGeneral); - } - - } - - protected static void promuteLanguage(I18nBundle bundle, Locale locale, Locale defaultLocale, List<I18nBundleEntry> result, boolean promuteGeneral) { - if (bundle.size() == 0) { - return; - } - for (I18nBundleEntry entry : bundle.getEntries()) { - I18nBundleScope i18nBundleScope = entry.getScope(); - // load from general to the max scope and always if there is only one bundle entry found - if (i18nBundleScope == I18nBundleScope.FULL && entry.getLocale().getLanguage().equals(locale.getLanguage())) { - result.add(entry); - log.warn(locale + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - // we take the first one, this is a resuce!!! - break; - } - } - if (result.isEmpty() && promuteGeneral) { - // language promotion failed,try general promotion - promuteGeneral(bundle, locale, defaultLocale, result); - } - } - - protected static void promuteGeneral(I18nBundle bundle, Locale locale, Locale defaultLocale, List<I18nBundleEntry> result) { - if (bundle.size() == 0) { - return; - } - if (bundle.size() == 1) { - // there is one entry take it,what ever... - I18nBundleEntry entry = bundle.getEntries().get(0); - result.add(entry); - log.warn(locale + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - return; - } - I18nBundleScope scope = I18nBundleScope.valueOf(defaultLocale); - for (I18nBundleEntry entry : bundle.getEntries(scope)) { - if (entry.getLocale().equals(defaultLocale)) { - // default locale found - log.warn(locale + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - result.add(entry); - return; - } - } - - // default locale not found, take the first one ? - I18nBundleEntry entry = bundle.getEntries().get(0); - result.add(entry); - log.warn(locale + " to " + entry.getLocale() + " [" + bundle.getBundlePrefix() + ']'); - //TODO Should try to load default en_GB from I18nLoader ? - //I18n.DEFAULT_LOCALE.getCountry() - } - - protected static List<URL> getURLsFromJar(URL incomingURL, File jarfile) { - - String pattern = SEARCH_BUNDLE_PATTERN; - try { - - List<URL> result = new ArrayList<URL>(); - InputStream in = new FileInputStream(jarfile); - ZipInputStream zis = new ZipInputStream(in); - ClassLoader cl = new URLClassLoader(new URL[]{incomingURL}, I18nBundleFactory.class.getClassLoader()); - while (zis.available() != 0) { - ZipEntry entry = zis.getNextEntry(); - - if (entry == null) { - break; - } - - String name = entry.getName(); - - if (pattern == null || name.matches(pattern)) { - // on recupere le fichier correspondant au pattern dans le - // classloader - if (log.isDebugEnabled()) { - log.debug(name + " accepted for pattern " + pattern); - } - URL url = cl.getResource(name); - // on ajoute le fichier correspondant au pattern dans la - // liste - result.add(url); - } - } - - return result; - } catch (Exception eee) { - throw new RuntimeException("n'a pas pu trouve la resource dans le jar " + jarfile.getAbsolutePath(), eee); - } - } - - protected static List<URL> getURLsFromDirectory(URL incomingURL, File repository) { - String pattern = SEARCH_BUNDLE_PATTERN; - try { - if (log.isDebugEnabled()) { - log.debug("search '" + pattern + "' in " + repository); - } - - List<URL> urlList = new ArrayList<URL>(); - File[] filesList = repository.listFiles(); - - if (filesList != null) { - - for (File file : filesList) { - - String name = file.getAbsolutePath(); - - // cas de recursivite : repertoire dans un repertoire - if (file.exists() && file.isDirectory()) { - urlList.addAll(I18nUtil.getURLsFromDirectory(file, - pattern)); - // si le fichier du repertoire n'est pas un repertoire - // on verifie s'il correspond au pattern - } else if (pattern == null || name.matches(pattern)) { - URL url = file.toURI().toURL(); - if (log.isDebugEnabled()) { - log.debug("directory: " + repository + " url: " + url); - } - urlList.add(url); - } - } - } - return urlList; - } catch (MalformedURLException eee) { - throw new RuntimeException("n'a pas pu trouve la resource dans le repertoire " + repository.getAbsolutePath(), eee); - } - } } Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleScope.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleScope.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleScope.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,5 +1,5 @@ /* -* *##% + * *##% * I18n :: Api * Copyright (C) 2004 - 2009 CodeLutin * @@ -16,45 +16,49 @@ * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* */ + * ##%* + */ package org.nuiton.i18n.bundle; +import org.nuiton.i18n.I18nUtil; + import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.nuiton.i18n.I18nUtil; /** * The enumaration defines the scope of a bundle entry. * <p/> - * There is three scope possible: - * <ul> - * <li>{@link #GENERAL} : for a bundle entry with no locale specialized information, eg : <code>bundle.properties</code></li> - * <li>{@link #LANGUAGE} : for a bundle entry with language locale specialized information, eg : <code>bundle-en.properties</code></li> - * <li>{@link #FULL} : for a bundle entry with full locale specialized information, eg : <code>bundle-en_GB.properties</code></li> - * </ul> + * There is three scope possible: <ul> <li>{@link #GENERAL} : for a bundle entry + * with no locale specialized information, eg : <code>bundle.properties</code></li> + * <li>{@link #LANGUAGE} : for a bundle entry with language locale specialized + * information, eg : <code>bundle-en.properties</code></li> <li>{@link #FULL} : + * for a bundle entry with full locale specialized information, eg : + * <code>bundle-en_GB.properties</code></li> </ul> * <p/> * We define a order relation, from general to full scope : * <p/> * {@link #GENERAL} < {@link #LANGUAGE} < {@link #FULL} * <p/> - * Scopes are inclusives, in a search of entries, eg the search of <code>en_GB</code> will include <code>en</code> scope... + * Scopes are inclusives, in a search of entries, eg the search of + * <code>en_GB</code> will include <code>en</code> scope... * <p/> * The {@link #patternAll} is the searching pattern of bundle of the scope. * <p/> - * The method {@link #getMatcher(String)} obtain from the {@link #patternAll} the matcher for a bundle path. + * The method {@link #getMatcher(String)} obtain from the {@link #patternAll} + * the matcher for a bundle path. * <p/> - * The method {@link #getLocale(Matcher)} obtain from the {@link #patternAll} matched in a bundle path, the - * corresponding locale. + * The method {@link #getLocale(Matcher)} obtain from the {@link #patternAll} + * matched in a bundle path, the corresponding locale. * <p/> - * The class offer also a static method {@link #valueOf(java.util.Locale)} to obtain the scope of a locale. + * The class offer also a static method {@link #valueOf(Locale)} to obtain the + * scope of a locale. * - * @author chemit + * @author tchemit <chemit@codelutin.com> */ public enum I18nBundleScope { /** default scope (with no language, nor country information) */ -// GENERAL("(.*18n/.+)\\.properties") { GENERAL("(.*/.+)\\.properties") { @Override public Locale getLocale(Matcher matcher) { @@ -64,7 +68,6 @@ }, /** language scope (no country information) */ -// LANGUAGE("(.*18n/.+)-(\\w\\w)\\.properties") { LANGUAGE("(.*/.+)-(\\w\\w)\\.properties") { @Override public Locale getLocale(Matcher matcher) { @@ -77,7 +80,6 @@ }, /** full scope : language + country */ -// FULL("(.*18n/.+)-(\\w\\w_\\w\\w)\\.properties") { FULL("(.*/.+)-(\\w\\w_\\w\\w)\\.properties") { @Override public Locale getLocale(Matcher matcher) { @@ -95,13 +97,15 @@ /** * Obtain the scope of a given <code>locale</code>. * <p/> - * The given locale can be null, which means {@link I18nBundleScope#GENERAL} scope. + * The given locale can be null, which means {@link I18nBundleScope#GENERAL} + * scope. * * @param locale given locale to convert * @return the scope of given locale */ public static I18nBundleScope valueOf(Locale locale) { - if (locale == null || locale.getLanguage() == null || locale.getLanguage().length() == 0) { + if (locale == null || locale.getLanguage() == null || + locale.getLanguage().length() == 0) { return GENERAL; } if (locale.getCountry() == null || locale.getCountry().length() == 0) { @@ -140,7 +144,7 @@ return result; } - private I18nBundleScope(String patternAll) { + I18nBundleScope(String patternAll) { this.patternAll = Pattern.compile(patternAll); } } Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleUtil.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleUtil.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleUtil.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,603 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.bundle; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.I18nUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.regex.Matcher; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Usefull methods on bundles. + * <p/> + * <b>Note:</b> Replace the previous class {@code org.nuiton.i18n.bundle.I18nBundleFactory}. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class I18nBundleUtil { + + /** Logger */ + private static final Log log = LogFactory.getLog(I18nBundleUtil.class); + + public static final String DIRECTORY_SEARCH_BUNDLE_PATTERN = "i18n"; + + public static final String SEARCH_BUNDLE_PATTERN = ".*i18n/.+\\.properties"; + + private static final I18nBundleEntry[] EMPTY_I18N_BUNDLE_ENTRYS_ARRAY = + new I18nBundleEntry[0]; + + public static final URL[] EMPTY_URL_ARRAY = new URL[0]; + + /** + * Récuperation de toutes les locales connus par un ensemble de bundles. + * + * @param bundles les bundles a parcourir + * @return la liste des locales rencontrées + */ + public static Locale[] getLocales(I18nBundle... bundles) { + Set<Locale> result = new HashSet<Locale>(); + for (I18nBundle i18nBundle : bundles) { + for (I18nBundleEntry entry : i18nBundle.getEntries()) { + Locale o = entry.getLocale(); + if (o != null) { + result.add(o); + } + } + } + return result.toArray(new Locale[result.size()]); + } + + /** + * Récuperation des noms de bundle par un ensemble de bundles. + * + * @param bundles les bundles a parcourir + * @return la liste des noms de bundle rencontrées + */ + public static String[] getBundleNames(I18nBundle... bundles) { + List<String> result = new ArrayList<String>(); + for (I18nBundle i18nBundle : bundles) { + result.add(i18nBundle.getBundlePrefix()); + } + return result.toArray(new String[result.size()]); + } + + /** + * Filtrage des bundles qui correspondante à la locale donnée. + * + * @param l la locale à filtrer + * @param bundles les bundles a parcourir + * @return les bundles qui correspondent à la locale donnée. + */ + public static I18nBundle[] getBundles(Locale l, I18nBundle... bundles) { + List<I18nBundle> result = new ArrayList<I18nBundle>(); + for (I18nBundle i18nBundle : bundles) { + if (i18nBundle.matchLocale(l)) { + result.add(i18nBundle); + } + } + return result.toArray(new I18nBundle[result.size()]); + } + + /** + * Récupération de toutes les entrées de bundles pour les bundles données. + * + * @param bundles les bundles a parcourir + * @return toutes les entrées de bundles. + */ + public static I18nBundleEntry[] getBundleEntries(I18nBundle... bundles) { + List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); + for (I18nBundle i18nBundle : bundles) { + List<I18nBundleEntry> list = i18nBundle.getEntries(); + if (!list.isEmpty()) { + result.addAll(list); + } + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + /** + * Filtrage des entrées de bundles pour une locale donnée. + * <p/> + * On essaye de trouver les meilleurs entrées possibles (possibilité de + * promotion). + * <p/> + * Note: Cette méthode doit être utilisé pour trouver toutes les entrées à + * charger par le système i18n pour une locale donnée. + * <p/> + * Note: Par defaut, on n'effectue pas les promotions générales ({@link + * #getBundleEntries(Locale, Locale, boolean, I18nBundle...)} + * + * @param l la locale à filtrer + * @param defaultLocale la locale à utiliser pour les promotions + * @param bundles les bundles a parcourir + * @return les entrées de bundles filtrés. + */ + public static I18nBundleEntry[] getBundleEntries(Locale l, + Locale defaultLocale, + I18nBundle... bundles) { + return getBundleEntries(l, defaultLocale, false, bundles); + } + + /** + * Filtrage des entrées de bundles pour une locale donnée. + * <p/> + * On essaye de trouver les meilleurs entrées possibles (possibilité de + * promotion). + * <p/> + * Note: Cette méthode doit être utilisé pour trouver toutes les entrées à + * charger par le système i18n pour une locale donnée. + * + * @param l la locale à filtrer + * @param defaultLocale la locale à utiliser pour les promotions + * @param promuteGeneral un drapeau pour indiquer si l'on autorise le + * chargement de la locale par defaut si pour un + * bundle donne on a pas trouve de traductions pour la + * locale donnee. + * @param bundles les bundles a parcourir + * @return les entrées de bundles filtrés. + */ + public static I18nBundleEntry[] getBundleEntries(Locale l, + Locale defaultLocale, + boolean promuteGeneral, + I18nBundle... bundles) { + + List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); + for (I18nBundle i18nBundle : bundles) { + I18nBundleEntry[] entries = i18nBundle.getEntries(l); + if (entries.length == 0) { + //no entry found for the bundle, try pomotion + entries = promuteBundle(i18nBundle, l, defaultLocale, + promuteGeneral); + } + result.addAll(Arrays.asList(entries)); + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + /** + * Recherche la liste des url de toutes les resources i18n, i.e les urls des + * fichiers de traduction. + * + * @param urls les urls à inspecter pour trouver des resources i18n + * @return la liste des urls de bundle i18n + */ + public static URL[] getURLs(URL... urls) { + + try { + // on calcule toutes les urls utilisable dans le classloader donnee + List<URL> urlToSeek = new ArrayList<URL>(); + urlToSeek.addAll(Arrays.asList(urls)); + + // on va maintenant supprimer toutes les urls qui ne respectent pas + // le pattern i18n : il faut que la resource contienne un + // repertoire i18n, ce simple test permet de restreindre la + // recherche des resources + // i18n qui est tres couteuse + int size = urlToSeek.size(); + for (Iterator<URL> it = urlToSeek.iterator(); it.hasNext();) { + URL url = it.next(); + if (!I18nUtil.containsDirectDirectory( + url, DIRECTORY_SEARCH_BUNDLE_PATTERN)) { + if (log.isDebugEnabled()) { + log.debug("skip url with no " + + DIRECTORY_SEARCH_BUNDLE_PATTERN + + " directory : " + url); + } + it.remove(); + } + } + + if (log.isDebugEnabled()) { + log.debug("detect " + urlToSeek.size() + + " i18n capable url (out of " + size + ")"); + } + + List<URL> listURLs = new ArrayList<URL>(); + + for (URL url : urlToSeek) { + // on recherche tous les fichiers de traduction pour cet url + + List<URL> result = null; + + if (log.isDebugEnabled()) { + log.debug("seek in : " + url); + } + + String fileName = url.getFile(); + // TODO deal with encoding in windows, this is very durty, + // TODO but it works... + File file = new File(fileName.replaceAll("%20", " ")); + + if (I18nUtil.isJar(fileName)) { + // cas ou le ichier du classLoader est un fichier jar + if (log.isDebugEnabled()) { + log.debug("jar to search " + file); + } + result = getURLsFromJar(url, file); + + } else if (file.isDirectory()) { + // cas ou le ichier du classLoader est un repertoire + if (log.isDebugEnabled()) { + log.debug("directory to search " + file); + } + // on traite le cas ou il peut y avoir des repertoire + // dans ce repertoire + result = getURLsFromDirectory(url, file); + } + if (result != null && !result.isEmpty()) { + listURLs.addAll(result); + } + + } + return listURLs.toArray(new URL[listURLs.size()]); + } catch (Exception eee) { + log.warn("Unable to find urls for urls : " + Arrays.toString(urls) + + " for reason " + eee.getMessage(), eee); + return EMPTY_URL_ARRAY; + } + } + + /** + * Teste si un ensemble de bundles contient au moins une entrée. + * + * @param bundles les bundles a parcourir + * @return <code>true</code> si aucune entree trouvee, <code>false</code> + * autrement. + */ + public static boolean isEmpty(I18nBundle... bundles) { + for (I18nBundle i18nBundle : bundles) { + if (!i18nBundle.getEntries().isEmpty()) { + // on a trouve au moins une entree + return false; + } + } + return true; + } + + /** + * Detecte les bundles i18n a partir des urls des fichiers de traduction + * donnes. + * <p/> + * Tous les entrées de bundles sont triees dans l'ordre des scopes i18n. + * + * @param urls les urls des fichiers de traductions + * @return la liste des bundle i18n construits à partir des fichiers de + * traduction donnes. + */ + public static List<I18nBundle> detectBundles(URL... urls) { + + List<String> bundleNames = new ArrayList<String>(); + List<I18nBundle> bundles = new ArrayList<I18nBundle>(); + + for (URL url : urls) { + + if (addBundleEntry(url, I18nBundleScope.FULL, bundleNames, + bundles)) { + // found a full bundle + continue; + } + if (addBundleEntry(url, I18nBundleScope.LANGUAGE, bundleNames, + bundles)) { + // found a language bundle + continue; + } + // must be a general bundle with no locale defined + addBundleEntry(url, I18nBundleScope.GENERAL, bundleNames, bundles); + } + bundleNames.clear(); + + // once for all, sort entries from general to full + for (I18nBundle bundle : bundles) { + Collections.sort(bundle.getEntries()); + } + + return bundles; + } + + protected static boolean addBundleEntry(URL url, + I18nBundleScope scope, + List<String> bundleNames, + List<I18nBundle> bundles) { + String path = url.toString(); + Matcher matcher = scope.getMatcher(path); + if (!matcher.matches()) { + // no match at this scope + return false; + } + // create a new bundle entry + I18nBundleEntry entry = + new I18nBundleEntry(url, scope.getLocale(matcher), scope); + if (log.isDebugEnabled()) { + log.debug("bundle (" + bundles.size() + ") : " + entry); + } + // get the associated bundle + I18nBundle bundle = + addBundle(scope.getBundlePrefix(matcher), bundleNames, bundles); + // add entry to bundle + bundle.addEntry(entry); + return true; + } + + protected static I18nBundle addBundle(String bundleName, + List<String> bundleNames, + List<I18nBundle> bundles) { + I18nBundle bundle; + int index = bundleNames.indexOf(bundleName); + if (index > -1) { + bundle = bundles.get(index); + } else { + bundle = new I18nBundle(bundleName); + if (log.isDebugEnabled()) { + log.debug("bundle (" + bundles.size() + ") : " + bundle); + } + bundles.add(bundle); + bundleNames.add(bundleName); + } + return bundle; + } + + /** + * Obtain some rescue entries for a given locale. + * <p/> + * Note: <b>Calling this method implies there is no entry matched by the + * common method {@link #getBundleEntries(Locale, Locale, I18nBundle...)} + * returns a empty array.</b> + * + * @param bundle the bundle to promute + * @param l the locale required + * @param defaultLocale the default locale to used for promotion + * @param promuteGeneral a flag to authorize promotion to default locale + * @return the table of entries promuted for the given locale + */ + protected static I18nBundleEntry[] promuteBundle(I18nBundle bundle, + Locale l, + Locale defaultLocale, + boolean promuteGeneral) { + + I18nBundleScope scope = I18nBundleScope.valueOf(l); + + if (log.isDebugEnabled()) { + log.debug('[' + bundle.getBundlePrefix() + "] did not find" + + " matching entries for locale " + l + + ". Try to detect best entries..."); + } + + if (bundle.size() == 0) { + // there is no entry to take... + log.warn("PROMUTE NO ENTRY FOUND"); + return EMPTY_I18N_BUNDLE_ENTRYS_ARRAY; + } + + if (bundle.size() == 1) { + // there is one entry take it,what ever... + I18nBundleEntry entry = bundle.getEntries().get(0); + log.warn("PROMUTE" + l + " to " + entry.getLocale() + + " [" + bundle.getBundlePrefix() + ']'); + return new I18nBundleEntry[]{entry}; + } + + List<I18nBundleEntry> result = new ArrayList<I18nBundleEntry>(); + + switch (scope) { + case FULL: + promuteFull(bundle, l, defaultLocale, result, promuteGeneral); + break; + case LANGUAGE: + promuteLanguage(bundle, l, defaultLocale, result, + promuteGeneral); + break; + case GENERAL: + if (promuteGeneral) { + promuteGeneral(bundle, l, defaultLocale, result); + } + break; + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + protected static void promuteFull(I18nBundle bundle, + Locale locale, + Locale defaultLocale, + List<I18nBundleEntry> result, + boolean promuteGeneral) { + if (bundle.size() == 0) { + return; + } + // try with a another FULL matching locale ? + for (I18nBundleEntry entry : bundle.getEntries()) { + I18nBundleScope i18nBundleScope = entry.getScope(); + // load from general to the max scope and always if there is only + // one bundle entry found + Locale locale1 = entry.getLocale(); + if (i18nBundleScope == I18nBundleScope.FULL && + !locale1.getCountry().equals(locale.getCountry()) && + locale1.getLanguage().equals(locale.getLanguage())) { + log.warn(locale + " to " + locale1 + + " [" + bundle.getBundlePrefix() + ']'); + result.add(entry); + // we take the first one, this is a resuce!!! + break; + } + } + if (result.isEmpty()) { + // full promotion failed,trylanguage promotion + promuteLanguage(bundle, locale, defaultLocale, result, + promuteGeneral); + } + + } + + protected static void promuteLanguage(I18nBundle bundle, + Locale locale, + Locale defaultLocale, + List<I18nBundleEntry> result, + boolean promuteGeneral) { + if (bundle.size() == 0) { + return; + } + for (I18nBundleEntry entry : bundle.getEntries()) { + I18nBundleScope i18nBundleScope = entry.getScope(); + // load from general to the max scope and always if there is only + // one bundle entry found + Locale locale1 = entry.getLocale(); + if (i18nBundleScope == I18nBundleScope.FULL && + locale1.getLanguage().equals(locale.getLanguage())) { + result.add(entry); + log.warn(locale + " to " + locale1 + " [" + + bundle.getBundlePrefix() + ']'); + // we take the first one, this is a resuce!!! + break; + } + } + if (result.isEmpty() && promuteGeneral) { + // language promotion failed,try general promotion + promuteGeneral(bundle, locale, defaultLocale, result); + } + } + + protected static void promuteGeneral(I18nBundle bundle, + Locale locale, + Locale defaultLocale, + List<I18nBundleEntry> result) { + if (bundle.size() == 0) { + return; + } + if (bundle.size() == 1) { + // there is one entry take it,what ever... + I18nBundleEntry entry = bundle.getEntries().get(0); + result.add(entry); + log.warn(locale + " to " + entry.getLocale() + " [" + + bundle.getBundlePrefix() + ']'); + return; + } + I18nBundleScope scope = I18nBundleScope.valueOf(defaultLocale); + for (I18nBundleEntry entry : bundle.getEntries(scope)) { + if (entry.getLocale().equals(defaultLocale)) { + // default locale found + log.warn(locale + " to " + entry.getLocale() + " [" + + bundle.getBundlePrefix() + ']'); + result.add(entry); + return; + } + } + + // default locale not found, take the first one ? + I18nBundleEntry entry = bundle.getEntries().get(0); + result.add(entry); + log.warn(locale + " to " + entry.getLocale() + " [" + + bundle.getBundlePrefix() + ']'); + //TODO Should try to load default en_GB from I18nLoader ? + //I18n.DEFAULT_LOCALE.getCountry() + } + + protected static List<URL> getURLsFromJar(URL incomingURL, File jarfile) { + + String pattern = SEARCH_BUNDLE_PATTERN; + try { + + List<URL> result = new ArrayList<URL>(); + InputStream in = new FileInputStream(jarfile); + ZipInputStream zis = new ZipInputStream(in); + ClassLoader cl = new URLClassLoader( + new URL[]{incomingURL}, + I18nBundleUtil.class.getClassLoader()); + while (zis.available() != 0) { + ZipEntry entry = zis.getNextEntry(); + + if (entry == null) { + break; + } + + String name = entry.getName(); + + if (name.matches(pattern)) { + // on recupere le fichier correspondant au pattern dans le + // classloader + if (log.isDebugEnabled()) { + log.debug(name + " accepted for pattern " + pattern); + } + URL url = cl.getResource(name); + // on ajoute le fichier correspondant au pattern dans la + // liste + result.add(url); + } + } + + return result; + } catch (Exception eee) { + throw new RuntimeException( + "n'a pas pu trouve la resource dans le jar " + + jarfile.getAbsolutePath(), eee); + } + } + + protected static List<URL> getURLsFromDirectory(URL incomingURL, + File repository) { + String pattern = SEARCH_BUNDLE_PATTERN; + try { + if (log.isDebugEnabled()) { + log.debug("search '" + pattern + "' in " + repository); + } + + List<URL> urlList = new ArrayList<URL>(); + File[] filesList = repository.listFiles(); + + if (filesList != null) { + + for (File file : filesList) { + + String name = file.getAbsolutePath(); + + // cas de recursivite : repertoire dans un repertoire + if (file.exists() && file.isDirectory()) { + urlList.addAll(I18nUtil.getURLsFromDirectory(file, + pattern)); + // si le fichier du repertoire n'est pas un repertoire + // on verifie s'il correspond au pattern + } else if (name.matches(pattern)) { + URL url = file.toURI().toURL(); + if (log.isDebugEnabled()) { + log.debug("directory: " + repository + " url: " + + url); + } + urlList.add(url); + } + } + } + return urlList; + } catch (MalformedURLException eee) { + throw new RuntimeException( + "n'a pas pu trouve la resource dans le repertoire " + + repository.getAbsolutePath(), eee); + } + } +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/bundle/I18nBundleUtil.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ClassPathI18nInitializer.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ClassPathI18nInitializer.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ClassPathI18nInitializer.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,132 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.init; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.I18nUtil; +import org.nuiton.i18n.bundle.I18nBundle; +import org.nuiton.i18n.bundle.I18nBundleUtil; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + + +/** + * Implementation of a {@link I18nInitializer} using all i18n resources (from + * artifacts) discovered in classpath. + * <p/> + * Will scan all classpath. + * <p/> + * <b>Note:</b> No order can be predicted with this implementation on bundles. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class ClassPathI18nInitializer extends I18nInitializer { + + /** Logger */ + private static final Log log = + LogFactory.getLog(DefaultI18nInitializer.class); + + /** class loader to use (optional) */ + protected ClassLoader loader; + + protected URL[] extraURLs; + + public ClassPathI18nInitializer() { + this(null, null); + } + + public ClassPathI18nInitializer(ClassLoader loader) { + this(loader, null); + } + + public ClassPathI18nInitializer(ClassLoader loader, URL[] extraURLs) { + this.loader = loader == null ? getClass().getClassLoader() : loader; + this.extraURLs = extraURLs; + } + + public URL[] resolvURLs() throws Exception { + // on calcule toutes les urls utilisable dans le classloader donnee + List<URL> urlToSeek = new ArrayList<URL>(); + URLClassLoader loader = (URLClassLoader) getLoader(); + urlToSeek.addAll(Arrays.asList(I18nUtil.getDeepURLs(loader))); + + // on va maintenant supprimer toutes les urls qui ne respectent pas + // le pattern i18n : il faut que la resource contienne un repertoire i18n + // ce simple test permet de restreindre la recherche des resources + // i18n qui est tres couteuse + int size = urlToSeek.size(); + for (Iterator<URL> it = urlToSeek.iterator(); it.hasNext();) { + URL url = it.next(); + if (!I18nUtil.containsDirectDirectory( + url, I18nBundleUtil.DIRECTORY_SEARCH_BUNDLE_PATTERN)) { + if (log.isDebugEnabled()) { + log.debug("skip url with no " + + I18nBundleUtil.DIRECTORY_SEARCH_BUNDLE_PATTERN + + " directory : " + url); + } + it.remove(); + } + } + if (log.isDebugEnabled()) { + log.debug("detect " + urlToSeek.size() + + " i18n capable url (out of " + size + ")"); + } + // on effectue la recherche des urls des resources i18n (tous les + // fichiers de traductions) sur toutes les urls precedemment calculees) + URL[] url1 = urlToSeek.toArray(new URL[urlToSeek.size()]); + URL[] result = I18nBundleUtil.getURLs(url1); + if (log.isDebugEnabled()) { + for (URL url : result) { + log.debug(url.toString()); + } + } + return result; + + } + + @Override + public I18nBundle[] resolvBundles() throws Exception { + // detect bundles urls + + URL[] urls = resolvURLs(); + + // detect bundles + + I18nBundle[] result = resolvBundles(urls); + return result; + } + + public URL[] getExtraURLs() { + return extraURLs; + } + + public ClassLoader getLoader() { + return loader; + } + +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/ClassPathI18nInitializer.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/DefaultI18nInitializer.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/DefaultI18nInitializer.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/DefaultI18nInitializer.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,215 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.init; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.I18nUtil; +import org.nuiton.i18n.bundle.I18nBundle; + +import java.io.InputStream; +import java.net.URL; +import java.util.*; + +/** + * Default implementation of a {@link I18nInitializer} using the default i18n + * implementation (one unique bundle with a definition properties file). + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class DefaultI18nInitializer extends I18nInitializer { + + /** Logger */ + private static final Log log = + LogFactory.getLog(DefaultI18nInitializer.class); + + public static String UNIQUE_BUNDLE_DEF = "%1$s-definition.properties"; + + public static String UNIQUE_BUNDLE_ENTRY = "%1$s-%2$s.properties"; + + public static String BUNDLE_DEF_LOCALES = "locales"; + + public static String BUNDLE_DEF_VERSION = "version"; + + public static String BUNDLES_FOR_LOCALE = "bundles."; + + /** the name of the bundle */ + protected final String bundleName; + + /** class loader to use (optional) */ + protected ClassLoader loader; + + /** i18n path where to seek for resources (optional) */ + protected String i18nPath; + + protected Properties definition; + + protected URL definitionURL; + + public static final String DEFAULT_I18N_PATH = "META-INF/"; + + public DefaultI18nInitializer(String bundleName) throws + NullPointerException { + this(bundleName, null, null); + } + + public DefaultI18nInitializer(String bundleName, + ClassLoader loader) throws + NullPointerException { + this(bundleName, loader, null); + } + + public DefaultI18nInitializer(String bundleName, + ClassLoader loader, + String i18nPath) throws NullPointerException { + if (bundleName == null) { + throw new NullPointerException( + "parameter 'bundleName' can not be null"); + } + this.bundleName = bundleName; + this.loader = loader == null ? getClass().getClassLoader() : loader; + this.i18nPath = i18nPath == null ? DEFAULT_I18N_PATH : i18nPath; + } + + public String getBundleName() { + return bundleName; + } + + public ClassLoader getLoader() { + return loader; + } + + public String getI18nPath() { + return i18nPath; + } + + protected void setLoader(ClassLoader loader) { + this.loader = loader; + } + + protected void setI18nPath(String i18nPath) { + this.i18nPath = i18nPath; + } + + protected URL getResourceURL(String resource) { + if (log.isInfoEnabled()) { + log.info("resource to seek : " + resource); + } + URL url = getLoader().getResource(resource); + return url; + } + + public String resolvDefinition(Properties p) throws Exception { + + String filename = String.format(UNIQUE_BUNDLE_DEF, getBundleName()); + + URL url = getDefinitionURL(); + + if (log.isInfoEnabled()) { + log.info("definition file to seek : " + url); + } + + // load definition file + InputStream stream = url.openStream(); + try { + p.load(stream); + stream.close(); + } finally { + stream.close(); + } + + String prefix = url.toString(); + prefix = prefix.substring(0, prefix.length() - filename.length()); + + return prefix; + } + + public URL getDefinitionURL() throws NullPointerException { + if (definitionURL == null) { + String filename = String.format(UNIQUE_BUNDLE_DEF, getBundleName()); + + String path = getI18nPath() + filename; + + definitionURL = getResourceURL(path); + + if (definitionURL == null) { + throw new NullPointerException( + "could not find bundle definition file at " + path); + } + } + return definitionURL; + } + +// @Override + + public URL[] resolvURLs(String prefixURL, Properties definition) throws Exception { + + // get locales from properties + + String localesAsStr = definition.getProperty(BUNDLE_DEF_LOCALES); + + Locale[] locales = I18nUtil.parseLocales(localesAsStr); + + if (log.isDebugEnabled()) { + log.debug("Detected locales : " + Arrays.toString(locales)); + } + + List<URL> lUrls = new ArrayList<URL>(locales.length); + + // for each locale found in definition file, add resource url + + String bundleName = getBundleName(); + + for (Locale l : locales) { + String url = prefixURL + + String.format(UNIQUE_BUNDLE_ENTRY, bundleName, l); + if (log.isInfoEnabled()) { + log.info("Detected resource for locale " + l + " : " + url); + } + URL u = new URL(url); + //FIXME on devrait tester que la resource est disponible ? + + lUrls.add(u); + } + + URL[] urls = lUrls.toArray(new URL[lUrls.size()]); + return urls; + } + + @Override + public I18nBundle[] resolvBundles() throws Exception { + + Properties definition = new Properties(); + + String prefixURL = resolvDefinition(definition); + + // detect bundles urls + + URL[] urls = resolvURLs(prefixURL, definition); + + // detect bundles + + I18nBundle[] result = resolvBundles(urls); + return result; + } + +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/DefaultI18nInitializer.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/I18nInitializer.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/I18nInitializer.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/I18nInitializer.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,69 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.init; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.bundle.I18nBundle; +import org.nuiton.i18n.bundle.I18nBundleEntry; +import org.nuiton.i18n.bundle.I18nBundleUtil; + +import java.net.URL; +import java.util.List; + +/** + * Contract of a resolver of {@link I18nBundle}. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public abstract class I18nInitializer { + + /** Logger */ + private static final Log log = LogFactory.getLog(I18nInitializer.class); + + /** + * Resolv the bundles. + * + * @return the bundles detected + * @throws Exception if any pb while getting bundles + */ + public abstract I18nBundle[] resolvBundles() throws Exception; + + public I18nBundle[] resolvBundles(URL... urls) throws Exception { + + // detect bundles + + List<I18nBundle> bundles = I18nBundleUtil.detectBundles(urls); + I18nBundle[] result = bundles.toArray(new I18nBundle[bundles.size()]); + + if (log.isInfoEnabled()) { + + I18nBundleEntry[] entries = I18nBundleUtil.getBundleEntries(result); + + log.info(bundles.size() + " bundle(s) found, in " + + entries.length + " file(s)."); + } + + return result; + } + +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/I18nInitializer.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/UserI18nInitializer.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/UserI18nInitializer.java (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/UserI18nInitializer.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,210 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.init; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.i18n.bundle.I18nBundle; +import org.nuiton.i18n.bundle.I18nBundleEntry; +import org.nuiton.i18n.bundle.I18nBundleUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Properties; + +/** + * Initializer which expose i18n resources in the {@link #userDirectory}, then + * use resources from this user directories. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class UserI18nInitializer extends DefaultI18nInitializer { + + /** Logger */ + private static final Log log = LogFactory.getLog(UserI18nInitializer.class); + + /** the user directory where to store the i18n resources. */ + protected final File userDirectory; + + /** + * the delegate initializer to obtain default i18n resources to put in user + * directory. + */ + protected final DefaultI18nInitializer delegate; + + public UserI18nInitializer(File userDirectory, + DefaultI18nInitializer delegate) + throws NullPointerException { + this(null, userDirectory, delegate); + } + + public UserI18nInitializer(String i18nPath, + File userDirectory, + DefaultI18nInitializer delegate) + throws NullPointerException { + + super(delegate == null ? null : delegate.getBundleName(), + null, + i18nPath == null ? "" : i18nPath + ); + + if (userDirectory == null) { + throw new NullPointerException( + "parameter 'userDirectory' can not be null"); + } + if (delegate == null) { + throw new NullPointerException( + "parameter 'delegate' can not be null"); + } + this.userDirectory = userDirectory; + this.delegate = delegate; + } + + public File getUserDirectory() { + return userDirectory; + } + + public DefaultI18nInitializer getDelegate() { + return delegate; + } + + @Override + public I18nBundle[] resolvBundles() throws Exception { + + File directory = getUserDirectory(); + + boolean isNew = !directory.exists(); + + if (isNew) { + + // creates the user directory and fill it with i18n resources + // coming from default initializer + createUserI18nLayout(directory); + } + + // use a new classloader directly on the directory + setLoader(new URLClassLoader(new URL[]{directory.toURI().toURL()})); + + I18nBundle[] bundles = super.resolvBundles(); + + if (!isNew) { + updateUserI18nLayout(directory, bundles); + } + return bundles; + } + + /** + * Creates the user i18n structure. + * <p/> + * will use the default initializer to obtain i18n resources from default + * system, then copy them to the user directory. + * + * @param directory the directory where to export i18n resources + * @throws Exception if any pb + */ + protected void createUserI18nLayout(File directory) throws Exception { + + // user i18n directory does not exists + // create it and fill it + + boolean b = directory.mkdirs(); + if (!b) { + throw new IOException("could not create directory " + directory); + } + + Properties definition = new Properties(); + + String prefixURL = getDelegate().resolvDefinition(definition); + + String filename = String.format(UNIQUE_BUNDLE_DEF, getBundleName()); + + File f = new File(directory, filename); + + FileOutputStream outStream = new FileOutputStream(f); + try { + + definition.store(outStream, "Generated by " + getClass().getName()); + } finally { + + outStream.close(); + } + + // detect bundles urls + + URL[] urls = resolvURLs(prefixURL, definition); + + // detect bundles + + I18nBundle[] bundles = resolvBundles(urls); + + // detect bundles entries + + I18nBundleEntry[] entries = + I18nBundleUtil.getBundleEntries(bundles); + + // copy all bundle entries resource + + for (I18nBundleEntry e : entries) { + + URL url = e.getPath(); + + if (log.isInfoEnabled()) { + log.info("I18n file to load : " + url); + } + + String path = url.getPath(); + + String name = path.substring(path.lastIndexOf("/")); + + File dst = new File(directory, name); + + if (log.isInfoEnabled()) { + log.info("Create user file to create : " + dst); + } + + // recopie du fichier + + outStream = new FileOutputStream(dst); + try { + IOUtils.copy(url.openStream(), outStream); + } finally { + outStream.close(); + } + } + } + + /** + * Hook to update the user i18n structure. + * <p/> + * If you wants to do something specific, overrides this method. + * + * @param directory the user directory where are i18n resources + * @param bundles the user i18n bundles + */ + public void updateUserI18nLayout(File directory, I18nBundle[] bundles) { + // by default nothing to do, change this if you wants something + } +} Property changes on: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/init/UserI18nInitializer.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Added: trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/package.html =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/package.html (rev 0) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/i18n/package.html 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,12 @@ +<html> +<body> +<h1>Nuiton i18n </h1> +Ensemble de classes Java permettant de charger le système i18n basée +sur la librairie nuiton-i18n-api. + +Pour initialiser le système utiliser les deux méthodes suivantes : + +<pre>org.nuiton.i18n.I18n.setBundleResolver(resolver);</pre> +<pre>org.nuiton.i18n.I18n.init(locale);</pre> +</body> +</html> Modified: trunk/nuiton-i18n-api/src/main/java/org/nuiton/util/LocaleConverter.java =================================================================== --- trunk/nuiton-i18n-api/src/main/java/org/nuiton/util/LocaleConverter.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/main/java/org/nuiton/util/LocaleConverter.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -23,6 +23,8 @@ import org.apache.commons.beanutils.ConversionException; import org.apache.commons.beanutils.Converter; import static org.apache.commons.logging.LogFactory.getLog; + +import org.apache.commons.logging.Log; import org.nuiton.i18n.CountryEnum; import org.nuiton.i18n.LanguageEnum; @@ -31,22 +33,26 @@ import java.util.regex.Pattern; /** - * classe pour convertir une chaine en un objet {@link java.util.Locale}. + * classe pour convertir une chaine en un objet {@link Locale}. * - * @author chemit + * @author chemit <chemit@codelutin.com> */ public class LocaleConverter implements Converter { - private static final Pattern FULL_SCOPE_PATTERN = Pattern.compile("([a-zA-Z]{2})_([a-zA-Z]{2})"); + private static final Pattern FULL_SCOPE_PATTERN = + Pattern.compile("([a-zA-Z]{2})_([a-zA-Z]{2})"); - private static final Pattern MEDIUM_SCOPE_PATTERN = Pattern.compile("([a-zA-Z]{2})"); + private static final Pattern MEDIUM_SCOPE_PATTERN = + Pattern.compile("([a-zA-Z]{2})"); /** to use log facility, just put in your code: log.info(\"...\"); */ - static org.apache.commons.logging.Log log = getLog(LocaleConverter.class); + static Log log = getLog(LocaleConverter.class); + @Override public Object convert(Class aClass, Object value) { if (value == null) { - throw new ConversionException("can not convert null value in " + this + " convertor"); + throw new ConversionException("can not convert null value in " + + this + " convertor"); } if (isEnabled(aClass)) { Object result; @@ -59,10 +65,12 @@ return result; } } - throw new ConversionException("could not find a convertor for type " + aClass.getName() + " and value : " + value); + throw new ConversionException( + "could not find a convertor for type " + aClass.getName() + + " and value : " + value); } - protected Locale valueOf(String value) { + public Locale valueOf(String value) { try { Locale result = convertFullScope(value); @@ -71,12 +79,14 @@ } if (result == null) { - throw new ConversionException("could not convert locale " + value); + throw new ConversionException("could not convert locale " + + value); } return result; } catch (Exception e) { - throw new ConversionException("could not convert locale " + value + " for reason " + e.getMessage()); + throw new ConversionException("could not convert locale " + value + + " for reason " + e.getMessage()); } } @@ -84,11 +94,13 @@ Matcher m = FULL_SCOPE_PATTERN.matcher(value); if (m.matches()) { // found a full scope pattern (language + country) - LanguageEnum language = LanguageEnum.valueOf(m.group(1).toLowerCase()); + LanguageEnum language = + LanguageEnum.valueOf(m.group(1).toLowerCase()); CountryEnum country = CountryEnum.valueOf(m.group(2).toUpperCase()); if (language == null || country == null) { // not safe - throw new ConversionException("could not convert locale " + value); + throw new ConversionException("could not convert locale " + + value); } return new Locale(language.name(), country.name()); } @@ -99,11 +111,13 @@ Matcher m = MEDIUM_SCOPE_PATTERN.matcher(value); if (m.matches()) { // found a medium scope pattern (only language) - LanguageEnum language = LanguageEnum.valueOf(m.group(1).toLowerCase()); + LanguageEnum language = + LanguageEnum.valueOf(m.group(1).toLowerCase()); if (language == null) { // not safe - throw new ConversionException("could not convert locale " + value); + throw new ConversionException("could not convert locale " + + value); } return new Locale(language.name()); } @@ -117,8 +131,8 @@ } } - protected boolean isEnabled(Class aClass) { - return aClass == Locale.class; + protected boolean isEnabled(Class<?> aClass) { + return Locale.class.equals(aClass); } public Class<?> getType() { Added: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/I18nStoreTest.java =================================================================== --- trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/I18nStoreTest.java (rev 0) +++ trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/I18nStoreTest.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,135 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.nuiton.i18n.init.DefaultI18nInitializer; + +import java.net.MalformedURLException; +import java.util.Locale; + +/** + * Tests {@link I18nStore}. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.1 + */ +public class I18nStoreTest { + + String encoding; + + Locale locale; + + I18nLanguage language; + + I18nStore store; + + @BeforeClass + public static void beforeClass() throws MalformedURLException { + I18n.setInitializer( + new DefaultI18nInitializer(I18nStoreTest.class.getSimpleName()) + ); + } + + @AfterClass + public static void afterClass() throws Exception { + I18n.setInitializer(null); + I18n.close(); + } + + @Test + public void testGetLoader() throws Exception { + + Assert.assertNull(I18n.store); + + store = I18n.getStore(); + Assert.assertNotNull(store); + //assertEquals(I18n.DEFAULT_ENCODING, store.getEncoding()); + Assert.assertNull(store.getLanguage()); + } + + @Test + public void testChangeLocale() throws Exception { + + locale = I18nUtil.newLocale("fr_FR"); + encoding = I18n.ISO_8859_1_ENCONDING; + updateLanguage(); + assertNbLanguages(1); + updateLanguage(); + assertNbLanguages(1); + + locale = I18nUtil.newLocale("en_GB"); + updateLanguage(); + assertLanguageChanged(); + assertNbLanguages(2); + + locale = I18nUtil.newLocale("en_US"); + updateLanguage(); + assertLanguageChanged(); + assertNbLanguages(3); + + locale = I18nUtil.newLocale("en"); + updateLanguage(); + assertLanguageChanged(); + assertNbLanguages(4); + } + + protected void assertLanguageChanged() { + Assert.assertNotSame(language, store.getLanguage()); + } + + /*public void testChangeEncoding() throws Exception { + locale = I18n.newLocale("fr_FR"); + encoding = I18n.ISO_8859_1_ENCONDING; + updateLanguage(); + + locale = I18n.newLocale("en_GB"); + updateLanguage(); + + // language change (from his encoding) + assertLanguageChanged(); + // 2 language in cache + assertNbLanguages(2); + + encoding = I18n.UTF_8_ENCONDING; + updateLanguage(); + // language change (from his encoding) + assertLanguageChanged(); + // one language in cache + assertNbLanguages(1); + + }*/ + + protected void assertNbLanguages(int i) { + Assert.assertEquals(i, store.getLanguages().size()); + } + + protected void updateLanguage() { + language = store == null ? null : store.getLanguage(); + store = I18n.getStore(); + store.setLanguage(locale); + } + +} + Property changes on: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/I18nStoreTest.java ___________________________________________________________________ Added: svn:keywords + "Author Date Id Revision HeadURL Copied: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBundleScopeTest.java (from rev 1700, trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBunsleScopeTest.java) =================================================================== --- trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBundleScopeTest.java (rev 0) +++ trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBundleScopeTest.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,70 @@ +/* + * *##% + * I18n :: Api + * Copyright (C) 2004 - 2010 CodeLutin + * + * 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 General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * ##%* + */ +package org.nuiton.i18n.bundle; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Locale; + +/** + * Tests {@link I18nBundleScope} + * + * @author tchemit <chemit@codelutin.com> + */ +public class I18nBundleScopeTest { + + Locale locale; + + I18nBundleScope excepted; + + @Test + public void testFullScope() { + excepted = I18nBundleScope.FULL; + + locale = new Locale("fr", "FR"); + Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); + } + + @Test + public void testLanguageScope() { + excepted = I18nBundleScope.LANGUAGE; + + locale = new Locale("fr"); + Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); + + locale = new Locale("fr", ""); + Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); + } + + @Test + public void testGeneralScope() { + + excepted = I18nBundleScope.GENERAL; + + locale = null; + Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); + + locale = new Locale(""); + Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); + } + +} Property changes on: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBundleScopeTest.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision Added: svn:eol-style + native Deleted: trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBunsleScopeTest.java =================================================================== --- trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBunsleScopeTest.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/test/java/org/nuiton/i18n/bundle/I18nBunsleScopeTest.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,65 +0,0 @@ -/** - * *##% - * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin - * - * 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 General Lesser Public License for more details. - * - * You should have received a copy of the GNU General Lesser Public - * License along with this program. If not, see - * <http://www.gnu.org/licenses/lgpl-3.0.html>. - * ##%* - */ -package org.nuiton.i18n.bundle; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.Locale; - -/** @author chemit */ -public class I18nBunsleScopeTest { - - Locale locale; - I18nBundleScope excepted; - - @Test - public void testFullScope() { - excepted = I18nBundleScope.FULL; - - locale = new Locale("fr", "FR"); - Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); - } - - @Test - public void testLanguageScope() { - excepted = I18nBundleScope.LANGUAGE; - - locale = new Locale("fr"); - Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); - - locale = new Locale("fr", ""); - Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); - } - - @Test - public void testGeneralScope() { - - excepted = I18nBundleScope.GENERAL; - - locale = null; - Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); - - locale = new Locale(""); - Assert.assertEquals(excepted, I18nBundleScope.valueOf(locale)); - } - -} Modified: trunk/nuiton-i18n-api/src/test/java/org/nuiton/util/LocaleConverterTest.java =================================================================== --- trunk/nuiton-i18n-api/src/test/java/org/nuiton/util/LocaleConverterTest.java 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/test/java/org/nuiton/util/LocaleConverterTest.java 2010-03-07 16:02:33 UTC (rev 1702) @@ -1,7 +1,7 @@ -/** - * *##% +/* + * *##% * I18n :: Api - * Copyright (C) 2004 - 2009 CodeLutin + * Copyright (C) 2004 - 2010 CodeLutin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -25,11 +25,17 @@ import java.util.Locale; -/** @author chemit */ +/** + * Tests {@link LocaleConverter}. + * + * @author tchemit <chemit@codelutin.com> + */ public class LocaleConverterTest extends TestCase { String toConvert; + Locale excepted; + Converter converter; @Override @@ -65,7 +71,7 @@ //TODO Arch, we must also check coherence ! toConvert = "fr_GB"; - excepted = new Locale("fr","GB"); + excepted = new Locale("fr", "GB"); assertEquals(toConvert, excepted); } Added: trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-definition.properties =================================================================== --- trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-definition.properties (rev 0) +++ trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-definition.properties 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,4 @@ +#Sun Mar 07 00:23:58 CET 2010 +bundles.en_GB=bundleTest/I18nStoreTest-en_GB.properties +bundles.fr_FR=bundleTest/I18nStoreTest-fr_FR.properties +locales=fr_FR,en_GB Added: trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-en_GB.properties =================================================================== --- trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-en_GB.properties (rev 0) +++ trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-en_GB.properties 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,2 @@ +key.one=First +key.two=Second Added: trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-fr_FR.properties =================================================================== --- trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-fr_FR.properties (rev 0) +++ trunk/nuiton-i18n-api/src/test/resources/META-INF/I18nStoreTest-fr_FR.properties 2010-03-07 16:02:33 UTC (rev 1702) @@ -0,0 +1,2 @@ +key.one=Premier +key.two=Seconde Modified: trunk/nuiton-i18n-api/src/test/resources/log4j.properties =================================================================== --- trunk/nuiton-i18n-api/src/test/resources/log4j.properties 2010-03-07 16:00:07 UTC (rev 1701) +++ trunk/nuiton-i18n-api/src/test/resources/log4j.properties 2010-03-07 16:02:33 UTC (rev 1702) @@ -6,6 +6,4 @@ log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) %M - %m%n #log4j.appender.stdout.layout.ConversionPattern=%%c=%c %%C=%C %%d=%d %%F=%F %%l=%l %%L=%L %%m=%m %%M=%M %%p=%p %%r=%r %%t=%t %%x=%x %%X=%X # package level -log4j.logger.org.codelutin.i18n=INFO -log4j.logger.org.codelutin.util=INFO -log4j.logger.org.codelutin.option=INFO +log4j.logger.org.nuiton.i18n=INFO