org.nuiton.i18n.bundle.I18nBundleUtil Maven / Gradle / Ivy
/*
* #%L
* I18n :: Runtime
* %%
* Copyright (C) 2018 Code Lutin, Ultreia.io
* %%
* 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
* .
* #L%
*/
package org.nuiton.i18n.bundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.nuiton.i18n.I18nUtil;
import java.io.File;
import java.io.FileInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Usefull methods on bundles.
*
* Note: Replace the previous class {@code org.nuiton.i18n.bundle.I18nBundleFactory}.
*
* @author Tony Chemit - [email protected]
* @since 1.1
*/
public class I18nBundleUtil {
public static final String DIRECTORY_SEARCH_BUNDLE_PATTERN = "i18n";
public static final URL[] EMPTY_URL_ARRAY = new URL[0];
/** Logger. */
private static final Logger log = LogManager.getLogger(I18nBundleUtil.class);
private static final I18nBundleEntry[] EMPTY_I18N_BUNDLE_ENTRYS_ARRAY = new I18nBundleEntry[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 result = new 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[0]);
}
/**
* 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[0]);
}
/**
* 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[0]);
}
/**
* 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[0]);
}
/**
* 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 = promoteBundle(i18nBundle, l, defaultLocale,
promuteGeneral);
}
result.addAll(Arrays.asList(entries));
}
return result.toArray(new I18nBundleEntry[0]);
}
/**
* Filtrage des entrées de bundles pour une locale donnée sans aucune promotion.
*
* @param l la locale à filtrer
* @param bundles les bundles a parcourir
* @return les entrées de bundles filtrées.
*/
public static I18nBundleEntry[] getBundleEntries(Locale l, I18nBundle... bundles) {
List result = new ArrayList<>();
for (I18nBundle i18nBundle : bundles) {
I18nBundleEntry[] entries = i18nBundle.getEntries(l);
for (I18nBundleEntry entry : entries) {
if (l.equals(entry.getLocale())) {
result.add(entry);
}
}
}
return result.toArray(new I18nBundleEntry[0]);
}
/**
* 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 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(String.format("skip url with no %s directory : %s", DIRECTORY_SEARCH_BUNDLE_PATTERN, url));
}
it.remove();
}
}
if (log.isDebugEnabled()) {
log.debug(String.format("detect %d i18n capable url (out of %d)", urlToSeek.size(), size));
}
List listURLs = new ArrayList<>();
for (URL url : urlToSeek) {
// on recherche tous les fichiers de traduction pour cet url
List result = null;
if (log.isDebugEnabled()) {
log.debug(String.format("seek in : %s", 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 (log.isDebugEnabled()) {
log.debug(String.format("url to search %s", file));
log.debug(String.format("Is a exsting file or directory ? %s", file.exists()));
}
if (I18nUtil.isJar(fileName)) {
// cas ou le ichier du classLoader est un fichier jar
if (log.isDebugEnabled()) {
log.debug(String.format("jar to search %s", file));
}
result = getURLsFromJar(url, file);
} else if (file.isDirectory()) {
// cas ou le ichier du classLoader est un repertoire
if (log.isDebugEnabled()) {
log.debug(String.format("directory to search %s", 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[0]);
} catch (Exception eee) {
if (log.isWarnEnabled()) {
log.warn(String.format("Unable to find urls for urls : %s for reason %s", Arrays.toString(urls), 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} si aucune entree trouvee, {@code 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;
}
/**
* 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) {
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(String.format("bundle (%d) : %s", 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(String.format("bundle (%d) : %s", 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...)}
* returns a empty array.
*
* @param bundle the bundle to promote
* @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 promoted for the given locale
*/
protected static I18nBundleEntry[] promoteBundle(I18nBundle bundle,
Locale l,
Locale defaultLocale,
boolean promuteGeneral) {
I18nBundleScope scope = I18nBundleScope.valueOf(l);
if (log.isDebugEnabled()) {
log.debug(String.format("%s%s] did not find matching entries for locale %s. Try to detect best entries...", '[', bundle.getBundlePrefix(), l));
}
if (bundle.size() == 0) {
// there is no entry to take...
if (log.isWarnEnabled()) {
log.warn("PROMOTE 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);
if (log.isWarnEnabled()) {
log.warn(String.format("PROMOTE %s to %s [%s]", l, entry.getLocale(), bundle.getBundlePrefix()));
}
return new I18nBundleEntry[]{entry};
}
List result = new ArrayList<>();
switch (scope) {
case FULL:
promoteFull(bundle, l, defaultLocale, result, promuteGeneral);
break;
case LANGUAGE:
promoteLanguage(bundle, l, defaultLocale, result, promuteGeneral);
break;
case GENERAL:
if (promuteGeneral) {
promoteGeneral(bundle, l, defaultLocale, result);
}
break;
}
return result.toArray(new I18nBundleEntry[0]);
}
protected static void promoteFull(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
Locale locale1 = entry.getLocale();
if (i18nBundleScope == I18nBundleScope.FULL &&
!locale1.getCountry().equals(locale.getCountry()) &&
locale1.getLanguage().equals(locale.getLanguage())) {
if (log.isWarnEnabled()) {
log.warn(String.format("%s to %s [%s]", locale, locale1, bundle.getBundlePrefix()));
}
result.add(entry);
// we take the first one, this is a resuce!!!
break;
}
}
if (result.isEmpty()) {
// full promotion failed,trylanguage promotion
promoteLanguage(bundle, locale, defaultLocale, result,
promuteGeneral);
}
}
protected static void promoteLanguage(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
Locale locale1 = entry.getLocale();
if (i18nBundleScope == I18nBundleScope.FULL &&
locale1.getLanguage().equals(locale.getLanguage())) {
result.add(entry);
if (log.isWarnEnabled()) {
log.warn(String.format("%s to %s [%s]", locale, locale1, bundle.getBundlePrefix()));
}
// we take the first one, this is a rescue!!!
break;
}
}
if (result.isEmpty() && promuteGeneral) {
// language promotion failed,try general promotion
promoteGeneral(bundle, locale, defaultLocale, result);
}
}
protected static void promoteGeneral(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);
if (log.isWarnEnabled()) {
log.warn(String.format("%s to %s [%s]", locale, entry.getLocale(), bundle.getBundlePrefix()));
}
return;
}
I18nBundleScope scope = I18nBundleScope.valueOf(defaultLocale);
for (I18nBundleEntry entry : bundle.getEntries(scope)) {
if (entry.getLocale().equals(defaultLocale)) {
// default locale found
if (log.isWarnEnabled()) {
log.warn(String.format("%s to %s [%s]", locale, 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);
if (log.isWarnEnabled()) {
log.warn(String.format("%s to %s [%s]", locale, entry.getLocale(), bundle.getBundlePrefix()));
}
}
protected static List getURLsFromJar(URL incomingURL, File jarfile) {
/**
* fix ano-1812
* use this fixed pattern instead of getSearchBundlePattern() because ZipEntry.getName() always use 'linux' path description
*/
String pattern = ".*i18n/.+\\.properties";
try {
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(jarfile))) {
List result = new ArrayList<>();
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(String.format("%s accepted for pattern %s", name, 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(
String.format("n'a pas pu trouve la resource dans le jar %s", jarfile.getAbsolutePath()), eee);
}
}
/**
* Compute the search pattern according to the {@link File#separator} on
* the underlying os.
*
* Under linux this is {@code .*i18n/.+\.properties}, and under windows this
* is {@code .*i18n\\\\.+\.properties}.
*
* @return the correct pattern
* @since 2.0
*/
protected static String getSearchBundlePattern() {
String result = ".*i18n";
String path = File.separator;
result += "\\".equals(path) ? path + path : path;
return result + ".+\\.properties";
}
protected static List getURLsFromDirectory(URL incomingURL,
File repository) {
String pattern = getSearchBundlePattern();
try {
if (log.isDebugEnabled()) {
log.debug(String.format("search '%s' in %s", pattern, 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 (name.matches(pattern)) {
URL url = file.toURI().toURL();
if (log.isDebugEnabled()) {
log.debug(String.format("directory: %s url: %s", repository, url));
}
urlList.add(url);
}
}
}
return urlList;
} catch (MalformedURLException eee) {
throw new RuntimeException(
String.format("n'a pas pu trouve la resource dans le repertoire %s", repository.getAbsolutePath()), eee);
}
}
}