OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
67 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils.i18n;
15
 
16
import org.openconcerto.utils.Log;
177 ilm 17
import org.openconcerto.utils.i18n.TranslationPool.Mode;
67 ilm 18
 
19
import java.io.IOException;
20
import java.io.InputStream;
21
import java.util.ArrayList;
177 ilm 22
import java.util.Collection;
67 ilm 23
import java.util.Collections;
24
import java.util.HashMap;
25
import java.util.List;
26
import java.util.Locale;
27
import java.util.Map;
28
import java.util.ResourceBundle.Control;
177 ilm 29
import java.util.function.Consumer;
67 ilm 30
 
174 ilm 31
import javax.xml.XMLConstants;
67 ilm 32
import javax.xml.parsers.DocumentBuilder;
33
import javax.xml.parsers.DocumentBuilderFactory;
177 ilm 34
import javax.xml.parsers.ParserConfigurationException;
67 ilm 35
 
36
import org.w3c.dom.Document;
37
import org.w3c.dom.Element;
38
import org.w3c.dom.NodeList;
177 ilm 39
import org.xml.sax.SAXException;
67 ilm 40
 
174 ilm 41
import net.jcip.annotations.GuardedBy;
42
import net.jcip.annotations.ThreadSafe;
43
 
73 ilm 44
@ThreadSafe
67 ilm 45
public class TranslationManager {
177 ilm 46
 
47
    public static final void setVMLocale(Locale locale) {
48
        Locale.setDefault(locale);
49
        // As explained in Locale.setDefault() javadoc : "be prepared to reinitialize
50
        // locale-sensitive code". As ResourceBundle.getBundle(), this throws RuntimeException if no
51
        // instance can be created.
52
        TranslationManager.createDefaultInstance();
53
        // nothing to do for TM
54
    }
55
 
67 ilm 56
    private static final Locale FALLBACK_LOCALE = Locale.ENGLISH;
57
 
73 ilm 58
    private static final Control CONTROL = new I18nUtils.SameLanguageControl() {
67 ilm 59
        @Override
60
        public Locale getFallbackLocale(String baseName, Locale locale) {
61
            if (!locale.equals(FALLBACK_LOCALE))
62
                return FALLBACK_LOCALE;
63
            return null;
64
        }
65
    };
66
 
73 ilm 67
    public static final Control getControl() {
68
        return CONTROL;
69
    }
70
 
177 ilm 71
    static public interface Loader extends Consumer<TranslationManager> {
72
    }
73
 
67 ilm 74
    private static final String BASENAME = "translation";
177 ilm 75
    private static final TranslationPool<TranslationManager, RuntimeException> POOL = new TranslationPool<TranslationManager, RuntimeException>() {
76
        @Override
77
        protected TranslationManager createTM(Locale l) throws RuntimeException {
78
            assert Thread.holdsLock(POOL);
79
            // Allow some apps to not create an instance at all
80
            if (classes.isEmpty())
81
                return null;
82
            final TranslationManager res = new TranslationManager(l);
83
            res.setTranslations(classes, true);
84
            return res;
85
        }
86
    };
67 ilm 87
 
177 ilm 88
    /**
89
     * Create the default instance if needed. Must be called each time {@link TM#getDefaultLocale()}
90
     * changes otherwise {@link #getInstance()} will throw an exception.
91
     *
92
     * @return the instance for the default locale, <code>null</code> if
93
     *         {@link #addTranslationStreamFromClass(Class)} wasn't called.
94
     */
95
    public static final TranslationManager createDefaultInstance() {
96
        return POOL.get(TM.getDefaultLocale(), Mode.OPTIONAL_CREATE);
97
    }
98
 
67 ilm 99
    public static final TranslationManager getInstance() {
177 ilm 100
        return getInstance(TM.getDefaultLocale());
67 ilm 101
    }
102
 
177 ilm 103
    // TODO throw exception
104
    public static final TranslationManager createInstance(final Locale l) {
105
        return POOL.get(l);
106
    }
73 ilm 107
 
177 ilm 108
    /**
109
     * Get an instance already created by {@link #createInstance(Locale)}. This method won't block,
110
     * that's what createInstance() is for.
111
     *
112
     * @param l the locale
113
     * @return the cached instance.
114
     */
115
    public static final TranslationManager getInstance(final Locale l) {
116
        return POOL.getCreated(l);
117
    }
67 ilm 118
 
177 ilm 119
    @GuardedBy("POOL")
120
    private static final List<Object> classes = new ArrayList<>();
121
 
122
    public static final void addTranslationStreamFromClass(Class<?> c) {
123
        _addLoader(c);
67 ilm 124
    }
125
 
177 ilm 126
    public static final void addLoader(final Loader loader) {
127
        _addLoader(loader);
67 ilm 128
    }
129
 
177 ilm 130
    private static final void _addLoader(final Object loader) {
131
        synchronized (POOL) {
132
            classes.add(loader);
133
            for (final TranslationManager tm : POOL.getCreated()) {
134
                tm.addTranslations(loader, true);
73 ilm 135
            }
136
        }
137
    }
138
 
177 ilm 139
    public static final void removeTranslationStreamFromClass(Class<?> c) {
140
        _removeLoader(c);
73 ilm 141
    }
142
 
177 ilm 143
    public static final void removeLoader(final Loader loader) {
144
        _removeLoader(loader);
145
    }
146
 
147
    private static final void _removeLoader(final Object o) {
148
        synchronized (POOL) {
149
            if (classes.remove(o)) {
150
                for (final TranslationManager tm : POOL.getCreated()) {
151
                    tm.setTranslations(classes, false);
152
                }
73 ilm 153
            }
67 ilm 154
        }
155
    }
156
 
177 ilm 157
    private final Locale locale;
158
 
159
    private final Object trMutex = new Object();
160
    @GuardedBy("trMutex")
161
    private Map<String, String> menuTranslation;
162
    @GuardedBy("trMutex")
163
    private Map<String, String> itemTranslation;
164
    @GuardedBy("trMutex")
165
    private Map<String, String> actionTranslation;
166
 
167
    private TranslationManager(final Locale locale) {
168
        this.locale = locale;
169
    }
170
 
171
    public final Locale getLocale() {
172
        return this.locale;
173
    }
174
 
67 ilm 175
    private void checkNulls(String id, String label) {
176
        if (id == null)
177
            throw new NullPointerException("null id");
178
        if (label == null)
179
            throw new NullPointerException("null label");
180
    }
181
 
177 ilm 182
    // Menus in menu bar and menu items
67 ilm 183
 
184
    public String getTranslationForMenu(String id) {
73 ilm 185
        synchronized (this.trMutex) {
186
            return this.menuTranslation.get(id);
187
        }
67 ilm 188
    }
189
 
190
    public void setTranslationForMenu(String id, String label) {
191
        checkNulls(id, label);
73 ilm 192
        synchronized (this.trMutex) {
193
            this.menuTranslation.put(id, label);
194
        }
67 ilm 195
    }
196
 
177 ilm 197
    // Items labels in panels
67 ilm 198
 
199
    public String getTranslationForItem(String id) {
73 ilm 200
        synchronized (this.trMutex) {
201
            return this.itemTranslation.get(id);
202
        }
67 ilm 203
    }
204
 
205
    public void setTranslationForItem(String id, String label) {
206
        checkNulls(id, label);
73 ilm 207
        synchronized (this.trMutex) {
208
            this.itemTranslation.put(id, label);
209
        }
67 ilm 210
    }
211
 
177 ilm 212
    // Actions (buttons or contextual menus)
67 ilm 213
 
214
    public String getTranslationForAction(String id) {
73 ilm 215
        synchronized (this.trMutex) {
216
            return this.actionTranslation.get(id);
217
        }
67 ilm 218
    }
219
 
220
    public void setTranslationForAction(String id, String label) {
221
        checkNulls(id, label);
73 ilm 222
        synchronized (this.trMutex) {
223
            this.actionTranslation.put(id, label);
224
        }
67 ilm 225
    }
226
 
177 ilm 227
    private void setTranslations(final Collection<?> classes, final boolean warn) {
73 ilm 228
        synchronized (this.trMutex) {
174 ilm 229
            this.menuTranslation = new HashMap<>();
230
            this.itemTranslation = new HashMap<>();
231
            this.actionTranslation = new HashMap<>();
177 ilm 232
            if (warn && classes.isEmpty()) {
73 ilm 233
                Log.get().warning("TranslationManager has no resources to load (" + this.getLocale() + ")");
234
            }
177 ilm 235
            for (Object o : classes) {
236
                this.addTranslations(o, warn);
73 ilm 237
            }
67 ilm 238
        }
239
    }
240
 
177 ilm 241
    private void addTranslations(final Object o, final boolean warn) {
242
        if (o instanceof Class) {
243
            final Class<?> c = (Class<?>) o;
244
            boolean loaded = loadTranslation(this.getLocale(), c);
245
            if (warn && !loaded) {
246
                Log.get().warning("TranslationManager was unable to load translation " + c.getCanonicalName() + " for locale " + this.getLocale());
247
            }
248
        } else {
249
            final Loader loader = (Loader) o;
250
            loader.accept(this);
251
        }
252
    }
253
 
67 ilm 254
    // return all existing (e.g fr_CA only specify differences with fr)
177 ilm 255
    private List<String> findResources(final Locale locale, final Class<?> c, final boolean rootLast) {
67 ilm 256
        final Control cntrl = CONTROL;
177 ilm 257
        final List<String> res = new ArrayList<>();
67 ilm 258
        final String baseName = c.getPackage().getName() + "." + BASENAME;
259
 
260
        // test emptiness to not mix languages
261
        for (Locale targetLocale = locale; targetLocale != null && res.isEmpty(); targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
262
            for (final Locale candidate : cntrl.getCandidateLocales(baseName, targetLocale)) {
177 ilm 263
                res.add(cntrl.toResourceName(cntrl.toBundleName(baseName, candidate), "xml"));
67 ilm 264
            }
265
        }
266
        if (!rootLast)
267
            Collections.reverse(res);
268
        return res;
269
    }
270
 
177 ilm 271
    private boolean loadTranslation(final Locale l, final Class<?> c) {
83 ilm 272
        boolean translationLoaded = false;
67 ilm 273
 
174 ilm 274
        // FIXME : l'implementation de Java est lente
275
        // com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl : 60 ms!
276
        // On pourrait passer à 1ms avec Piccolo...
67 ilm 277
        final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
177 ilm 278
        final DocumentBuilder dBuilder;
67 ilm 279
        try {
174 ilm 280
            dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
281
            dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
177 ilm 282
            dBuilder = dbFactory.newDocumentBuilder();
283
        } catch (ParserConfigurationException e) {
284
            throw new IllegalStateException("Couldn't create DocumentBuilder", e);
285
        }
286
 
287
        // we want more specific translations to replace general ones, i.e. root Locale first
288
        for (final String rsrcName : findResources(l, c, false)) {
289
            // create new instances to check if there's no duplicates in each resource
290
            final Map<String, String> menuTranslation = new HashMap<>(), itemTranslation = new HashMap<>(), actionTranslation = new HashMap<>();
291
            try (final InputStream input = c.getClassLoader().getResourceAsStream(rsrcName)) {
292
                if (input == null)
293
                    continue;
294
                final Document doc = dBuilder.parse(input);
295
                // Menus
296
                loadTranslation(doc, "menu", menuTranslation);
297
                // Items (title, labels not related to fields...)
298
                loadTranslation(doc, "item", itemTranslation);
299
                // Actions
300
                loadTranslation(doc, "action", actionTranslation);
301
            } catch (SAXException | IOException e) {
67 ilm 302
                e.printStackTrace();
177 ilm 303
                // don't return to load as much as possible
67 ilm 304
            }
177 ilm 305
            // on the other hand, it's OK for one resource to override another
306
            this.menuTranslation.putAll(menuTranslation);
307
            this.itemTranslation.putAll(itemTranslation);
308
            this.actionTranslation.putAll(actionTranslation);
309
            translationLoaded = true;
67 ilm 310
        }
177 ilm 311
        return translationLoaded;
67 ilm 312
    }
313
 
174 ilm 314
    private static void loadTranslation(final Document doc, final String tagName, final Map<String, String> m) {
67 ilm 315
        final NodeList menuChildren = doc.getElementsByTagName(tagName);
316
        final int size = menuChildren.getLength();
317
        for (int i = 0; i < size; i++) {
318
            final Element element = (Element) menuChildren.item(i);
319
            final String id = element.getAttributeNode("id").getValue();
320
            final String label = element.getAttributeNode("label").getValue();
321
            if (m.containsKey(id)) {
322
                throw new IllegalStateException("Duplicate " + tagName + " translation entry for " + id + " (" + label + ")");
323
            }
324
            m.put(id, label);
325
        }
326
    }
327
}