OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.utils.i18n;

import org.openconcerto.utils.Log;
import org.openconcerto.utils.i18n.TranslationPool.Mode;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle.Control;
import java.util.function.Consumer;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

@ThreadSafe
public class TranslationManager {

    public static final void setVMLocale(Locale locale) {
        Locale.setDefault(locale);
        // As explained in Locale.setDefault() javadoc : "be prepared to reinitialize
        // locale-sensitive code". As ResourceBundle.getBundle(), this throws RuntimeException if no
        // instance can be created.
        TranslationManager.createDefaultInstance();
        // nothing to do for TM
    }

    private static final Locale FALLBACK_LOCALE = Locale.ENGLISH;

    private static final Control CONTROL = new I18nUtils.SameLanguageControl() {
        @Override
        public Locale getFallbackLocale(String baseName, Locale locale) {
            if (!locale.equals(FALLBACK_LOCALE))
                return FALLBACK_LOCALE;
            return null;
        }
    };

    public static final Control getControl() {
        return CONTROL;
    }

    static public interface Loader extends Consumer<TranslationManager> {
    }

    private static final String BASENAME = "translation";
    private static final TranslationPool<TranslationManager, RuntimeException> POOL = new TranslationPool<TranslationManager, RuntimeException>() {
        @Override
        protected TranslationManager createTM(Locale l) throws RuntimeException {
            assert Thread.holdsLock(POOL);
            // Allow some apps to not create an instance at all
            if (classes.isEmpty())
                return null;
            final TranslationManager res = new TranslationManager(l);
            res.setTranslations(classes, true);
            return res;
        }
    };

    /**
     * Create the default instance if needed. Must be called each time {@link TM#getDefaultLocale()}
     * changes otherwise {@link #getInstance()} will throw an exception.
     * 
     * @return the instance for the default locale, <code>null</code> if
     *         {@link #addTranslationStreamFromClass(Class)} wasn't called.
     */
    public static final TranslationManager createDefaultInstance() {
        return POOL.get(TM.getDefaultLocale(), Mode.OPTIONAL_CREATE);
    }

    public static final TranslationManager getInstance() {
        return getInstance(TM.getDefaultLocale());
    }

    // TODO throw exception
    public static final TranslationManager createInstance(final Locale l) {
        return POOL.get(l);
    }

    /**
     * Get an instance already created by {@link #createInstance(Locale)}. This method won't block,
     * that's what createInstance() is for.
     * 
     * @param l the locale
     * @return the cached instance.
     */
    public static final TranslationManager getInstance(final Locale l) {
        return POOL.getCreated(l);
    }

    @GuardedBy("POOL")
    private static final List<Object> classes = new ArrayList<>();

    public static final void addTranslationStreamFromClass(Class<?> c) {
        _addLoader(c);
    }

    public static final void addLoader(final Loader loader) {
        _addLoader(loader);
    }

    private static final void _addLoader(final Object loader) {
        synchronized (POOL) {
            classes.add(loader);
            for (final TranslationManager tm : POOL.getCreated()) {
                tm.addTranslations(loader, true);
            }
        }
    }

    public static final void removeTranslationStreamFromClass(Class<?> c) {
        _removeLoader(c);
    }

    public static final void removeLoader(final Loader loader) {
        _removeLoader(loader);
    }

    private static final void _removeLoader(final Object o) {
        synchronized (POOL) {
            if (classes.remove(o)) {
                for (final TranslationManager tm : POOL.getCreated()) {
                    tm.setTranslations(classes, false);
                }
            }
        }
    }

    private final Locale locale;

    private final Object trMutex = new Object();
    @GuardedBy("trMutex")
    private Map<String, String> menuTranslation;
    @GuardedBy("trMutex")
    private Map<String, String> itemTranslation;
    @GuardedBy("trMutex")
    private Map<String, String> actionTranslation;

    private TranslationManager(final Locale locale) {
        this.locale = locale;
    }

    public final Locale getLocale() {
        return this.locale;
    }

    private void checkNulls(String id, String label) {
        if (id == null)
            throw new NullPointerException("null id");
        if (label == null)
            throw new NullPointerException("null label");
    }

    // Menus in menu bar and menu items

    public String getTranslationForMenu(String id) {
        synchronized (this.trMutex) {
            return this.menuTranslation.get(id);
        }
    }

    public void setTranslationForMenu(String id, String label) {
        checkNulls(id, label);
        synchronized (this.trMutex) {
            this.menuTranslation.put(id, label);
        }
    }

