Dépôt officiel du code source de l'ERP OpenConcerto
Rev 174 | Go to most recent revision | 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);
}
}
}