1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2019 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
20 package com.puppycrawl.tools.checkstyle.api;
21
22 import java.io.IOException;
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.nio.charset.StandardCharsets;
29 import java.text.MessageFormat;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.MissingResourceException;
36 import java.util.Objects;
37 import java.util.PropertyResourceBundle;
38 import java.util.ResourceBundle;
39 import java.util.ResourceBundle.Control;
40
41 /**
42 * Represents a message that can be localised. The translations come from
43 * message.properties files. The underlying implementation uses
44 * java.text.MessageFormat.
45 *
46 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
47 */
48 public final class LocalizedMessage
49 implements Comparable<LocalizedMessage>, Serializable {
50
51 private static final long serialVersionUID = 5675176836184862150L;
52
53 /**
54 * A cache that maps bundle names to ResourceBundles.
55 * Avoids repetitive calls to ResourceBundle.getBundle().
56 */
57 private static final Map<String, ResourceBundle> BUNDLE_CACHE =
58 Collections.synchronizedMap(new HashMap<>());
59
60 /** The default severity level if one is not specified. */
61 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
62
63 /** The locale to localise messages to. **/
64 private static Locale sLocale = Locale.getDefault();
65
66 /** The line number. **/
67 private final int lineNo;
68 /** The column number. **/
69 private final int columnNo;
70 /** The column char index. **/
71 private final int columnCharIndex;
72 /** The token type constant. See {@link TokenTypes}. **/
73 private final int tokenType;
74
75 /** The severity level. **/
76 private final SeverityLevel severityLevel;
77
78 /** The id of the module generating the message. */
79 private final String moduleId;
80
81 /** Key for the message format. **/
82 private final String key;
83
84 /** Arguments for MessageFormat.
85 * @noinspection NonSerializableFieldInSerializableClass
86 */
87 private final Object[] args;
88
89 /** Name of the resource bundle to get messages from. **/
90 private final String bundle;
91
92 /** Class of the source for this LocalizedMessage. */
93 private final Class<?> sourceClass;
94
95 /** A custom message overriding the default message from the bundle. */
96 private final String customMessage;
97
98 /**
99 * Creates a new {@code LocalizedMessage} instance.
100 *
101 * @param lineNo line number associated with the message
102 * @param columnNo column number associated with the message
103 * @param columnCharIndex column char index associated with the message
104 * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
105 * @param bundle resource bundle name
106 * @param key the key to locate the translation
107 * @param args arguments for the translation
108 * @param severityLevel severity level for the message
109 * @param moduleId the id of the module the message is associated with
110 * @param sourceClass the Class that is the source of the message
111 * @param customMessage optional custom message overriding the default
112 * @noinspection ConstructorWithTooManyParameters
113 */
114 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
115 public LocalizedMessage(int lineNo,
116 int columnNo,
117 int columnCharIndex,
118 int tokenType,
119 String bundle,
120 String key,
121 Object[] args,
122 SeverityLevel severityLevel,
123 String moduleId,
124 Class<?> sourceClass,
125 String customMessage) {
126 this.lineNo = lineNo;
127 this.columnNo = columnNo;
128 this.columnCharIndex = columnCharIndex;
129 this.tokenType = tokenType;
130 this.key = key;
131
132 if (args == null) {
133 this.args = null;
134 }
135 else {
136 this.args = Arrays.copyOf(args, args.length);
137 }
138 this.bundle = bundle;
139 this.severityLevel = severityLevel;
140 this.moduleId = moduleId;
141 this.sourceClass = sourceClass;
142 this.customMessage = customMessage;
143 }
144
145 /**
146 * Creates a new {@code LocalizedMessage} instance.
147 *
148 * @param lineNo line number associated with the message
149 * @param columnNo column number associated with the message
150 * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
151 * @param bundle resource bundle name
152 * @param key the key to locate the translation
153 * @param args arguments for the translation
154 * @param severityLevel severity level for the message
155 * @param moduleId the id of the module the message is associated with
156 * @param sourceClass the Class that is the source of the message
157 * @param customMessage optional custom message overriding the default
158 * @noinspection ConstructorWithTooManyParameters
159 */
160 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
161 public LocalizedMessage(int lineNo,
162 int columnNo,
163 int tokenType,
164 String bundle,
165 String key,
166 Object[] args,
167 SeverityLevel severityLevel,
168 String moduleId,
169 Class<?> sourceClass,
170 String customMessage) {
171 this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId,
172 sourceClass, customMessage);
173 }
174
175 /**
176 * Creates a new {@code LocalizedMessage} instance.
177 *
178 * @param lineNo line number associated with the message
179 * @param columnNo 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 severityLevel severity level for the message
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 * @noinspection ConstructorWithTooManyParameters
188 */
189 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
190 public LocalizedMessage(int lineNo,
191 int columnNo,
192 String bundle,
193 String key,
194 Object[] args,
195 SeverityLevel severityLevel,
196 String moduleId,
197 Class<?> sourceClass,
198 String customMessage) {
199 this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass,
200 customMessage);
201 }
202
203 /**
204 * Creates a new {@code LocalizedMessage} instance.
205 *
206 * @param lineNo line number associated with the message
207 * @param columnNo column number associated with the message
208 * @param bundle resource bundle name
209 * @param key the key to locate the translation
210 * @param args arguments for the translation
211 * @param moduleId the id of the module the message is associated with
212 * @param sourceClass the Class that is the source of the message
213 * @param customMessage optional custom message overriding the default
214 * @noinspection ConstructorWithTooManyParameters
215 */
216 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
217 public LocalizedMessage(int lineNo,
218 int columnNo,
219 String bundle,
220 String key,
221 Object[] args,
222 String moduleId,
223 Class<?> sourceClass,
224 String customMessage) {
225 this(lineNo,
226 columnNo,
227 bundle,
228 key,
229 args,
230 DEFAULT_SEVERITY,
231 moduleId,
232 sourceClass,
233 customMessage);
234 }
235
236 /**
237 * Creates a new {@code LocalizedMessage} instance.
238 *
239 * @param lineNo line number associated with the message
240 * @param bundle resource bundle name
241 * @param key the key to locate the translation
242 * @param args arguments for the translation
243 * @param severityLevel severity level for the message
244 * @param moduleId the id of the module the message is associated with
245 * @param sourceClass the source class for the message
246 * @param customMessage optional custom message overriding the default
247 * @noinspection ConstructorWithTooManyParameters
248 */
249 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
250 public LocalizedMessage(int lineNo,
251 String bundle,
252 String key,
253 Object[] args,
254 SeverityLevel severityLevel,
255 String moduleId,
256 Class<?> sourceClass,
257 String customMessage) {
258 this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
259 sourceClass, customMessage);
260 }
261
262 /**
263 * Creates a new {@code LocalizedMessage} instance. The column number
264 * defaults to 0.
265 *
266 * @param lineNo line number associated with the message
267 * @param bundle name of a resource bundle that contains error messages
268 * @param key the key to locate the translation
269 * @param args arguments for the translation
270 * @param moduleId the id of the module the message is associated with
271 * @param sourceClass the name of the source for the message
272 * @param customMessage optional custom message overriding the default
273 */
274 public LocalizedMessage(
275 int lineNo,
276 String bundle,
277 String key,
278 Object[] args,
279 String moduleId,
280 Class<?> sourceClass,
281 String customMessage) {
282 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
283 sourceClass, customMessage);
284 }
285
286 /**
287 * Indicates whether some other object is "equal to" this one.
288 * Suppression on enumeration is needed so code stays consistent.
289 * @noinspection EqualsCalledOnEnumConstant
290 */
291 // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
292 @Override
293 public boolean equals(Object object) {
294 if (this == object) {
295 return true;
296 }
297 if (object == null || getClass() != object.getClass()) {
298 return false;
299 }
300 final LocalizedMessage localizedMessage = (LocalizedMessage) object;
301 return Objects.equals(lineNo, localizedMessage.lineNo)
302 && Objects.equals(columnNo, localizedMessage.columnNo)
303 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
304 && Objects.equals(tokenType, localizedMessage.tokenType)
305 && Objects.equals(severityLevel, localizedMessage.severityLevel)
306 && Objects.equals(moduleId, localizedMessage.moduleId)
307 && Objects.equals(key, localizedMessage.key)
308 && Objects.equals(bundle, localizedMessage.bundle)
309 && Objects.equals(sourceClass, localizedMessage.sourceClass)
310 && Objects.equals(customMessage, localizedMessage.customMessage)
311 && Arrays.equals(args, localizedMessage.args);
312 }
313
314 @Override
315 public int hashCode() {
316 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
317 key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
318 }
319
320 /** Clears the cache. */
321 public static void clearCache() {
322 BUNDLE_CACHE.clear();
323 }
324
325 /**
326 * Gets the translated message.
327 * @return the translated message
328 */
329 public String getMessage() {
330 String message = getCustomMessage();
331
332 if (message == null) {
333 try {
334 // Important to use the default class loader, and not the one in
335 // the GlobalProperties object. This is because the class loader in
336 // the GlobalProperties is specified by the user for resolving
337 // custom classes.
338 final ResourceBundle resourceBundle = getBundle(bundle);
339 final String pattern = resourceBundle.getString(key);
340 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
341 message = formatter.format(args);
342 }
343 catch (final MissingResourceException ignored) {
344 // If the Check author didn't provide i18n resource bundles
345 // and logs error messages directly, this will return
346 // the author's original message
347 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
348 message = formatter.format(args);
349 }
350 }
351 return message;
352 }
353
354 /**
355 * Returns the formatted custom message if one is configured.
356 * @return the formatted custom message or {@code null}
357 * if there is no custom message
358 */
359 private String getCustomMessage() {
360 String message = null;
361 if (customMessage != null) {
362 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
363 message = formatter.format(args);
364 }
365 return message;
366 }
367
368 /**
369 * Find a ResourceBundle for a given bundle name. Uses the classloader
370 * of the class emitting this message, to be sure to get the correct
371 * bundle.
372 * @param bundleName the bundle name
373 * @return a ResourceBundle
374 */
375 private ResourceBundle getBundle(String bundleName) {
376 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
377 name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
378 }
379
380 /**
381 * Gets the line number.
382 * @return the line number
383 */
384 public int getLineNo() {
385 return lineNo;
386 }
387
388 /**
389 * Gets the column number.
390 * @return the column number
391 */
392 public int getColumnNo() {
393 return columnNo;
394 }
395
396 /**
397 * Gets the column char index.
398 * @return the column char index
399 */
400 public int getColumnCharIndex() {
401 return columnCharIndex;
402 }
403
404 /**
405 * Gets the token type.
406 * @return the token type
407 */
408 public int getTokenType() {
409 return tokenType;
410 }
411
412 /**
413 * Gets the severity level.
414 * @return the severity level
415 */
416 public SeverityLevel getSeverityLevel() {
417 return severityLevel;
418 }
419
420 /**
421 * Returns id of module.
422 * @return the module identifier.
423 */
424 public String getModuleId() {
425 return moduleId;
426 }
427
428 /**
429 * Returns the message key to locate the translation, can also be used
430 * in IDE plugins to map error messages to corrective actions.
431 *
432 * @return the message key
433 */
434 public String getKey() {
435 return key;
436 }
437
438 /**
439 * Gets the name of the source for this LocalizedMessage.
440 * @return the name of the source for this LocalizedMessage
441 */
442 public String getSourceName() {
443 return sourceClass.getName();
444 }
445
446 /**
447 * Sets a locale to use for localization.
448 * @param locale the locale to use for localization
449 */
450 public static void setLocale(Locale locale) {
451 clearCache();
452 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
453 sLocale = Locale.ROOT;
454 }
455 else {
456 sLocale = locale;
457 }
458 }
459
460 ////////////////////////////////////////////////////////////////////////////
461 // Interface Comparable methods
462 ////////////////////////////////////////////////////////////////////////////
463
464 @Override
465 public int compareTo(LocalizedMessage other) {
466 final int result;
467
468 if (lineNo == other.lineNo) {
469 if (columnNo == other.columnNo) {
470 if (Objects.equals(moduleId, other.moduleId)) {
471 result = getMessage().compareTo(other.getMessage());
472 }
473 else if (moduleId == null) {
474 result = -1;
475 }
476 else if (other.moduleId == null) {
477 result = 1;
478 }
479 else {
480 result = moduleId.compareTo(other.moduleId);
481 }
482 }
483 else {
484 result = Integer.compare(columnNo, other.columnNo);
485 }
486 }
487 else {
488 result = Integer.compare(lineNo, other.lineNo);
489 }
490 return result;
491 }
492
493 /**
494 * <p>
495 * Custom ResourceBundle.Control implementation which allows explicitly read
496 * the properties files as UTF-8.
497 * </p>
498 */
499 public static class Utf8Control extends Control {
500
501 @Override
502 public ResourceBundle newBundle(String baseName, Locale locale, String format,
503 ClassLoader loader, boolean reload) throws IOException {
504 // The below is a copy of the default implementation.
505 final String bundleName = toBundleName(baseName, locale);
506 final String resourceName = toResourceName(bundleName, "properties");
507 final URL url = loader.getResource(resourceName);
508 ResourceBundle resourceBundle = null;
509 if (url != null) {
510 final URLConnection connection = url.openConnection();
511 if (connection != null) {
512 connection.setUseCaches(!reload);
513 try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
514 StandardCharsets.UTF_8.name())) {
515 // Only this line is changed to make it read property files as UTF-8.
516 resourceBundle = new PropertyResourceBundle(streamReader);
517 }
518 }
519 }
520 return resourceBundle;
521 }
522
523 }
524
525 }