View Javadoc

1   /*
2    *  Gettext Commons
3    *
4    *  Copyright (C) 2005  Felix Berger
5    *  Copyright (C) 2005  Steffen Pingel
6    *
7    *  This library is free software; you can redistribute it and/or
8    *  modify it under the terms of the GNU Lesser General Public
9    *  License as published by the Free Software Foundation; either
10   *  version 2.1 of the License, or (at your option) any later version.
11   *
12   *  This library is distributed in the hope that it will be useful,
13   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   *  Lesser General Public License for more details.
16   *
17   *  You should have received a copy of the GNU Lesser General Public
18   *  License along with this library; if not, write to the Free Software
19   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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 			// chop of last segment of path
226 			index = prefix.lastIndexOf('.');
227 			prefix = (index != -1) ? prefix.substring(0, index) : "";
228 			String name = prefix.length() == 0 ? bundleName : prefix + "." + bundleName;
229 			
230 			// check cache
231 			I18n i18n = i18nCache.get(name, locale);
232 			if (i18n != null) {
233 				return i18n;
234 			}
235 			
236 			// look for resource bundle in class path
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 		// fallback to default bundle
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 				// XXX now what?
284 			}
285 			finally {
286 				try {
287 					in.close();
288 				}
289 				catch (IOException e) {
290 					// this exception is lost
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 }