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

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

There is a newer version: 1.0.1
Show newest version
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