    // Items labels in panels

    public String getTranslationForItem(String id) {
        synchronized (this.trMutex) {
            return this.itemTranslation.get(id);
        }
    }

    public void setTranslationForItem(String id, String label) {
        checkNulls(id, label);
        synchronized (this.trMutex) {
            this.itemTranslation.put(id, label);
        }
    }

    // Actions (buttons or contextual menus)

    public String getTranslationForAction(String id) {
        synchronized (this.trMutex) {
            return this.actionTranslation.get(id);
        }
    }

    public void setTranslationForAction(String id, String label) {
        checkNulls(id, label);
        synchronized (this.trMutex) {
            this.actionTranslation.put(id, label);
        }
    }

    private void setTranslations(final Collection<?> classes, final boolean warn) {
        synchronized (this.trMutex) {
            this.menuTranslation = new HashMap<>();
            this.itemTranslation = new HashMap<>();
            this.actionTranslation = new HashMap<>();
            if (warn && classes.isEmpty()) {
                Log.get().warning("TranslationManager has no resources to load (" + this.getLocale() + ")");
            }
            for (Object o : classes) {
                this.addTranslations(o, warn);
            }
        }
    }

    private void addTranslations(final Object o, final boolean warn) {
        if (o instanceof Class) {
            final Class<?> c = (Class<?>) o;
            boolean loaded = loadTranslation(this.getLocale(), c);
            if (warn && !loaded) {
                Log.get().warning("TranslationManager was unable to load translation " + c.getCanonicalName() + " for locale " + this.getLocale());
            }
        } else {
            final Loader loader = (Loader) o;
            loader.accept(this);
        }
    }

    // return all existing (e.g fr_CA only specify differences with fr)
    private List<String> findResources(final Locale locale, final Class<?> c, final boolean rootLast) {
        final Control cntrl = CONTROL;
        final List<String> res = new ArrayList<>();
        final String baseName = c.getPackage().getName() + "." + BASENAME;

        // test emptiness to not mix languages
        for (Locale targetLocale = locale; targetLocale != null && res.isEmpty(); targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
            for (final Locale candidate : cntrl.getCandidateLocales(baseName, targetLocale)) {
                res.add(cntrl.toResourceName(cntrl.toBundleName(baseName, candidate), "xml"));
            }
        }
        if (!rootLast)
            Collections.reverse(res);
        return res;
    }

    private boolean loadTranslation(final Locale l, final Class<?> c) {
        boolean translationLoaded = false;

        // FIXME : l'implementation de Java est lente
        // com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl : 60 ms!
        // On pourrait passer à 1ms avec Piccolo...
        final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        final DocumentBuilder dBuilder;
        try {
            dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            dBuilder = dbFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("Couldn't create DocumentBuilder", e);
        }

        // we want more specific translations to replace general ones, i.e. root Locale first
        for (final String rsrcName : findResources(l, c, false)) {
            // create new instances to check if there's no duplicates in each resource
            final Map<String, String> menuTranslation = new HashMap<>(), itemTranslation = new HashMap<>(), actionTranslation = new HashMap<>();
            try (final InputStream input = c.getClassLoader().getResourceAsStream(rsrcName)) {
                if (input == null)
                    continue;
                final Document doc = dBuilder.parse(input);
                // Menus
                loadTranslation(doc, "menu", menuTranslation);
                // Items (title, labels not related to fields...)
                loadTranslation(doc, "item", itemTranslation);
                // Actions
                loadTranslation(doc, "action", actionTranslation);
            } catch (SAXException | IOException e) {
                e.printStackTrace();
                // don't return to load as much as possible
            }
            // on the other hand, it's OK for one resource to override another
            this.menuTranslation.putAll(menuTranslation);
            this.itemTranslation.putAll(itemTranslation);
            this.actionTranslation.putAll(actionTranslation);
            translationLoaded = true;
        }
        return translationLoaded;
    }

    private static void loadTranslation(final Document doc, final String tagName, final Map<String, String> m) {
        final NodeList menuChildren = doc.getElementsByTagName(tagName);
        final int size = menuChildren.getLength();
        for (int i = 0; i < size; i++) {
            final Element element = (Element) menuChildren.item(i);
            final String id = element.getAttributeNode("id").getValue();
            final String label = element.getAttributeNode("label").getValue();
            if (m.containsKey(id)) {
                throw new IllegalStateException("Duplicate " + tagName + " translation entry for " + id + " (" + label + ")");
            }
            m.put(id, label);
        }
    }
}