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 *
385 * @noinspection EqualsCalledOnEnumConstant
386 */
387 // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
388 @Override
389 public boolean equals(Object object) {
390 if (this == object) {
391 return true;
392 }
393 if (object == null || getClass() != object.getClass()) {
394 return false;
395 }
396 final LocalizedMessageuppycrawl/tools/checkstyle/api/LocalizedMessage.html#LocalizedMessage">LocalizedMessage localizedMessage = (LocalizedMessage) object;
397 return Objects.equals(lineNo, localizedMessage.lineNo)
398 && Objects.equals(columnNo, localizedMessage.columnNo)
399 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
400 && Objects.equals(tokenType, localizedMessage.tokenType)
401 && Objects.equals(severityLevel, localizedMessage.severityLevel)
402 && Objects.equals(moduleId, localizedMessage.moduleId)
403 && Objects.equals(key, localizedMessage.key)
404 && Objects.equals(bundle, localizedMessage.bundle)
405 && Objects.equals(sourceClass, localizedMessage.sourceClass)
406 && Objects.equals(customMessage, localizedMessage.customMessage)
407 && Arrays.equals(args, localizedMessage.args);
408 }
409
410 @Override
411 public int hashCode() {
412 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
413 key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
414 }
415
416 ////////////////////////////////////////////////////////////////////////////
417 // Interface Comparable methods
418 ////////////////////////////////////////////////////////////////////////////
419
420 @Override
421 public int compareTo(LocalizedMessage other) {
422 final int result;
423
424 if (lineNo == other.lineNo) {
425 if (columnNo == other.columnNo) {
426 if (Objects.equals(moduleId, other.moduleId)) {
427 result = getMessage().compareTo(other.getMessage());
428 }
429 else if (moduleId == null) {
430 result = -1;
431 }
432 else if (other.moduleId == null) {
433 result = 1;
434 }
435 else {
436 result = moduleId.compareTo(other.moduleId);
437 }
438 }
439 else {
440 result = Integer.compare(columnNo, other.columnNo);
441 }
442 }
443 else {
444 result = Integer.compare(lineNo, other.lineNo);
445 }
446 return result;
447 }
448
449 /**
450 * Gets the translated message.
451 *
452 * @return the translated message
453 */
454 public String getMessage() {
455 String message = getCustomMessage();
456
457 if (message == null) {
458 try {
459 // Important to use the default class loader, and not the one in
460 // the GlobalProperties object. This is because the class loader in
461 // the GlobalProperties is specified by the user for resolving
462 // custom classes.
463 final ResourceBundle resourceBundle = getBundle(bundle);
464 final String pattern = resourceBundle.getString(key);
465 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
466 message = formatter.format(args);
467 }
468 catch (final MissingResourceException ignored) {
469 // If the Check author didn't provide i18n resource bundles
470 // and logs audit event messages directly, this will return
471 // the author's original message
472 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
473 message = formatter.format(args);
474 }
475 }
476 return message;
477 }
478
479 /**
480 * Returns the formatted custom message if one is configured.
481 *
482 * @return the formatted custom message or {@code null}
483 * if there is no custom message
484 */
485 private String getCustomMessage() {
486 String message = null;
487 if (customMessage != null) {
488 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
489 message = formatter.format(args);
490 }
491 return message;
492 }
493
494 /**
495 * Find a ResourceBundle for a given bundle name. Uses the classloader
496 * of the class emitting this message, to be sure to get the correct
497 * bundle.
498 *
499 * @param bundleName the bundle name
500 * @return a ResourceBundle
501 */
502 private ResourceBundle getBundle(String bundleName) {
503 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> {
504 return ResourceBundle.getBundle(
505 name, sLocale, sourceClass.getClassLoader(), new Utf8Control());
506 });
507 }
508
509 /**
510 * <p>
511 * Custom ResourceBundle.Control implementation which allows explicitly read
512 * the properties files as UTF-8.
513 * </p>
514 */
515 public static class Utf8Control extends Control {
516
517 @Override
518 public ResourceBundle newBundle(String baseName, Locale locale, String format,
519 ClassLoader loader, boolean reload) throws IOException {
520 // The below is a copy of the default implementation.
521 final String bundleName = toBundleName(baseName, locale);
522 final String resourceName = toResourceName(bundleName, "properties");
523 final URL url = loader.getResource(resourceName);
524 ResourceBundle resourceBundle = null;
525 if (url != null) {
526 final URLConnection connection = url.openConnection();
527 if (connection != null) {
528 connection.setUseCaches(!reload);
529 try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
530 StandardCharsets.UTF_8.name())) {
531 // Only this line is changed to make it read property files as UTF-8.
532 resourceBundle = new PropertyResourceBundle(streamReader);
533 }
534 }
535 }
536 return resourceBundle;
537 }
538
539 }
540
541 }