OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 174 Rev 177
Line 12... Line 12...
12
 */
12
 */
13
 
13
 
14
 package org.openconcerto.utils.i18n;
14
 package org.openconcerto.utils.i18n;
15
 
15
 
16
import org.openconcerto.utils.Log;
16
import org.openconcerto.utils.Log;
-
 
17
import org.openconcerto.utils.i18n.TranslationPool.Mode;
17
 
18
 
18
import java.io.IOException;
19
import java.io.IOException;
19
import java.io.InputStream;
20
import java.io.InputStream;
20
import java.util.ArrayList;
21
import java.util.ArrayList;
-
 
22
import java.util.Collection;
21
import java.util.Collections;
23
import java.util.Collections;
22
import java.util.HashMap;
24
import java.util.HashMap;
23
import java.util.List;
25
import java.util.List;
24
import java.util.Locale;
26
import java.util.Locale;
25
import java.util.Map;
27
import java.util.Map;
26
import java.util.ResourceBundle.Control;
28
import java.util.ResourceBundle.Control;
-
 
29
import java.util.function.Consumer;
27
 
30
 
28
import javax.xml.XMLConstants;
31
import javax.xml.XMLConstants;
29
import javax.xml.parsers.DocumentBuilder;
32
import javax.xml.parsers.DocumentBuilder;
30
import javax.xml.parsers.DocumentBuilderFactory;
33
import javax.xml.parsers.DocumentBuilderFactory;
-
 
34
import javax.xml.parsers.ParserConfigurationException;
31
 
35
 
32
import org.w3c.dom.Document;
36
import org.w3c.dom.Document;
33
import org.w3c.dom.Element;
37
import org.w3c.dom.Element;
34
import org.w3c.dom.NodeList;
38
import org.w3c.dom.NodeList;
-
 
39
import org.xml.sax.SAXException;
35
 
40
 
36
import net.jcip.annotations.GuardedBy;
41
import net.jcip.annotations.GuardedBy;
37
import net.jcip.annotations.ThreadSafe;
42
import net.jcip.annotations.ThreadSafe;
38
 
43
 
