org.nuiton.i18n.bundle.I18nBundleFactory Maven / Gradle / Ivy
package org.nuiton.i18n.bundle;
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.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}.
*
* On retrouve aussi ici des méthodes utiles de parcours de bundles.
*
* @author chemit
*
* @since 1.0.6
*/
public class I18nBundleFactory {
/** 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 result = new java.util.HashSet();
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 result = new ArrayList();
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 result = new ArrayList();
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 result = new ArrayList();
for (I18nBundle i18nBundle : bundles) {
List 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 result = new ArrayList();
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 true
si aucune entree trouvee, false
* 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.
*
* 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
*
* Ensuite pour chaque locale on recupere l'url du fichier :
*
* /META-INF/uniqueBundleName-locale.properties
*
* Exemple :
*
*
* fichier de définition : /META-INF/monAppli-definition.properties
* locales=fr_fr,es_ES
*
* fichiers de traduction
* /META-INF/monAppli-fr_FR.properties
* /META-INF/monAppli-es_ES.properties
*
*
*
* @param uniqueBundleName le nom de l'unique bundle a charger
* @return la liste des urls de bundle i18n
*/
public static URL[] getURLs(String uniqueBundleName) {
String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, uniqueBundleName);
URL[] urls = null;
try {
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 lUrls = new java.util.ArrayList(1);
String prefixURL = defURL.toString();
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());
log.info("detected bundle properties file : " + url);
URL u = new URL(url);
// //FIXME on devrait tester que la resource est disponible ?
lUrls.add(u);
}
if (!lUrls.isEmpty()) {
urls = lUrls.toArray(new URL[lUrls.size()]);
} else {
// l'unique bundle n'a pas ete trouve!
// on utilise la methode classique de chargement avec recherche
// de tous les bundles i18n
log.warn("not bundle files detected in " + prefixURL);
urls = null;
}
} catch (Exception ex) {
log.warn("could not load unique bundle " + uniqueBundleName + " for reason " + ex.getMessage(), ex);
urls = null;
}
return urls;
}
public static Properties loadUniqueNameDefFile(String uniqueBundleName) {
String definitionFileName = String.format(UNIQUE_BUNDLE_DEF, uniqueBundleName);
Properties p = new Properties();
try {
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);
}
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 urlToSeek = new ArrayList();
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 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 listURLs = new java.util.ArrayList();
for (URL url : urlToSeek) {
// on recherche tous les fichiers de traduction pour cet url
List 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 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();
// 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 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(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 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;
}
/**
* Obtain some rescue entries for a given locale.
*
* Note: 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 result = new ArrayList();
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 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 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 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 getURLsFromJar(URL incomingURL, File jarfile) {
String pattern = SEARCH_BUNDLE_PATTERN;
try {
List result = new ArrayList();
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 getURLsFromDirectory(URL incomingURL, File repository) {
String pattern = SEARCH_BUNDLE_PATTERN;
try {
if (log.isDebugEnabled()) {
log.debug("search '" + pattern + "' in " + repository);
}
List urlList = new ArrayList();
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);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy