1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 package org.modeshape.common.i18n;
25
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.Modifier;
30 import java.net.URL;
31 import java.util.Collections;
32 import java.util.HashSet;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Properties;
36 import java.util.Set;
37 import java.util.Map.Entry;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.concurrent.ConcurrentMap;
40 import java.util.concurrent.CopyOnWriteArraySet;
41 import net.jcip.annotations.ThreadSafe;
42 import org.modeshape.common.CommonI18n;
43 import org.modeshape.common.SystemFailureException;
44 import org.modeshape.common.util.CheckArg;
45 import org.modeshape.common.util.ClassUtil;
46 import org.modeshape.common.util.StringUtil;
47
48
49
50
51
52 @ThreadSafe
53 public final class I18n {
54
55 private static final LocalizationRepository DEFAULT_LOCALIZATION_REPOSITORY = new ClasspathLocalizationRepository();
56
57
58
59
60
61 static final ConcurrentMap<Locale, Map<Class<?>, Set<String>>> LOCALE_TO_CLASS_TO_PROBLEMS_MAP = new ConcurrentHashMap<Locale, Map<Class<?>, Set<String>>>();
62
63 private static LocalizationRepository localizationRepository = DEFAULT_LOCALIZATION_REPOSITORY;
64
65
66
67
68
69
70
71
72 public static Set<Locale> getLocalizationProblemLocales( Class<?> i18nClass ) {
73 CheckArg.isNotNull(i18nClass, "i18nClass");
74 Set<Locale> locales = new HashSet<Locale>(LOCALE_TO_CLASS_TO_PROBLEMS_MAP.size());
75 for (Entry<Locale, Map<Class<?>, Set<String>>> localeEntry : LOCALE_TO_CLASS_TO_PROBLEMS_MAP.entrySet()) {
76 for (Entry<Class<?>, Set<String>> classEntry : localeEntry.getValue().entrySet()) {
77 if (!classEntry.getValue().isEmpty()) {
78 locales.add(localeEntry.getKey());
79 break;
80 }
81 }
82 }
83 return locales;
84 }
85
86
87
88
89
90
91
92
93 public static Set<String> getLocalizationProblems( Class<?> i18nClass ) {
94 return getLocalizationProblems(i18nClass, null);
95 }
96
97
98
99
100
101
102
103
104
105
106 public static Set<String> getLocalizationProblems( Class<?> i18nClass,
107 Locale locale ) {
108 CheckArg.isNotNull(i18nClass, "i18nClass");
109 Map<Class<?>, Set<String>> classToProblemsMap = LOCALE_TO_CLASS_TO_PROBLEMS_MAP.get(locale == null ? Locale.getDefault() : locale);
110 if (classToProblemsMap == null) {
111 return Collections.emptySet();
112 }
113 Set<String> problems = classToProblemsMap.get(i18nClass);
114 if (problems == null) {
115 return Collections.emptySet();
116 }
117 return problems;
118 }
119
120
121
122
123
124
125
126 public static LocalizationRepository getLocalizationRepository() {
127 return localizationRepository;
128 }
129
130
131
132
133
134
135
136
137 public static void setLocalizationRepository( LocalizationRepository localizationRepository ) {
138 I18n.localizationRepository = localizationRepository != null ? localizationRepository : DEFAULT_LOCALIZATION_REPOSITORY;
139 }
140
141
142
143
144
145
146
147
148 public static void initialize( Class<?> i18nClass ) {
149 CheckArg.isNotNull(i18nClass, "i18nClass");
150 if (i18nClass.isInterface()) {
151 throw new IllegalArgumentException(CommonI18n.i18nClassInterface.text(i18nClass.getName()));
152 }
153
154 synchronized (i18nClass) {
155
156 try {
157 for (Field fld : i18nClass.getDeclaredFields()) {
158
159
160 if (fld.getType() == I18n.class) {
161
162
163 if ((fld.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
164 throw new SystemFailureException(CommonI18n.i18nFieldNotPublic.text(fld.getName(), i18nClass));
165 }
166
167
168 if ((fld.getModifiers() & Modifier.STATIC) != Modifier.STATIC) {
169 throw new SystemFailureException(CommonI18n.i18nFieldNotStatic.text(fld.getName(), i18nClass));
170 }
171
172
173 if ((fld.getModifiers() & Modifier.FINAL) == Modifier.FINAL) {
174 throw new SystemFailureException(CommonI18n.i18nFieldFinal.text(fld.getName(), i18nClass));
175 }
176
177
178 ClassUtil.makeAccessible(fld);
179
180
181 fld.set(null, new I18n(fld.getName(), i18nClass));
182 }
183 }
184
185
186 for (Entry<Locale, Map<Class<?>, Set<String>>> entry : LOCALE_TO_CLASS_TO_PROBLEMS_MAP.entrySet()) {
187 entry.getValue().remove(i18nClass);
188 }
189 } catch (IllegalAccessException err) {
190
191 throw new IllegalArgumentException(CommonI18n.i18nClassNotPublic.text(i18nClass));
192 }
193 }
194 }
195
196
197
198
199
200
201
202 private static void localize( final Class<?> i18nClass,
203 final Locale locale ) {
204 assert i18nClass != null;
205 assert locale != null;
206
207 Map<Class<?>, Set<String>> classToProblemsMap = new ConcurrentHashMap<Class<?>, Set<String>>();
208 Map<Class<?>, Set<String>> existingClassToProblemsMap = LOCALE_TO_CLASS_TO_PROBLEMS_MAP.putIfAbsent(locale,
209 classToProblemsMap);
210 if (existingClassToProblemsMap != null) {
211 classToProblemsMap = existingClassToProblemsMap;
212 }
213
214 if (classToProblemsMap.get(i18nClass) != null) {
215 return;
216 }
217 synchronized (i18nClass) {
218
219
220 Set<String> problems = classToProblemsMap.get(i18nClass);
221 if (problems == null) {
222 problems = new CopyOnWriteArraySet<String>();
223 classToProblemsMap.put(i18nClass, problems);
224 } else {
225 return;
226 }
227
228 final LocalizationRepository repos = getLocalizationRepository();
229 final String localizationBaseName = i18nClass.getName();
230 URL url = repos.getLocalizationBundle(localizationBaseName, locale);
231 if (url == null) {
232
233 Locale defaultLocale = Locale.getDefault();
234 if (!defaultLocale.equals(locale)) {
235 url = repos.getLocalizationBundle(localizationBaseName, defaultLocale);
236 }
237
238 if (url == null) {
239 problems.add(CommonI18n.i18nLocalizationFileNotFound.text(localizationBaseName));
240 return;
241 }
242 }
243
244 final URL finalUrl = url;
245 final Set<String> finalProblems = problems;
246 Properties props = new Properties() {
247
248
249
250 private static final long serialVersionUID = 3920620306881072843L;
251
252 @Override
253 public synchronized Object put( Object key,
254 Object value ) {
255 String id = (String)key;
256 String text = (String)value;
257
258 try {
259 Field fld = i18nClass.getDeclaredField(id);
260 if (fld.getType() != I18n.class) {
261
262 finalProblems.add(CommonI18n.i18nFieldInvalidType.text(id, finalUrl, getClass().getName()));
263 } else {
264 I18n i18n = (I18n)fld.get(null);
265 if (i18n.localeToTextMap.putIfAbsent(locale, text) != null) {
266
267 String prevProblem = i18n.localeToProblemMap.putIfAbsent(locale,
268 CommonI18n.i18nPropertyDuplicate.text(id,
269 finalUrl));
270 assert prevProblem == null;
271 }
272 }
273 } catch (NoSuchFieldException err) {
274
275 finalProblems.add(CommonI18n.i18nPropertyUnused.text(id, finalUrl));
276 } catch (IllegalAccessException notPossible) {
277
278 finalProblems.add(notPossible.getMessage());
279 }
280
281 return null;
282 }
283 };
284
285 try {
286 InputStream propStream = url.openStream();
287 try {
288 props.load(propStream);
289
290 for (Field fld : i18nClass.getDeclaredFields()) {
291 if (fld.getType() == I18n.class) {
292 try {
293 I18n i18n = (I18n)fld.get(null);
294 if (i18n.localeToTextMap.get(locale) == null) {
295 i18n.localeToProblemMap.put(locale, CommonI18n.i18nPropertyMissing.text(fld.getName(), url));
296 }
297 } catch (IllegalAccessException notPossible) {
298
299 finalProblems.add(notPossible.getMessage());
300 }
301 }
302 }
303 } finally {
304 propStream.close();
305 }
306 } catch (IOException err) {
307 finalProblems.add(err.getMessage());
308 }
309 }
310 }
311
312 private final String id;
313 private final Class<?> i18nClass;
314 final ConcurrentHashMap<Locale, String> localeToTextMap = new ConcurrentHashMap<Locale, String>();
315 final ConcurrentHashMap<Locale, String> localeToProblemMap = new ConcurrentHashMap<Locale, String>();
316
317 private I18n( String id,
318 Class<?> i18nClass ) {
319 this.id = id;
320 this.i18nClass = i18nClass;
321 }
322
323
324
325
326
327 public String id() {
328 return id;
329 }
330
331
332
333
334
335 public boolean hasProblem() {
336 return (problem() != null);
337 }
338
339
340
341
342
343
344 public boolean hasProblem( Locale locale ) {
345 return (problem(locale) != null);
346 }
347
348
349
350
351
352 public String problem() {
353 return problem(null);
354 }
355
356
357
358
359
360
361 public String problem( Locale locale ) {
362 if (locale == null) {
363 locale = Locale.getDefault();
364 }
365 localize(i18nClass, locale);
366
367 String problem = localeToProblemMap.get(locale);
368 if (problem != null) {
369 return problem;
370 }
371
372 if (localeToTextMap.get(locale) != null) {
373
374 return null;
375 }
376
377
378 problem = CommonI18n.i18nLocalizationProblems.text(i18nClass, locale);
379 localeToProblemMap.put(locale, problem);
380 return problem;
381 }
382
383 private String rawText( Locale locale ) {
384 assert locale != null;
385 localize(i18nClass, locale);
386
387 String text = localeToTextMap.get(locale);
388 if (text != null) {
389 return text;
390 }
391
392
393 throw new SystemFailureException(problem(locale));
394 }
395
396
397
398
399
400
401
402
403 public String text( Object... arguments ) {
404 return text(null, arguments);
405 }
406
407
408
409
410
411
412
413
414 public String text( Locale locale,
415 Object... arguments ) {
416 try {
417 String rawText = rawText(locale == null ? Locale.getDefault() : locale);
418 return StringUtil.createString(rawText, arguments);
419 } catch (IllegalArgumentException err) {
420 throw new IllegalArgumentException(CommonI18n.i18nRequiredToSuppliedParameterMismatch.text(id,
421 i18nClass,
422 err.getMessage()));
423 } catch (SystemFailureException err) {
424 return '<' + err.getMessage() + '>';
425 }
426 }
427
428
429
430
431 @Override
432 public String toString() {
433 try {
434 return rawText(Locale.getDefault());
435 } catch (SystemFailureException err) {
436 return '<' + err.getMessage() + '>';
437 }
438 }
439 }