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 }