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      * @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 }