Index: lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleManager.java diff -u /dev/null lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleManager.java:1.1 --- /dev/null Mon Mar 3 13:14:45 2008 +++ lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleManager.java Mon Mar 3 13:14:40 2008 @@ -0,0 +1,235 @@ +/* +* ##% Copyright (C) 2007, 2008 Code Lutin, Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.i18n.bundle; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.codelutin.i18n.Language; +import org.codelutin.i18n.LanguageManager; +import org.codelutin.util.Resource; +import org.codelutin.util.StringUtil; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.regex.Matcher; + +/** + * Classe permettant de gerer les bundles. + * + * @author chemit + */ +public class I18nBundleManager { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + private static final Log log = LogFactory.getLog(LanguageManager.class); + + /** pattern to find all i18n bundles in classloader class path */ + static final String SEARCH_BUNDLE_PATTERN = ".*18n/.+\\.properties"; + + /** shared instance */ + protected static I18nBundleManager instance; + + /** le cache de languages deja charges indexees par leur encoding */ + protected List cache; + + public static synchronized I18nBundleManager getInstance() { + if (instance == null) { + instance = new I18nBundleManager(); + } + return instance; + } + + public static void init() { + I18nBundleManager manager = getInstance(); + if (manager.isInit()) { + // already init + return; + } + + // get all bundles urls + URL[] urls = manager.getURLs(Language.getLoader()); + + long t0 = System.nanoTime(); + + // detect bundles + List bundles = manager.detectBundles(urls); + + // once for all, sort entries from general to full + for (I18nBundle bundle : bundles) { + Collections.sort(bundle.getEntries(), + new I18nBundleEntry.I18nBundleEntryByLevelComparator()); + } + + // save bundles in cache + manager.cache = Collections.unmodifiableList(bundles); + + log.info(bundles.size() + " bundle(s) found, ["+manager.getBundleEntries().length+" file(s)] in "+ StringUtil.convertTime(System.nanoTime()-t0)); + } + + public I18nBundle[] getBundles() { + checkInit(); + return cache.toArray(new I18nBundle[cache.size()]); + } + + public I18nBundle[] getBundles(Locale l) { + checkInit(); + List result = new ArrayList(); + for (I18nBundle i18nBundle : cache) { + if (i18nBundle.matchLocale(l)) { + result.add(i18nBundle); + } + } + return result.toArray(new I18nBundle[result.size()]); + } + + public I18nBundleEntry[] getBundleEntries() { + checkInit(); + List result = new ArrayList(); + for (I18nBundle i18nBundle : cache) { + List list = i18nBundle.getEntries(); + if (!list.isEmpty()) { + result.addAll(list); + } + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + public I18nBundleEntry[] getBundleEntries(Locale l) { + checkInit(); + List result = new ArrayList(); + for (I18nBundle i18nBundle : cache) { + I18nBundleEntry[] entries = i18nBundle.getEntries(l); + if (entries.length > 0) { + result.addAll(Arrays.asList(entries)); + } + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + public void load(Language language, Properties resource) { + long t0 = System.nanoTime(); + I18nBundleEntry[] entries = getBundleEntries(language.getLocale()); + for (I18nBundleEntry entry : entries) { + entry.load(language, resource); + } + log.info(language + ", nbEntries: " + entries.length + ", nbSentences: " + language.size()+" in "+StringUtil.convertTime(System.nanoTime()-t0)); + } + + /** + * Recherche la liste des url de bundles i18n correspondant à la langue + * donné. + * + * @param loader le classe loader où trouver les bundles + * @return la liste des urls de bundle i18n pour la langue donné + */ + protected URL[] getURLs(URLClassLoader loader) { + + List urls = new ArrayList(); + try { + // always try to match the lutinutil class loader, + // since other url should be in this one ? + //FIXME: this does not work in a jboss application... + if (loader == null) { + urls.addAll(Resource.getURLs(SEARCH_BUNDLE_PATTERN)); + } else { + urls.addAll(Resource.getURLs(SEARCH_BUNDLE_PATTERN, loader)); + } + } catch (Exception eee) { + log.warn("Unable to find urls for loader : " + loader); + return new URL[0]; + } + return urls.toArray(new URL[urls.size()]); + } + + protected List detectBundles(URL... urls) { + + List bundleNames = new ArrayList(); + List bundles = new ArrayList(); + + 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(); + return bundles; + } + + protected boolean addBundleEntry(URL url, I18nBundleScope scope, List bundleNames, List 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(scope.getLocale(matcher), scope, url); + 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 I18nBundle addBundle(String bundleName, List bundleNames, List 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; + } + + protected boolean isInit() { + return cache != null; + } + + protected void checkInit() { + if (!isInit()) { + throw new IllegalStateException("should call init method on " + I18nBundleManager.class); + } + } + + protected I18nBundleManager() { + + } +} \ No newline at end of file Index: lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleEntry.java diff -u /dev/null lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleEntry.java:1.1 --- /dev/null Mon Mar 3 13:14:45 2008 +++ lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleEntry.java Mon Mar 3 13:14:40 2008 @@ -0,0 +1,120 @@ +/* +* ##% Copyright (C) 2007, 2008 Code Lutin, Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.i18n.bundle; + +import org.codelutin.i18n.I18nFileReader; +import org.codelutin.i18n.Language; + +import java.io.IOException; +import java.net.URL; +import java.util.Comparator; +import java.util.Locale; +import java.util.Properties; + +/** + * Une classe pour representer une entree dans un bundle, i.e une implementation + * de bundle, soit un fichier de properties. + * + * @author chemit + */ +public class I18nBundleEntry { + + /** le niveau i18n du bundle encapsulé */ + protected I18nBundleScope scope; + + /** la locale du bundle (null si le scope est générale ???) */ + protected Locale locale; + + /** le chemin d'accès au bundle */ + protected URL path; + + public I18nBundleEntry(Locale locale, I18nBundleScope scope, URL path) { + this.locale = locale; + this.scope = scope; + this.path = path; + } + + public I18nBundleScope getScope() { + return scope; + } + + public Locale getLocale() { + return locale; + } + + public URL getPath() { + return path; + } + + public boolean matchLocale(Locale locale, I18nBundleScope scope) { + if (this.locale == null) { + // a general bundle entry is always matched! + return true; + } + if (locale == null) { + // can not match a specialized entry with a general scope + return false; + } + // this.locale!=null && locale!=null + return this.locale.equals(locale) || + (this.scope.ordinal() < scope.ordinal() && locale.getLanguage().equals(this.locale.getLanguage())); + + } + + public void load(Language language, Properties resource) { + try { + loadResource(language.getEncoding(), resource, getPath()); + if (I18nBundle.log.isDebugEnabled()) { + I18nBundle.log.info(getPath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void loadResource(String encoding, Properties resource, URL url) throws IOException { + I18nFileReader fileReader; + fileReader = new I18nFileReader(); + fileReader.load2(url.openStream(), encoding); + resource.putAll(fileReader); + fileReader.clear(); + } + + @Override + public boolean equals(Object o) { + return this == o || o instanceof I18nBundleEntry && path.equals(((I18nBundleEntry) o).path); + + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public String toString() { + String s = super.toString(); + return "<" + s.substring(s.lastIndexOf(".") + 1) + ", locale:" + locale + ", scope " + scope + ", path:" + path + ">"; + } + + public static class I18nBundleEntryByLevelComparator implements Comparator { + public int compare(I18nBundleEntry o1, I18nBundleEntry o2) { + return o1.getScope().compareTo(o2.getScope()); + } + } +} Index: lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleScope.java diff -u /dev/null lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleScope.java:1.1 --- /dev/null Mon Mar 3 13:14:45 2008 +++ lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundleScope.java Mon Mar 3 13:14:40 2008 @@ -0,0 +1,126 @@ +/* +* ##% Copyright (C) 2007, 2008 Code Lutin, Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.i18n.bundle; + +import java.util.Locale; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Definition de la portée d'un bundle i18n. + *

+ * La portée est le niveau de spécialisation du bundle. + *

+ * La portée le plus basse étant {@link #GENERAL}, qui correspond à un bunle sans + * indication de locale (par exemple lutin.properties). + *

+ * Vient ensuite la portée {@link #LANGUAGE} qui correspond à un bundle avec un + * spécialisation sur la langue (par exemple lutin-fr.properties). + *

+ * Enfin, la portée la plus haute {@link #FULL} qui correspond à un bunle avec + * une spécialisation sur la langue et le pays (par exemple + * lutin-fr_FR.properties). + *

+ * Les portées sont inclusives, i.e qu'une portée inclue les portées de niveaux + * inferieures. + *

+ * Note : on utilise la propriété {@link #ordinal} de l'énum pour définir l'ordre + * sur les portées. + * + * @author chemit + */ +public enum I18nBundleScope { + /** none */ + GENERAL("(.*18n/.+)\\.properties") { + public Locale getLocale(Matcher matcher) { + // no locale for general bundle + return null; + } + }, + /** language */ + LANGUAGE("(.*18n/.+)-(\\w\\w)\\.properties") { + + public Locale getLocale(Matcher matcher) { + Locale result = null; + if (matcher.matches()) { + String language = matcher.group(2).toLowerCase(); + result = new Locale(language); + } + return result; + } + }, + /** language + country */ + FULL("(.*18n/.+)-(\\w\\w)_(\\w\\w)\\.properties") { + public Locale getLocale(Matcher matcher) { + Locale result = null; + if (matcher.matches()) { + String language = matcher.group(2).toLowerCase(); + String country = matcher.group(3).toUpperCase(); + result = new Locale(language, country); + } + return result; + } + }; + + private Pattern patternAll; + + I18nBundleScope(String patternAll) { + this.patternAll = Pattern.compile(patternAll); + } + + public Matcher getMatcher(String path) { + return patternAll.matcher(path); + } + + public String getBundlePrefix(Matcher matcher) { + String result = null; + if (matcher.matches()) { + result = matcher.group(1); + } + return result; + } + + public abstract Locale getLocale(Matcher matcher); + + public static I18nBundleScope valueOf(Locale locale) { + if (locale == null) { + return GENERAL; + } + if (locale.getCountry() == null) { + return LANGUAGE; + } + return FULL; + } + + + static I18nBundleScope[] reverseValues; + + public static I18nBundleScope[] reverseValues() { + if (reverseValues==null) { + I18nBundleScope[] result = values(); + ArrayList list = new ArrayList(Arrays.asList(values())); + Collections.sort(list); + Collections.reverse(list); + reverseValues =list.toArray(new I18nBundleScope[list.size()]); + } + return reverseValues; + } +} \ No newline at end of file Index: lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundle.java diff -u /dev/null lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundle.java:1.1 --- /dev/null Mon Mar 3 13:14:45 2008 +++ lutinutil/src/java/org/codelutin/i18n/bundle/I18nBundle.java Mon Mar 3 13:14:40 2008 @@ -0,0 +1,106 @@ +/* +* \#\#% Copyright (C) 2007, 2008 Code Lutin, Tony Chemit, Gabriel Landais +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* \#\#% */ +package org.codelutin.i18n.bundle; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Cette classe représente un bundle chargé par i18n. + *

+ * Il encapsule un bundle et de ses différents scopes. + * + * @author chemit + * @see I18nBundleScope + */ +public class I18nBundle { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + static final Log log = LogFactory.getLog(I18nBundle.class); + + /** les entrés du bundle */ + protected List entries; + + /** le nom du bundle encapsulé (correspond au prefix de l'url de chargement) */ + final String bundlePrefix; + + public I18nBundle(String bundlePrefix) { + this.bundlePrefix = bundlePrefix; + } + + public String getBundlePrefix() { + return bundlePrefix; + } + + public I18nBundleEntry[] getEntries(Locale locale) { + I18nBundleScope scope = I18nBundleScope.valueOf(locale); + List result = new ArrayList(); + for (I18nBundleEntry entry : entries) { + I18nBundleScope i18nBundleScope = entry.getScope(); + if ((i18nBundleScope == scope || i18nBundleScope.ordinal() < scope.ordinal()) && entry.matchLocale(locale, scope)) { + result.add(entry); + } + } + return result.toArray(new I18nBundleEntry[result.size()]); + } + + public int size() { + return entries == null ? 0 : entries.size(); + } + + protected List getEntries() { + return entries; + } + + protected boolean matchLocale(Locale locale) { + I18nBundleScope scope = I18nBundleScope.valueOf(locale); + boolean result = false; + if (size() != 0) { + for (I18nBundleEntry entry : entries) { + if (entry.matchLocale(locale, scope)) { + result = true; + break; + } + } + } + return result; + } + + protected boolean addEntry(I18nBundleEntry entry) { + if (entries == null) { + entries = new ArrayList(); + } + boolean b = entries.add(entry); + if (log.isDebugEnabled()) { + log.info(this + "\n\t" + entry); + } + return b; + } + + @Override + public String toString() { + String s = super.toString(); + return "<" + s.substring(s.lastIndexOf(".") + 1) + ", bundlePrefix:" + bundlePrefix + ", size:" + size() + ">"; + } + + +}