All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.i18n.bundle.I18nBundleUtil Maven / Gradle / Ivy

There is a newer version: 4.0-beta-27
Show newest version
/*
 * #%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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy