1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2014 Oliver Burn
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ////////////////////////////////////////////////////////////////////////////////
19 package com.puppycrawl.tools.checkstyle.api;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.Reader;
25 import java.io.Serializable;
26 import java.net.URL;
27 import java.net.URLConnection;
28 import java.text.MessageFormat;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.MissingResourceException;
35 import java.util.PropertyResourceBundle;
36 import java.util.ResourceBundle;
37 import java.util.ResourceBundle.Control;
38
39
40 /**
41 * Represents a message that can be localised. The translations come from
42 * message.properties files. The underlying implementation uses
43 * java.text.MessageFormat.
44 *
45 * @author Oliver Burn
46 * @author lkuehne
47 * @version 1.0
48 */
49 public final class LocalizedMessage
50 implements Comparable<LocalizedMessage>, Serializable
51 {
52 /** Required for serialization. */
53 private static final long serialVersionUID = 5675176836184862150L;
54
55 /** hash function multiplicand */
56 private static final int HASH_MULT = 29;
57
58 /** the locale to localise messages to **/
59 private static Locale sLocale = Locale.getDefault();
60
61 /**
62 * A cache that maps bundle names to RessourceBundles.
63 * Avoids repetitive calls to ResourceBundle.getBundle().
64 */
65 private static final Map<String, ResourceBundle> BUNDLE_CACHE =
66 Collections.synchronizedMap(new HashMap<String, ResourceBundle>());
67
68 /** the line number **/
69 private final int lineNo;
70 /** the column number **/
71 private final int colNo;
72
73 /** the severity level **/
74 private final SeverityLevel severityLevel;
75
76 /** the id of the module generating the message. */
77 private final String moduleId;
78
79 /** the default severity level if one is not specified */
80 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
81
82 /** key for the message format **/
83 private final String key;
84
85 /** arguments for MessageFormat **/
86 private final Object[] args;
87
88 /** name of the resource bundle to get messages from **/
89 private final String bundle;
90
91 /** class of the source for this LocalizedMessage */
92 private final Class<?> sourceClass;
93
94 /** a custom message overriding the default message from the bundle. */
95 private final String customMessage;
96
97 @Override
98 public boolean equals(Object object)
99 {
100 if (this == object) {
101 return true;
102 }
103 if (!(object instanceof LocalizedMessage)) {
104 return false;
105 }
106
107 final LocalizedMessage localizedMessage = (LocalizedMessage) object;
108
109 if (colNo != localizedMessage.colNo) {
110 return false;
111 }
112 if (lineNo != localizedMessage.lineNo) {
113 return false;
114 }
115 if (!key.equals(localizedMessage.key)) {
116 return false;
117 }
118
119 if (!Arrays.equals(args, localizedMessage.args)) {
120 return false;
121 }
122 // ignoring bundle for perf reasons.
123
124 // we currently never load the same error from different bundles.
125
126 return true;
127 }
128
129 @Override
130 public int hashCode()
131 {
132 int result;
133 result = lineNo;
134 result = HASH_MULT * result + colNo;
135 result = HASH_MULT * result + key.hashCode();
136 for (final Object element : args) {
137 result = HASH_MULT * result + element.hashCode();
138 }
139 return result;
140 }
141
142 /**
143 * Creates a new <code>LocalizedMessage</code> instance.
144 *
145 * @param lineNo line number associated with the message
146 * @param colNo column number associated with the message
147 * @param bundle resource bundle name
148 * @param key the key to locate the translation
149 * @param args arguments for the translation
150 * @param severityLevel severity level for the message
151 * @param moduleId the id of the module the message is associated with
152 * @param sourceClass the Class that is the source of the message
153 * @param customMessage optional custom message overriding the default
154 */
155 public LocalizedMessage(int lineNo,
156 int colNo,
157 String bundle,
158 String key,
159 Object[] args,
160 SeverityLevel severityLevel,
161 String moduleId,
162 Class<?> sourceClass,
163 String customMessage)
164 {
165 this.lineNo = lineNo;
166 this.colNo = colNo;
167 this.key = key;
168 this.args = (null == args) ? null : args.clone();
169 this.bundle = bundle;
170 this.severityLevel = severityLevel;
171 this.moduleId = moduleId;
172 this.sourceClass = sourceClass;
173 this.customMessage = customMessage;
174 }
175
176 /**
177 * Creates a new <code>LocalizedMessage</code> instance.
178 *
179 * @param lineNo line number associated with the message
180 * @param colNo column number associated with the message
181 * @param bundle resource bundle name
182 * @param key the key to locate the translation
183 * @param args arguments for the translation
184 * @param moduleId the id of the module the message is associated with
185 * @param sourceClass the Class that is the source of the message
186 * @param customMessage optional custom message overriding the default
187 */
188 public LocalizedMessage(int lineNo,
189 int colNo,
190 String bundle,
191 String key,
192 Object[] args,
193 String moduleId,
194 Class<?> sourceClass,
195 String customMessage)
196 {
197 this(lineNo,
198 colNo,
199 bundle,
200 key,
201 args,
202 DEFAULT_SEVERITY,
203 moduleId,
204 sourceClass,
205 customMessage);
206 }
207
208 /**
209 * Creates a new <code>LocalizedMessage</code> instance.
210 *
211 * @param lineNo line number associated with the message
212 * @param bundle resource bundle name
213 * @param key the key to locate the translation
214 * @param args arguments for the translation
215 * @param severityLevel severity level for the message
216 * @param moduleId the id of the module the message is associated with
217 * @param sourceClass the source class for the message
218 * @param customMessage optional custom message overriding the default
219 */
220 public LocalizedMessage(int lineNo,
221 String bundle,
222 String key,
223 Object[] args,
224 SeverityLevel severityLevel,
225 String moduleId,
226 Class<?> sourceClass,
227 String customMessage)
228 {
229 this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
230 sourceClass, customMessage);
231 }
232
233 /**
234 * Creates a new <code>LocalizedMessage</code> instance. The column number
235 * defaults to 0.
236 *
237 * @param lineNo line number associated with the message
238 * @param bundle name of a resource bundle that contains error messages
239 * @param key the key to locate the translation
240 * @param args arguments for the translation
241 * @param moduleId the id of the module the message is associated with
242 * @param sourceClass the name of the source for the message
243 * @param customMessage optional custom message overriding the default
244 */
245 public LocalizedMessage(
246 int lineNo,
247 String bundle,
248 String key,
249 Object[] args,
250 String moduleId,
251 Class<?> sourceClass,
252 String customMessage)
253 {
254 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
255 sourceClass, customMessage);
256 }
257
258 /** Clears the cache. */
259 public static void clearCache()
260 {
261 synchronized (BUNDLE_CACHE) {
262 BUNDLE_CACHE.clear();
263 }
264 }
265
266 /** @return the translated message **/
267 public String getMessage()
268 {
269
270 final String customMessage = getCustomMessage();
271 if (customMessage != null) {
272 return customMessage;
273 }
274
275 try {
276 // Important to use the default class loader, and not the one in
277 // the GlobalProperties object. This is because the class loader in
278 // the GlobalProperties is specified by the user for resolving
279 // custom classes.
280 final ResourceBundle bundle = getBundle(this.bundle);
281 final String pattern = bundle.getString(key);
282 return MessageFormat.format(pattern, args);
283 }
284 catch (final MissingResourceException ex) {
285 // If the Check author didn't provide i18n resource bundles
286 // and logs error messages directly, this will return
287 // the author's original message
288 return MessageFormat.format(key, args);
289 }
290 }
291
292 /**
293 * Returns the formatted custom message if one is configured.
294 * @return the formatted custom message or <code>null</code>
295 * if there is no custom message
296 */
297 private String getCustomMessage()
298 {
299
300 if (customMessage == null) {
301 return null;
302 }
303
304 return MessageFormat.format(customMessage, args);
305 }
306
307 /**
308 * Find a ResourceBundle for a given bundle name. Uses the classloader
309 * of the class emitting this message, to be sure to get the correct
310 * bundle.
311 * @param bundleName the bundle name
312 * @return a ResourceBundle
313 */
314 private ResourceBundle getBundle(String bundleName)
315 {
316 synchronized (BUNDLE_CACHE) {
317 ResourceBundle bundle = BUNDLE_CACHE
318 .get(bundleName);
319 if (bundle == null) {
320 bundle = ResourceBundle.getBundle(bundleName, sLocale,
321 sourceClass.getClassLoader(), new UTF8Control());
322 BUNDLE_CACHE.put(bundleName, bundle);
323 }
324 return bundle;
325 }
326 }
327
328 /** @return the line number **/
329 public int getLineNo()
330 {
331 return lineNo;
332 }
333
334 /** @return the column number **/
335 public int getColumnNo()
336 {
337 return colNo;
338 }
339
340 /** @return the severity level **/
341 public SeverityLevel getSeverityLevel()
342 {
343 return severityLevel;
344 }
345
346 /** @return the module identifier. */
347 public String getModuleId()
348 {
349 return moduleId;
350 }
351
352 /**
353 * Returns the message key to locate the translation, can also be used
354 * in IDE plugins to map error messages to corrective actions.
355 *
356 * @return the message key
357 */
358 public String getKey()
359 {
360 return key;
361 }
362
363 /** @return the name of the source for this LocalizedMessage */
364 public String getSourceName()
365 {
366 return sourceClass.getName();
367 }
368
369 /** @param locale the locale to use for localization **/
370 public static void setLocale(Locale locale)
371 {
372 sLocale = locale;
373 }
374
375 ////////////////////////////////////////////////////////////////////////////
376 // Interface Comparable methods
377 ////////////////////////////////////////////////////////////////////////////
378
379 /** {@inheritDoc} */
380 @Override
381 public int compareTo(LocalizedMessage other)
382 {
383 if (getLineNo() == other.getLineNo()) {
384 if (getColumnNo() == other.getColumnNo()) {
385 return getMessage().compareTo(other.getMessage());
386 }
387 return (getColumnNo() < other.getColumnNo()) ? -1 : 1;
388 }
389
390 return (getLineNo() < other.getLineNo()) ? -1 : 1;
391 }
392
393 /**
394 * <p>
395 * Custom ResourceBundle.Control implementation which allows explicitly read
396 * the properties files as UTF-8
397 * </p>
398 *
399 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
400 */
401 private static class UTF8Control extends Control
402 {
403 @Override
404 public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
405 ClassLoader aLoader, boolean aReload) throws IllegalAccessException,
406 InstantiationException, IOException
407 {
408 // The below is a copy of the default implementation.
409 final String bundleName = toBundleName(aBaseName, aLocale);
410 final String resourceName = toResourceName(bundleName, "properties");
411 ResourceBundle bundle = null;
412 InputStream stream = null;
413 if (aReload) {
414 final URL url = aLoader.getResource(resourceName);
415 if (url != null) {
416 final URLConnection connection = url.openConnection();
417 if (connection != null) {
418 connection.setUseCaches(false);
419 stream = connection.getInputStream();
420 }
421 }
422 }
423 else {
424 stream = aLoader.getResourceAsStream(resourceName);
425 }
426 if (stream != null) {
427 try (Reader streamReader = new InputStreamReader(stream, "UTF-8")) {
428 // Only this line is changed to make it to read properties files as UTF-8.
429 bundle = new PropertyResourceBundle(streamReader);
430 } finally {
431 stream.close();
432 }
433 }
434 return bundle;
435 }
436 }
437 }