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