View Javadoc
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 }