1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.xnap.commons.i18n;
22
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.util.Locale;
26 import java.util.MissingResourceException;
27 import java.util.Properties;
28
29 /**
30 * Factory class that creates and caches I18n instances.
31 * <p>
32 * Given a {@link Class} object the factory looks up the resource bundle
33 * responsible for handling message translations. The bundle is returned with an
34 * {@link I18n} object wrapped around it, which provides the translation
35 * methods. The lookup is described at {@link #getI18n(Class,String)}.
36 * <p>
37 * Use the factory for creating <code>I18n</code> objects to make sure no
38 * extraneous objects are created.
39 *
40 * @author Felix Berger
41 * @author Tammo van Lessen
42 * @author Steffen Pingel
43 * @since 0.9
44 */
45 public class I18nFactory {
46
47 private static final String BASENAME_KEY = "basename";
48
49 /**
50 * Use the default configuration.
51 *
52 * @since 0.9.1
53 */
54 public static final int DEFAULT = 0;
55 /**
56 * Fall back to a default resource bundle that returns the passed text if no
57 * resource bundle can be located.
58 *
59 * @since 0.9.1
60 */
61 public static final int FALLBACK = 1 << 0;
62 /**
63 * Look for files named {@link #PROPS_FILENAME} to determine the basename.
64 *
65 * @since 0.9.1
66 */
67 public static final int READ_PROPERTIES = 2 << 0;
68 /**
69 * Do not cache {@link I18n} instance.
70 *
71 * @since 0.9.1
72 */
73 public static final int NO_CACHE = 4 << 0;
74
75 /**
76 * Default name for Message bundles, is "i18n.Messages".
77 *
78 * @since 0.9.1
79 */
80 public static final String DEFAULT_BASE_NAME = "i18n.Messages";
81
82 /**
83 * Filename of the properties file that contains the i18n properties, is
84 * "i18n.properties".
85 *
86 * @since 0.9
87 */
88 public static final String PROPS_FILENAME = "i18n.properties";
89
90 private static final I18nCache i18nCache = new I18nCache();
91
92 private I18nFactory()
93 {
94 }
95
96 /**
97 * Clears the cache of i18n objects. Used by the test classes.
98 */
99 static void clearCache()
100 {
101 i18nCache.visit(new I18nCache.Visitor() {
102
103 public void visit(I18n i18n)
104 {
105 I18nManager.getInstance().remove(i18n);
106 }
107 });
108 i18nCache.clear();
109 }
110
111 /**
112 * Calls {@link #getI18n(Class, Locale) getI18n(clazz, Locale.getDefault())}.
113 */
114 public static I18n getI18n(final Class clazz)
115 {
116 return getI18n(clazz, Locale.getDefault());
117 }
118
119 /**
120 * Calls {@link #getI18n(Class, Locale, int) getI18n(clazz, locale,
121 * READ_PROPERTIES)}.
122 *
123 * @since 0.9.1
124 */
125 public static I18n getI18n(final Class clazz, final Locale locale)
126 {
127 return getI18n(clazz, locale, READ_PROPERTIES);
128 }
129
130 /**
131 * Returns the I18n instance responsible for translating messages in the
132 * package specified by <code>clazz</code>.
133 * <p>
134 * Lookup works by iterating upwards in the package hierarchy: First the
135 * internal cache is asked for an I18n object for a package, otherwise the
136 * algorithm looks for an <code>i18n.properties</code> file in the
137 * package. The properties file is queried for a key named
138 * <code>basename</code> whose value should be the fully qualified
139 * resource/class name of the resource bundle, e.g
140 * <code>org.xnap.commons.i18n.Messages</code>.
141 * <p>
142 * If after the first iteration no I18n instance has been found, a second
143 * search begins by looking for resource bundles having the name
144 * <code>baseName</code>.
145 *
146 * @param clazz
147 * the package hierarchy of the clazz and its class loader are
148 * used for resolving and loading the resource bundle
149 * @param baseName
150 * the name of the underlying resource bundle
151 * @param locale
152 * the locale of the underlying resource bundle
153 * @param flags
154 * a combination of these configuration flags: {@link #FALLBACK}
155 * @return created or cached <code>I18n</code> instance
156 * @throws MissingResourceException
157 * if no resource bundle was found
158 * @since 0.9.1
159 */
160 public static I18n getI18n(final Class clazz, final Locale locale, final int flags)
161 {
162 ClassLoader classLoader = getClassLoader(clazz.getClassLoader());
163
164 String bundleName = null;
165 if (isReadPropertiesSet(flags)) {
166 String path = clazz.getName();
167 int index;
168 do {
169 index = path.lastIndexOf('.');
170 path = (index != -1) ? path.substring(0, index) : "";
171 bundleName = readFromPropertiesFile(path, locale, classLoader);
172 }
173 while (bundleName == null && index != -1);
174 }
175
176 if (bundleName == null) {
177 bundleName = DEFAULT_BASE_NAME;
178 }
179
180 return getI18n("", bundleName, classLoader, locale, flags);
181 }
182
183 /**
184 * Calls
185 * {@link #getI18n(Class, String, Locale) getI18n(clazz, bundleName, Locale.getDefault())}.
186 *
187 * @since 0.9
188 */
189 public static I18n getI18n(final Class clazz, final String bundleName)
190 {
191 return getI18n(clazz, bundleName, Locale.getDefault());
192 }
193
194 /**
195 * Calls
196 * {@link #getI18n(Class, String, Locale, int) getI18n(clazz, bundleName, locale, DEFAULT)}.
197 *
198 * @since 0.9.1
199 */
200 public static I18n getI18n(final Class clazz, final String bundleName, final Locale locale)
201 {
202 return getI18n(clazz, bundleName, locale, DEFAULT);
203 }
204
205 /**
206 * Calls
207 * {@link #getI18n(Class, String, Locale) getI18n(getPackageName(clazz), bundleName, clazz.getClassLoader(), locale, DEFAULT)}.
208 *
209 * @since 0.9.1
210 */
211 public static I18n getI18n(final Class clazz, final String bundleName, final Locale locale, int flags)
212 {
213 return getI18n(clazz.getName(), bundleName, clazz.getClassLoader(), locale, flags);
214 }
215
216 /**
217 * @since 0.9.1
218 */
219 public static I18n getI18n(final String path, final String bundleName, final ClassLoader classLoader, final Locale locale,
220 final int flags)
221 {
222 int index;
223 String prefix = path;
224 do {
225
226 index = prefix.lastIndexOf('.');
227 prefix = (index != -1) ? prefix.substring(0, index) : "";
228 String name = prefix.length() == 0 ? bundleName : prefix + "." + bundleName;
229
230
231 I18n i18n = i18nCache.get(name, locale);
232 if (i18n != null) {
233 return i18n;
234 }
235
236
237 i18n = findByBaseName(name, locale, getClassLoader(classLoader), flags);
238 if (i18n != null) {
239 if ((flags & NO_CACHE) == 0) {
240 i18nCache.put(name, i18n);
241 }
242 return i18n;
243 }
244 }
245 while (index != -1);
246
247
248 if (isFallbackSet(flags)) {
249 I18n i18n = i18nCache.get("", locale);
250 if (i18n == null) {
251 i18n = new I18n(new EmptyResourceBundle(locale));
252 i18nCache.put("", i18n);
253 }
254 return i18n;
255 }
256
257 throw new MissingResourceException("Resource bundle not found", path, bundleName);
258 }
259
260 static ClassLoader getClassLoader(ClassLoader classLoader) {
261 return (classLoader != null) ? classLoader : ClassLoader.getSystemClassLoader();
262 }
263
264 /**
265 * Tries to create an I18n instance from a properties file.
266 *
267 * @param path
268 * @param loader
269 * @return null if no properties file was found
270 * @throws MissingResourceException
271 * if properties file was found but specified resource not
272 */
273 static String readFromPropertiesFile(final String path, final Locale locale, final ClassLoader loader)
274 {
275 Properties props = new Properties();
276 String filename = path.length() == 0 ? PROPS_FILENAME : path.replace('.', '/') + "/" + PROPS_FILENAME;
277 InputStream in = loader.getResourceAsStream(filename);
278 if (in != null) {
279 try {
280 props.load(in);
281 }
282 catch (IOException e) {
283
284 }
285 finally {
286 try {
287 in.close();
288 }
289 catch (IOException e) {
290
291 }
292 }
293 return props.getProperty(BASENAME_KEY);
294 }
295 return null;
296 }
297
298 /**
299 * Uses the class loader to look for a messages properties file.
300 *
301 * @param baseName
302 * the base name of the resource bundle
303 * @param path
304 * the path that prefixes baseName
305 * @param loader
306 * the class loader used to look up the bundle
307 * @param flags
308 * @return the created instance
309 */
310 static I18n findByBaseName(final String baseName, final Locale locale, final ClassLoader loader, int flags)
311 {
312 try {
313 return createI18n(baseName, locale, loader, flags);
314 }
315 catch (MissingResourceException e) {
316 return null;
317 }
318 }
319
320 /**
321 * Creates a new i18n instance and registers it with {@link I18nManager}.
322 *
323 * @param baseName
324 * the base name of the resource bundle
325 * @param locale
326 * the locale
327 * @param loader
328 * the class loader used to look up the bundle
329 * @return the created instance
330 */
331 private static I18n createI18n(final String baseName, final Locale locale, final ClassLoader loader, final int flags)
332 {
333 I18n i18n = new I18n(baseName, locale, loader);
334 if (!isNoCacheSet(flags)) {
335 I18nManager.getInstance().add(i18n);
336 }
337 return i18n;
338 }
339
340 private static boolean isFallbackSet(final int flags)
341 {
342 return (flags & FALLBACK) != 0;
343 }
344
345 private static boolean isReadPropertiesSet(final int flags)
346 {
347 return (flags & READ_PROPERTIES) != 0;
348 }
349
350 private static boolean isNoCacheSet(final int flags)
351 {
352 return (flags & NO_CACHE) != 0;
353 }
354
355 }