39
@ThreadSafe
44
@ThreadSafe
40
public class TranslationManager {
45
public class TranslationManager {
-
 
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
 
41
    private static final Locale FALLBACK_LOCALE = Locale.ENGLISH;
56
    private static final Locale FALLBACK_LOCALE = Locale.ENGLISH;
42
 
57
 
43
    private static final Control CONTROL = new I18nUtils.SameLanguageControl() {
58
    private static final Control CONTROL = new I18nUtils.SameLanguageControl() {
44
        @Override
59
        @Override
45
        public Locale getFallbackLocale(String baseName, Locale locale) {
60
        public Locale getFallbackLocale(String baseName, Locale locale) {
Line 51... Line 66...
51
 
66
 
52
    public static final Control getControl() {
67
    public static final Control getControl() {
53
        return CONTROL;
68
        return CONTROL;
54
    }
69
    }
55
 
70
 
56
    private static final String BASENAME = "translation";
-
 
57
    private static final TranslationManager instance = new TranslationManager();
71
    static public interface Loader extends Consumer<TranslationManager> {
-
 
72
    }
58
 
73
 
-
 
74
    private static final String BASENAME = "translation";
-
 
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;
59
    public static final TranslationManager getInstance() {
82
            final TranslationManager res = new TranslationManager(l);
-
 
83
            res.setTranslations(classes, true);
60
        return instance;
84
            return res;
61
    }
85
        }
-
 
86
    };
62
 
87
 
63
    @GuardedBy("classes")
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
     * 
64
    private final List<Class<?>> classes;
92
     * @return the instance for the default locale, <code>null</code> if
-
 
93
     *         {@link #addTranslationStreamFromClass(Class)} wasn't called.
65
    @GuardedBy("classes")
94
     */
66
    private Locale locale;
95
    public static final TranslationManager createDefaultInstance() {
-
 
96
        return POOL.get(TM.getDefaultLocale(), Mode.OPTIONAL_CREATE);
-
 
97
    }
67
 
98
 
68
    private final Object trMutex = new Object();
99
    public static final TranslationManager getInstance() {
69
    @GuardedBy("trMutex")
-
 
70
    private Map<String, String> menuTranslation;
-
 
71
    @GuardedBy("trMutex")
-
 
72
    private Map<String, String> itemTranslation;
100
        return getInstance(TM.getDefaultLocale());
73
    @GuardedBy("trMutex")
101
    }
74
    private Map<String, String> actionTranslation;
-
 
75
 
102
 
-
 
103
    // TODO throw exception
76
    private TranslationManager() {
104
    public static final TranslationManager createInstance(final Locale l) {
77
        this.classes = new ArrayList<>();
105
        return POOL.get(l);
78
    }
106
    }
79
 
107
 
-
 
108
    /**
-
 
109
     * Get an instance already created by {@link #createInstance(Locale)}. This method won't block,
80
    public void addTranslationStreamFromClass(Class<?> c) {
110
     * that's what createInstance() is for.
-
 
111
     * 
81
        synchronized (this.classes) {
112
     * @param l the locale
82
            this.classes.add(c);
113
     * @return the cached instance.
-
 
114
     */
83
            if (this.getLocale() != null) {
115
    public static final TranslationManager getInstance(final Locale l) {
84
                loadTranslation(this.getLocale(), c);
116
        return POOL.getCreated(l);
85
            }
117
    }
-
 
118
 
-
 
119
    @GuardedBy("POOL")
-
 
120
    private static final List<Object> classes = new ArrayList<>();
-
 
121
 
-
 
122
    public static final void addTranslationStreamFromClass(Class<?> c) {
-
 
123
        _addLoader(c);
86
        }
124
    }
-
 
125
 
-
 
126
    public static final void addLoader(final Loader loader) {
-
 
127
        _addLoader(loader);
87
    }
128
    }
88
 
129
 
89
    public void removeTranslationStreamFromClass(Class<?> c) {
130
    private static final void _addLoader(final Object loader) {
90
        synchronized (this.classes) {
131
        synchronized (POOL) {
-
 
132
            classes.add(loader);
91
            if (this.classes.remove(c) && this.getLocale() != null) {
133
            for (final TranslationManager tm : POOL.getCreated()) {
92
                loadAllTranslation();
134
                tm.addTranslations(loader, true);
93
            }
135
            }
94
        }
136
        }
95
    }
137
    }
96
 
138
 
97
    public final Locale getLocale() {
139
    public static final void removeTranslationStreamFromClass(Class<?> c) {
98
        synchronized (this.classes) {
-
 
99
            return this.locale;
140
        _removeLoader(c);
100
        }
141
    }
-
 
142
 
-
 
143
    public static final void removeLoader(final Loader loader) {
-
 
144
        _removeLoader(loader);
101
    }
145
    }
102
 
146
 
103
    public final void setLocale(Locale l) {
147
    private static final void _removeLoader(final Object o) {
104
        if (l == null)
148
        synchronized (POOL) {
105
            throw new NullPointerException("null Locale");
-
 
106
        synchronized (this.classes) {
149
            if (classes.remove(o)) {
107
            if (!l.equals(this.locale)) {
150
                for (final TranslationManager tm : POOL.getCreated()) {
108
                this.locale = l;
151
                    tm.setTranslations(classes, false);
109
                loadAllTranslation();
152
                }
110
            }
153
            }
111
        }
154
        }
112
    }
155
    }
113
 
156
 
-
 
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
 
114
    private void checkNulls(String id, String label) {
175
    private void checkNulls(String id, String label) {
115
        if (id == null)
176
        if (id == null)
116
            throw new NullPointerException("null id");
177
            throw new NullPointerException("null id");
117
        if (label == null)
178
        if (label == null)
118
            throw new NullPointerException("null label");
179
            throw new NullPointerException("null label");
119
    }
180
    }
120
 
181
 
121
    // Menus
182
    // Menus in menu bar and menu items
122
 
183
 
123
    public String getTranslationForMenu(String id) {
184
    public String getTranslationForMenu(String id) {
124
        synchronized (this.trMutex) {
185
        synchronized (this.trMutex) {
125
            return this.menuTranslation.get(id);
186
            return this.menuTranslation.get(id);
126
        }
187
        }
Line 131... Line 192...
131
        synchronized (this.trMutex) {
192
        synchronized (this.trMutex) {
132
            this.menuTranslation.put(id, label);
193
            this.menuTranslation.put(id, label);
133
        }
194
        }
134
    }
195
    }
135
 
196
 
136
    // Items
197
    // Items labels in panels
137
 
198
 
138
    public String getTranslationForItem(String id) {
199
    public String getTranslationForItem(String id) {
139
        synchronized (this.trMutex) {
200
        synchronized (this.trMutex) {
140
            return this.itemTranslation.get(id);
201
            return this.itemTranslation.get(id);
141
        }
202
        }
Line 146... Line 207...
146
        synchronized (this.trMutex) {
207
        synchronized (this.trMutex) {
147
            this.itemTranslation.put(id, label);
208
            this.itemTranslation.put(id, label);
148
        }
209
        }
149
    }
210
    }
150
 
211
 
151
    // Actions
212
    // Actions (buttons or contextual menus)
152
 
213
 
153
    public String getTranslationForAction(String id) {
214
    public String getTranslationForAction(String id) {
154
        synchronized (this.trMutex) {
215
        synchronized (this.trMutex) {
155
            return this.actionTranslation.get(id);
216
            return this.actionTranslation.get(id);
156
        }
217
        }
Line 161... Line 222...
161
        synchronized (this.trMutex) {
222
        synchronized (this.trMutex) {
162
            this.actionTranslation.put(id, label);
223
            this.actionTranslation.put(id, label);
163
        }
224
        }
164
    }
225
    }
165
 
226
 
166
    private void loadAllTranslation() {
227
    private void setTranslations(final Collection<?> classes, final boolean warn) {
167
        synchronized (this.trMutex) {
228
        synchronized (this.trMutex) {
168
            this.menuTranslation = new HashMap<>();
229
            this.menuTranslation = new HashMap<>();
169
            this.itemTranslation = new HashMap<>();
230
            this.itemTranslation = new HashMap<>();
170
            this.actionTranslation = new HashMap<>();
231
            this.actionTranslation = new HashMap<>();
171
            if (this.classes.isEmpty()) {
232
            if (warn && classes.isEmpty()) {
172
                Log.get().warning("TranslationManager has no resources to load (" + this.getLocale() + ")");
233
                Log.get().warning("TranslationManager has no resources to load (" + this.getLocale() + ")");
173
            }
234
            }
174
            for (Class<?> c : this.classes) {
235
            for (Object o : classes) {
-
 
236
                this.addTranslations(o, warn);
-
 
237
            }
-
 
238
        }
-
 
239
    }
-
 
240
 
-
 
241
    private void addTranslations(final Object o, final boolean warn) {
-
 
242
        if (o instanceof Class) {
-
 
243
            final Class<?> c = (Class<?>) o;
175
                boolean loaded = loadTranslation(this.getLocale(), c);
244
            boolean loaded = loadTranslation(this.getLocale(), c);
176
                if (!loaded) {
245
            if (warn && !loaded) {
177
                    Log.get().warning("TranslationManager was unable to load translation " + c.getCanonicalName() + " for locale " + this.getLocale());
246
                Log.get().warning("TranslationManager was unable to load translation " + c.getCanonicalName() + " for locale " + this.getLocale());
178
                }
247
            }
179
            }
248
        } else {
-
 
249
            final Loader loader = (Loader) o;
-
 
250
            loader.accept(this);
180
        }
251
        }
181
    }
252
    }
182
 
253
 
183
    // return all existing (e.g fr_CA only specify differences with fr)
254
    // return all existing (e.g fr_CA only specify differences with fr)
184
    private List<InputStream> findStream(final Locale locale, final Class<?> c, final boolean rootLast) {
255
    private List<String> findResources(final Locale locale, final Class<?> c, final boolean rootLast) {
185
        final Control cntrl = CONTROL;
256
        final Control cntrl = CONTROL;
186
        final List<InputStream> res = new ArrayList<>();
257
        final List<String> res = new ArrayList<>();
187
        final String baseName = c.getPackage().getName() + "." + BASENAME;
258
        final String baseName = c.getPackage().getName() + "." + BASENAME;
188
 
259
 
189
        // test emptiness to not mix languages
260
        // test emptiness to not mix languages
190
        for (Locale targetLocale = locale; targetLocale != null && res.isEmpty(); targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
261
        for (Locale targetLocale = locale; targetLocale != null && res.isEmpty(); targetLocale = cntrl.getFallbackLocale(baseName, targetLocale)) {
191
            for (final Locale candidate : cntrl.getCandidateLocales(baseName, targetLocale)) {
262
            for (final Locale candidate : cntrl.getCandidateLocales(baseName, targetLocale)) {
192
                final InputStream ins = c.getClassLoader().getResourceAsStream(cntrl.toResourceName(cntrl.toBundleName(baseName, candidate), "xml"));
263
                res.add(cntrl.toResourceName(cntrl.toBundleName(baseName, candidate), "xml"));
193
                if (ins != null)
-
 
194
                    res.add(ins);
-
 
195
            }
264
            }
196
        }
265
        }
197
        if (!rootLast)
266
        if (!rootLast)
198
            Collections.reverse(res);
267
            Collections.reverse(res);
199
        return res;
268
        return res;
200
    }
269
    }
201
 
270
 
202
    private boolean loadTranslation(final Locale l, Class<?> c) {
271
    private boolean loadTranslation(final Locale l, final Class<?> c) {
203
        boolean translationLoaded = false;
272
        boolean translationLoaded = false;
204
        // we want more specific translations to replace general ones, i.e. root Locale first
-
 
205
        for (final InputStream input : findStream(l, c, false)) {
-
 
206
            // create new instances to check if there's no duplicates in each resource
-
 
207
            final Map<String, String> menuTranslation = new HashMap<>(), itemTranslation = new HashMap<>(), actionTranslation = new HashMap<>();
-
 
208
            loadTranslation(input, menuTranslation, itemTranslation, actionTranslation);
-
 
209
            // on the other hand, it's OK for one resource to override another
-
 
210
            this.menuTranslation.putAll(menuTranslation);
-
 
211
            this.itemTranslation.putAll(itemTranslation);
-
 
212
            this.actionTranslation.putAll(actionTranslation);
-
 
213
            translationLoaded = true;
-
 
214
        }
-
 
215
        return translationLoaded;
-
 
216
    }
-
 
217
 
273
 
218
    private static void loadTranslation(final InputStream input, final Map<String, String> menuTranslation, final Map<String, String> itemTranslation, final Map<String, String> actionTranslation) {
-
 
219
        // FIXME : l'implementation de Java est lente
274
        // FIXME : l'implementation de Java est lente
220
        // com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl : 60 ms!
275
        // com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl : 60 ms!
221
        // On pourrait passer à 1ms avec Piccolo...
276
        // On pourrait passer à 1ms avec Piccolo...
222
        final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
277
        final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
-
 
278
        final DocumentBuilder dBuilder;
223
        try {
279
        try {
224
            dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
280
            dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
225
            dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
281
            dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
226
            final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
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;
227
            final Document doc = dBuilder.parse(input);
294
                final Document doc = dBuilder.parse(input);
228
            // Menus
295
                // Menus
229
            loadTranslation(doc, "menu", menuTranslation);
296
                loadTranslation(doc, "menu", menuTranslation);
230
            // Items (title, labels not related to fields...)
297
                // Items (title, labels not related to fields...)
231
            loadTranslation(doc, "item", itemTranslation);
298
                loadTranslation(doc, "item", itemTranslation);
232
            // Actions
299
                // Actions
233
            loadTranslation(doc, "action", actionTranslation);
300
                loadTranslation(doc, "action", actionTranslation);
234
        } catch (Exception e) {
-
 
235
            e.printStackTrace();
-
 
236
        } finally {
-
 
237
            try {
-
 
238
                if (input != null) {
-
 
239
                    input.close();
-
 
240
                }
-
 
241
            } catch (IOException e) {
301
            } catch (SAXException | IOException e) {
242
                e.printStackTrace();
302
                e.printStackTrace();
-
 
303
                // don't return to load as much as possible
243
            }
304
            }
-
 
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;
244
        }
310
        }
-
 
311
        return translationLoaded;
245
    }
312
    }
246
 
313
 
247
    private static void loadTranslation(final Document doc, final String tagName, final Map<String, String> m) {
314
    private static void loadTranslation(final Document doc, final String tagName, final Map<String, String> m) {
248
        final NodeList menuChildren = doc.getElementsByTagName(tagName);
315
        final NodeList menuChildren = doc.getElementsByTagName(tagName);
249
        final int size = menuChildren.getLength();
316
        final int size = menuChildren.getLength();