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