View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.Reader;
26  import java.io.Serializable;
27  import java.net.URL;
28  import java.net.URLConnection;
29  import java.nio.charset.StandardCharsets;
30  import java.text.MessageFormat;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.MissingResourceException;
37  import java.util.Objects;
38  import java.util.PropertyResourceBundle;
39  import java.util.ResourceBundle;
40  import java.util.ResourceBundle.Control;
41  
42  /**
43   * Represents a message that can be localised. The translations come from
44   * message.properties files. The underlying implementation uses
45   * java.text.MessageFormat.
46   *
47   * @author Oliver Burn
48   * @author lkuehne
49   * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
50   */
51  public final class LocalizedMessage
52      implements Comparable<LocalizedMessage>, Serializable {
53      private static final long serialVersionUID = 5675176836184862150L;
54  
55      /**
56       * A cache that maps bundle names to ResourceBundles.
57       * Avoids repetitive calls to ResourceBundle.getBundle().
58       */
59      private static final Map<String, ResourceBundle> BUNDLE_CACHE =
60          Collections.synchronizedMap(new HashMap<>());
61  
62      /** The default severity level if one is not specified. */
63      private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
64  
65      /** The locale to localise messages to. **/
66      private static Locale sLocale = Locale.getDefault();
67  
68      /** The line number. **/
69      private final int lineNo;
70      /** The column number. **/
71      private final int columnNo;
72      /** The column char index. **/
73      private final int columnCharIndex;
74      /** The token type constant. See {@link TokenTypes}. **/
75      private final int tokenType;
76  
77      /** The severity level. **/
78      private final SeverityLevel severityLevel;
79  
80      /** The id of the module generating the message. */
81      private final String moduleId;
82  
83      /** Key for the message format. **/
84      private final String key;
85  
86      /** Arguments for MessageFormat.
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 error 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      * @noinspection ConstructorWithTooManyParameters
276      */
277     public LocalizedMessage(
278         int lineNo,
279         String bundle,
280         String key,
281         Object[] args,
282         String moduleId,
283         Class<?> sourceClass,
284         String customMessage) {
285         this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
286                 sourceClass, customMessage);
287     }
288 
289     // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
290     @Override
291     public boolean equals(Object object) {
292         if (this == object) {
293             return true;
294         }
295         if (object == null || getClass() != object.getClass()) {
296             return false;
297         }
298         final LocalizedMessage localizedMessage = (LocalizedMessage) object;
299         return Objects.equals(lineNo, localizedMessage.lineNo)
300                 && Objects.equals(columnNo, localizedMessage.columnNo)
301                 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
302                 && Objects.equals(tokenType, localizedMessage.tokenType)
303                 && Objects.equals(severityLevel, localizedMessage.severityLevel)
304                 && Objects.equals(moduleId, localizedMessage.moduleId)
305                 && Objects.equals(key, localizedMessage.key)
306                 && Objects.equals(bundle, localizedMessage.bundle)
307                 && Objects.equals(sourceClass, localizedMessage.sourceClass)
308                 && Objects.equals(customMessage, localizedMessage.customMessage)
309                 && Arrays.equals(args, localizedMessage.args);
310     }
311 
312     @Override
313     public int hashCode() {
314         return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
315                 key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
316     }
317 
318     /** Clears the cache. */
319     public static void clearCache() {
320         BUNDLE_CACHE.clear();
321     }
322 
323     /**
324      * Gets the translated message.
325      * @return the translated message
326      */
327     public String getMessage() {
328         String message = getCustomMessage();
329 
330         if (message == null) {
331             try {
332                 // Important to use the default class loader, and not the one in
333                 // the GlobalProperties object. This is because the class loader in
334                 // the GlobalProperties is specified by the user for resolving
335                 // custom classes.
336                 final ResourceBundle resourceBundle = getBundle(bundle);
337                 final String pattern = resourceBundle.getString(key);
338                 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
339                 message = formatter.format(args);
340             }
341             catch (final MissingResourceException ignored) {
342                 // If the Check author didn't provide i18n resource bundles
343                 // and logs error messages directly, this will return
344                 // the author's original message
345                 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
346                 message = formatter.format(args);
347             }
348         }
349         return message;
350     }
351 
352     /**
353      * Returns the formatted custom message if one is configured.
354      * @return the formatted custom message or {@code null}
355      *          if there is no custom message
356      */
357     private String getCustomMessage() {
358         String message = null;
359         if (customMessage != null) {
360             final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
361             message = formatter.format(args);
362         }
363         return message;
364     }
365 
366     /**
367      * Find a ResourceBundle for a given bundle name. Uses the classloader
368      * of the class emitting this message, to be sure to get the correct
369      * bundle.
370      * @param bundleName the bundle name
371      * @return a ResourceBundle
372      */
373     private ResourceBundle getBundle(String bundleName) {
374         return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
375                 name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
376     }
377 
378     /**
379      * Gets the line number.
380      * @return the line number
381      */
382     public int getLineNo() {
383         return lineNo;
384     }
385 
386     /**
387      * Gets the column number.
388      * @return the column number
389      */
390     public int getColumnNo() {
391         return columnNo;
392     }
393 
394     /**
395      * Gets the column char index.
396      * @return the column char index
397      */
398     public int getColumnCharIndex() {
399         return columnCharIndex;
400     }
401 
402     /**
403      * Gets the token type.
404      * @return the token type
405      */
406     public int getTokenType() {
407         return tokenType;
408     }
409 
410     /**
411      * Gets the severity level.
412      * @return the severity level
413      */
414     public SeverityLevel getSeverityLevel() {
415         return severityLevel;
416     }
417 
418     /**
419      * Returns id of module.
420      * @return the module identifier.
421      */
422     public String getModuleId() {
423         return moduleId;
424     }
425 
426     /**
427      * Returns the message key to locate the translation, can also be used
428      * in IDE plugins to map error messages to corrective actions.
429      *
430      * @return the message key
431      */
432     public String getKey() {
433         return key;
434     }
435 
436     /**
437      * Gets the name of the source for this LocalizedMessage.
438      * @return the name of the source for this LocalizedMessage
439      */
440     public String getSourceName() {
441         return sourceClass.getName();
442     }
443 
444     /**
445      * Sets a locale to use for localization.
446      * @param locale the locale to use for localization
447      */
448     public static void setLocale(Locale locale) {
449         clearCache();
450         if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
451             sLocale = Locale.ROOT;
452         }
453         else {
454             sLocale = locale;
455         }
456     }
457 
458     ////////////////////////////////////////////////////////////////////////////
459     // Interface Comparable methods
460     ////////////////////////////////////////////////////////////////////////////
461 
462     @Override
463     public int compareTo(LocalizedMessage other) {
464         final int result;
465 
466         if (lineNo == other.lineNo) {
467             if (columnNo == other.columnNo) {
468                 if (Objects.equals(moduleId, other.moduleId)) {
469                     result = getMessage().compareTo(other.getMessage());
470                 }
471                 else if (moduleId == null) {
472                     result = -1;
473                 }
474                 else if (other.moduleId == null) {
475                     result = 1;
476                 }
477                 else {
478                     result = moduleId.compareTo(other.moduleId);
479                 }
480             }
481             else {
482                 result = Integer.compare(columnNo, other.columnNo);
483             }
484         }
485         else {
486             result = Integer.compare(lineNo, other.lineNo);
487         }
488         return result;
489     }
490 
491     /**
492      * <p>
493      * Custom ResourceBundle.Control implementation which allows explicitly read
494      * the properties files as UTF-8.
495      * </p>
496      *
497      * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
498      * @noinspection IOResourceOpenedButNotSafelyClosed
499      */
500     public static class Utf8Control extends Control {
501         @Override
502         public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
503                  ClassLoader aLoader, boolean aReload) throws IOException {
504             // The below is a copy of the default implementation.
505             final String bundleName = toBundleName(aBaseName, aLocale);
506             final String resourceName = toResourceName(bundleName, "properties");
507             InputStream stream = null;
508             if (aReload) {
509                 final URL url = aLoader.getResource(resourceName);
510                 if (url != null) {
511                     final URLConnection connection = url.openConnection();
512                     if (connection != null) {
513                         connection.setUseCaches(false);
514                         stream = connection.getInputStream();
515                     }
516                 }
517             }
518             else {
519                 stream = aLoader.getResourceAsStream(resourceName);
520             }
521             ResourceBundle resourceBundle = null;
522             if (stream != null) {
523                 final Reader streamReader = new InputStreamReader(stream,
524                         StandardCharsets.UTF_8.name());
525                 try {
526                     // Only this line is changed to make it to read properties files as UTF-8.
527                     resourceBundle = new PropertyResourceBundle(streamReader);
528                 }
529                 finally {
530                     stream.close();
531                 }
532             }
533             return resourceBundle;
534         }
535     }
536 }