View Javadoc

1   /*
2    * FCKeditor - The text editor for Internet - http://www.fckeditor.net
3    * Copyright (C) 2004-2010 Frederico Caldeira Knabben
4    * 
5    * == BEGIN LICENSE ==
6    * 
7    * Licensed under the terms of any of the following licenses at your
8    * choice:
9    * 
10   *  - GNU General Public License Version 2 or later (the "GPL")
11   *    http://www.gnu.org/licenses/gpl.html
12   * 
13   *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
14   *    http://www.gnu.org/licenses/lgpl.html
15   * 
16   *  - Mozilla Public License Version 1.1 or later (the "MPL")
17   *    http://www.mozilla.org/MPL/MPL-1.1.html
18   * 
19   * == END LICENSE ==
20   */
21  package net.fckeditor.localization;
22  
23  import java.io.BufferedInputStream;
24  import java.io.InputStream;
25  import java.text.MessageFormat;
26  import java.util.Collections;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.ResourceBundle;
33  
34  import javax.servlet.http.HttpServletRequest;
35  
36  import net.fckeditor.handlers.PropertiesLoader;
37  import net.fckeditor.tool.Utils;
38  
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Provides access to localized messages (properties).
44   * <p>
45   * Localized messages are loaded for a particular locale from a HTTP request.
46   * The locale is resolved by the current {@link LocaleResolver}
47   * instance/singleton. If a locale or a bundle for a locale cannot be found,
48   * default messages are used.
49   * </p>
50   * <p>
51   * Note: Loaded messages are cached per locale, any subsequent call of the same
52   * locale will be served by the cache instead of another resource bundle
53   * retrieval.
54   * </p>
55   * 
56   * @version $Id: LocalizedMessages.java 4785 2009-12-21 20:10:28Z mosipov $
57   */
58  public class LocalizedMessages {
59  
60  	private static final Map<Locale, LocalizedMessages> prototypes = Collections
61  			.synchronizedMap(new HashMap<Locale, LocalizedMessages>());
62  	private static final String DEFAULT_FILENAME = "default_messages.properties"; //$NON-NLS-1$
63  	private static final String LOCAL_PROPERTIES = "fckeditor_messages"; //$NON-NLS-1$
64  	private static final Properties defaultProperties = new Properties();
65  	
66  	private Properties properties;
67  	private static LocaleResolver localeResolver;
68  	private static final Locale NEUTRAL = new Locale(Utils.EMPTY_STRING); //$NON-NLS-1$
69  	private static final Logger logger = LoggerFactory
70  			.getLogger(LocalizedMessages.class);
71  
72  	static {
73  
74  		InputStream in = LocalizedMessages.class
75  				.getResourceAsStream(DEFAULT_FILENAME);
76  
77  		if (in == null) {
78  			logger.error("{} not found", DEFAULT_FILENAME); //$NON-NLS-1$
79  			throw new RuntimeException(DEFAULT_FILENAME + " not found"); //$NON-NLS-1$
80  		} else {
81  			if (!(in instanceof BufferedInputStream))
82  				in = new BufferedInputStream(in);
83  
84  			try {
85  				defaultProperties.load(in);
86  				in.close();
87  				logger.debug("{} loaded", DEFAULT_FILENAME); //$NON-NLS-1$
88  			} catch (Exception e) {
89  				logger.error("Error while loading {}", DEFAULT_FILENAME); //$NON-NLS-1$
90  				throw new RuntimeException(
91  						"Error while loading " + DEFAULT_FILENAME, e); //$NON-NLS-1$
92  			}
93  		}
94  	}
95  
96  	/**
97  	 * Returns an instance of <code>LocalizedMessages</code> for a given
98  	 * request. This method automatically determines the locale of this request
99  	 * and loads the appropriate bundle. If locale is null or not available,
100 	 * the default locale will be used.
101 	 * 
102 	 * @param request
103 	 *            the current request instance
104 	 * @return instance with localized messages
105 	 */
106 	public static LocalizedMessages getInstance(HttpServletRequest request) {
107 
108 		if (request == null)
109 			throw new NullPointerException("the request cannot be null");
110 		
111 		Locale locale = getLocaleResolverInstance().resolveLocale(request);
112 		
113 		if (locale == null)
114 			locale = NEUTRAL;
115 		
116 		synchronized (LocalizedMessages.class) {
117 			if (!prototypes.containsKey(locale))
118 				prototypes.put(locale, new LocalizedMessages(locale));
119 		}
120 		
121 		// for now we don't need any cloning since the values are accessed
122 		// read-only
123 		return prototypes.get(locale);
124 
125 	}
126 
127 	/**
128 	 * Returns the locale resolver instance. The implementation class name is
129 	 * provided by {@link PropertiesLoader#getLocaleResolverImpl()}.
130 	 * 
131 	 * @return the locale resolver instance
132 	 */
133 	private synchronized static LocaleResolver getLocaleResolverInstance() {
134 
135 		if (localeResolver == null) {
136 			String className = PropertiesLoader.getLocaleResolverImpl();
137 
138 			if (Utils.isEmpty(className))
139 				logger.error("Empty LocaleResolver implementation class name provided"); //$NON-NLS-1$
140 			else
141 				try {
142 					Class<?> clazz = Class.forName(className);
143 					localeResolver = (LocaleResolver) clazz.newInstance();
144 					logger.info("LocaleResolver initialized to {}", className); //$NON-NLS-1$
145 				} catch (Throwable e) {
146 					logger.error("LocaleResolver implementation {} could not be instantiated", className); //$NON-NLS-1$
147 					throw new RuntimeException("LocaleResolver implementation " + className + " could not be instantiated", e); //$NON-NLS-1$
148 				}
149 		}
150 
151 		return localeResolver;
152 	}
153 
154 	/**
155 	 * Loads the localized messages for the given locale. This constructor loads
156 	 * the resource bundle for this locale and only for this, in other words it
157 	 * short-circuits the default resource bundle load mechanism in order to
158 	 * prevent the loading of the system default locale which may result in a
159 	 * completely different resource bundle.
160 	 * 
161 	 * @param locale
162 	 *            the locale of the new localized messages
163 	 */
164 	private LocalizedMessages(Locale locale) {
165 
166 		properties = new Properties(defaultProperties);
167 
168 		ResourceBundle localized = null;
169 		try {
170 			localized = ResourceBundle.getBundle(LOCAL_PROPERTIES, locale,
171 					Thread.currentThread().getContextClassLoader());
172 		} catch (Exception e) {
173 			; // do nothing
174 		}
175 
176 		if (localized != null
177 				&& localized.getLocale().getLanguage().equals(
178 						locale.getLanguage())) {
179 			Enumeration<String> keys = localized.getKeys();
180 
181 			while (keys.hasMoreElements()) {
182 				String key = keys.nextElement();
183 				properties.setProperty(key, localized.getString(key));
184 			}
185 
186 			logger.debug("Resource bundle for locale '{}' loaded", locale); //$NON-NLS-1$
187 		} else {
188 			logger.debug("No resource bundle for locale '{}' found, loading default bundle", locale); //$NON-NLS-1$
189 
190 			ResourceBundle base = null;
191 			try {
192 				base = ResourceBundle.getBundle(LOCAL_PROPERTIES, NEUTRAL,
193 						Thread.currentThread().getContextClassLoader());
194 			} catch (Exception e) {
195 				; // do nothing
196 			}
197 
198 			if (base != null && base.getLocale().equals(NEUTRAL)) {
199 				Enumeration<String> keys = base.getKeys();
200 
201 				while (keys.hasMoreElements()) {
202 					String key = keys.nextElement();
203 					properties.setProperty(key, base.getString(key));
204 				}
205 			}
206 
207 		}
208 
209 	}
210 
211 	/**
212 	 * Searches for the message with the specified key in this message list.
213 	 * 
214 	 * @see Properties#getProperty(String)
215 	 */
216 	private String getMessage(String key) {
217 		return properties.getProperty(key);
218 	}
219 
220 	/** Returns localized <code>editor.compatibleBrowser.yes</code> property. */
221 	public String getCompatibleBrowserYes() {
222 		return getMessage("editor.compatibleBrowser.yes"); //$NON-NLS-1$
223 	}
224 
225 	/** Returns localized <code>editor.compatibleBrowser.no</code> property. */
226 	public String getCompatibleBrowserNo() {
227 		return getMessage("editor.compatibleBrowser.no"); //$NON-NLS-1$
228 	}
229 
230 	/** Returns localized <code>connector.fileUpload.enabled</code> property. */
231 	public String getFileUploadEnabled() {
232 		return getMessage("connector.fileUpload.enabled"); //$NON-NLS-1$
233 	}
234 
235 	/** Returns localized <code>connector.fileUpload.disabled</code> property. */
236 	public String getFileUploadDisabled() {
237 		return getMessage("connector.fileUpload.disabled"); //$NON-NLS-1$
238 	}
239 
240 	/**
241 	 * Returns localized <code>connector.file_renamed_warning</code> property.
242 	 * 
243 	 * @param newFileName
244 	 *            the new filename of the warning
245 	 * @return localized message with new filename
246 	 */
247 	public String getFileRenamedWarning(String newFileName) {
248 		return MessageFormat.format(getMessage("connector.fileUpload.file_renamed_warning"), newFileName); //$NON-NLS-1$
249 	}
250 
251 	/** Returns localized <code>connector.fileUpload.invalid_file_type_specified</code> property. */
252 	public String getInvalidFileTypeSpecified() {
253 		return getMessage("connector.fileUpload.invalid_file_type_specified"); //$NON-NLS-1$
254 	}
255 
256 	/** Returns localized <code>connector.fileUpload.write_error</code> property. */
257 	public String getFileUploadWriteError() {
258 		return getMessage("connector.fileUpload.write_error"); //$NON-NLS-1$
259 	}
260 
261 	/** Returns localized <code>connector.getResources.enabled</code> property. */
262 	public String getGetResourcesEnabled() {
263 		return getMessage("connector.getResources.enabled"); //$NON-NLS-1$
264 	}
265 
266 	/** Returns localized <code>connector.getResources.disabled</code> property. */
267 	public String getGetResourcesDisabled() {
268 		return getMessage("connector.getResources.disabled"); //$NON-NLS-1$
269 	}
270 
271 	/** Returns localized <code>connector.getResources.read_error</code> property. */
272 	public String getGetResourcesReadError() {
273 		return getMessage("connector.getResources.read_error"); //$NON-NLS-1$
274 	}
275 
276 	/** Returns localized <code>connector.createFolder.enabled</code> property. */
277 	public String getCreateFolderEnabled() {
278 		return getMessage("connector.createFolder.enabled"); //$NON-NLS-1$
279 	}
280 
281 	/** Returns localized <code>connector.createFolder.disabled</code> property. */
282 	public String getCreateFolderDisabled() {
283 		return getMessage("connector.createFolder.disabled"); //$NON-NLS-1$
284 	}
285 
286 	/** Returns localized <code>connector.invalid_command_specified</code> property. */
287 	public String getInvalidCommandSpecified() {
288 		return getMessage("connector.invalid_command_specified"); //$NON-NLS-1$
289 	}
290 
291 	/** Returns localized <code>connector.createFolder.folder_already_exists_error</code> property. */
292 	public String getFolderAlreadyExistsError() {
293 		return getMessage("connector.createFolder.folder_already_exists_error"); //$NON-NLS-1$
294 	}
295 
296 	/** Returns localized <code>connector.createFolder.invalid_new_folder_name_specified</code> property. */
297 	public String getInvalidNewFolderNameSpecified() {
298 		return getMessage("connector.createFolder.invalid_new_folder_name_specified"); //$NON-NLS-1$
299 	}
300 
301 	/** Returns localized <code>connector.createFolder.write_error</code> property. */
302 	public String getCreateFolderWriteError() {
303 		return getMessage("connector.createFolder.write_error"); //$NON-NLS-1$
304 	}
305 
306 	/** Returns localized <code>connector.invalid_resource_type_specified</code> property. */
307 	public String getInvalidResouceTypeSpecified() {
308 		return getMessage("connector.invalid_resource_type_specified"); //$NON-NLS-1$
309 	}
310 
311 	/** Returns localized <code>connector.invalid_current_folder_specified</code> property. */
312 	public String getInvalidCurrentFolderSpecified() {
313 		return getMessage("connector.invalid_current_folder_specified"); //$NON-NLS-1$
314 	}
315 
316 }