View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2015 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  package com.puppycrawl.tools.checkstyle.api;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
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.text.MessageFormat;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.MissingResourceException;
35  import java.util.PropertyResourceBundle;
36  import java.util.ResourceBundle;
37  import java.util.ResourceBundle.Control;
38  
39  
40  /**
41   * Represents a message that can be localised. The translations come from
42   * message.properties files. The underlying implementation uses
43   * java.text.MessageFormat.
44   *
45   * @author Oliver Burn
46   * @author lkuehne
47   */
48  public final class LocalizedMessage
49      implements Comparable<LocalizedMessage>, Serializable
50  {
51      /** Required for serialization. */
52      private static final long serialVersionUID = 5675176836184862150L;
53  
54      /** hash function multiplicand */
55      private static final int HASH_MULT = 29;
56  
57      /** the locale to localise messages to **/
58      private static Locale sLocale = Locale.getDefault();
59  
60      /**
61       * A cache that maps bundle names to RessourceBundles.
62       * Avoids repetitive calls to ResourceBundle.getBundle().
63       */
64      private static final Map<String, ResourceBundle> BUNDLE_CACHE =
65          Collections.synchronizedMap(new HashMap<String, ResourceBundle>());
66  
67      /** the line number **/
68      private final int lineNo;
69      /** the column number **/
70      private final int colNo;
71  
72      /** the severity level **/
73      private final SeverityLevel severityLevel;
74  
75      /** the id of the module generating the message. */
76      private final String moduleId;
77  
78      /** the default severity level if one is not specified */
79      private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
80  
81      /** key for the message format **/
82      private final String key;
83  
84      /** arguments for MessageFormat **/
85      private final Object[] args;
86  
87      /** name of the resource bundle to get messages from **/
88      private final String bundle;
89  
90      /** class of the source for this LocalizedMessage */
91      private final Class<?> sourceClass;
92  
93      /** a custom message overriding the default message from the bundle. */
94      private final String customMessage;
95  
96      @Override
97      public boolean equals(Object object)
98      {
99          if (this == object) {
100             return true;
101         }
102         if (!(object instanceof LocalizedMessage)) {
103             return false;
104         }
105 
106         final LocalizedMessage localizedMessage = (LocalizedMessage) object;
107 
108         if (colNo != localizedMessage.colNo) {
109             return false;
110         }
111         if (lineNo != localizedMessage.lineNo) {
112             return false;
113         }
114         if (!key.equals(localizedMessage.key)) {
115             return false;
116         }
117 
118         if (!Arrays.equals(args, localizedMessage.args)) {
119             return false;
120         }
121         // ignoring bundle for perf reasons.
122 
123         // we currently never load the same error from different bundles.
124 
125         return true;
126     }
127 
128     @Override
129     public int hashCode()
130     {
131         int result;
132         result = lineNo;
133         result = HASH_MULT * result + colNo;
134         result = HASH_MULT * result + key.hashCode();
135         for (final Object element : args) {
136             result = HASH_MULT * result + element.hashCode();
137         }
138         return result;
139     }
140 
141     /**
142      * Creates a new <code>LocalizedMessage</code> instance.
143      *
144      * @param lineNo line number associated with the message
145      * @param colNo column number associated with the message
146      * @param bundle resource bundle name
147      * @param key the key to locate the translation
148      * @param args arguments for the translation
149      * @param severityLevel severity level for the message
150      * @param moduleId the id of the module the message is associated with
151      * @param sourceClass the Class that is the source of the message
152      * @param customMessage optional custom message overriding the default
153      */
154     public LocalizedMessage(int lineNo,
155                             int colNo,
156                             String bundle,
157                             String key,
158                             Object[] args,
159                             SeverityLevel severityLevel,
160                             String moduleId,
161                             Class<?> sourceClass,
162                             String customMessage)
163     {
164         this.lineNo = lineNo;
165         this.colNo = colNo;
166         this.key = key;
167         this.args = null == args ? null : args.clone();
168         this.bundle = bundle;
169         this.severityLevel = severityLevel;
170         this.moduleId = moduleId;
171         this.sourceClass = sourceClass;
172         this.customMessage = customMessage;
173     }
174 
175     /**
176      * Creates a new <code>LocalizedMessage</code> instance.
177      *
178      * @param lineNo line number associated with the message
179      * @param colNo 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 moduleId the id of the module the message is associated with
184      * @param sourceClass the Class that is the source of the message
185      * @param customMessage optional custom message overriding the default
186      */
187     public LocalizedMessage(int lineNo,
188                             int colNo,
189                             String bundle,
190                             String key,
191                             Object[] args,
192                             String moduleId,
193                             Class<?> sourceClass,
194                             String customMessage)
195     {
196         this(lineNo,
197              colNo,
198              bundle,
199              key,
200              args,
201              DEFAULT_SEVERITY,
202              moduleId,
203              sourceClass,
204              customMessage);
205     }
206 
207     /**
208      * Creates a new <code>LocalizedMessage</code> instance.
209      *
210      * @param lineNo line number associated with the message
211      * @param bundle resource bundle name
212      * @param key the key to locate the translation
213      * @param args arguments for the translation
214      * @param severityLevel severity level for the message
215      * @param moduleId the id of the module the message is associated with
216      * @param sourceClass the source class for the message
217      * @param customMessage optional custom message overriding the default
218      */
219     public LocalizedMessage(int lineNo,
220                             String bundle,
221                             String key,
222                             Object[] args,
223                             SeverityLevel severityLevel,
224                             String moduleId,
225                             Class<?> sourceClass,
226                             String customMessage)
227     {
228         this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
229                 sourceClass, customMessage);
230     }
231 
232     /**
233      * Creates a new <code>LocalizedMessage</code> instance. The column number
234      * defaults to 0.
235      *
236      * @param lineNo line number associated with the message
237      * @param bundle name of a resource bundle that contains error messages
238      * @param key the key to locate the translation
239      * @param args arguments for the translation
240      * @param moduleId the id of the module the message is associated with
241      * @param sourceClass the name of the source for the message
242      * @param customMessage optional custom message overriding the default
243      */
244     public LocalizedMessage(
245         int lineNo,
246         String bundle,
247         String key,
248         Object[] args,
249         String moduleId,
250         Class<?> sourceClass,
251         String customMessage)
252     {
253         this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
254                 sourceClass, customMessage);
255     }
256 
257     /** Clears the cache. */
258     public static void clearCache()
259     {
260         synchronized (BUNDLE_CACHE) {
261             BUNDLE_CACHE.clear();
262         }
263     }
264 
265     /** @return the translated message **/
266     public String getMessage()
267     {
268 
269         final String customMessage = getCustomMessage();
270         if (customMessage != null) {
271             return customMessage;
272         }
273 
274         try {
275             // Important to use the default class loader, and not the one in
276             // the GlobalProperties object. This is because the class loader in
277             // the GlobalProperties is specified by the user for resolving
278             // custom classes.
279             final ResourceBundle bundle = getBundle(this.bundle);
280             final String pattern = bundle.getString(key);
281             return MessageFormat.format(pattern, args);
282         }
283         catch (final MissingResourceException ex) {
284             // If the Check author didn't provide i18n resource bundles
285             // and logs error messages directly, this will return
286             // the author's original message
287             return MessageFormat.format(key, args);
288         }
289     }
290 
291     /**
292      * Returns the formatted custom message if one is configured.
293      * @return the formatted custom message or <code>null</code>
294      *          if there is no custom message
295      */
296     private String getCustomMessage()
297     {
298 
299         if (customMessage == null) {
300             return null;
301         }
302 
303         return MessageFormat.format(customMessage, args);
304     }
305 
306     /**
307      * Find a ResourceBundle for a given bundle name. Uses the classloader
308      * of the class emitting this message, to be sure to get the correct
309      * bundle.
310      * @param bundleName the bundle name
311      * @return a ResourceBundle
312      */
313     private ResourceBundle getBundle(String bundleName)
314     {
315         synchronized (BUNDLE_CACHE) {
316             ResourceBundle bundle = BUNDLE_CACHE
317                     .get(bundleName);
318             if (bundle == null) {
319                 bundle = ResourceBundle.getBundle(bundleName, sLocale,
320                         sourceClass.getClassLoader(), new UTF8Control());
321                 BUNDLE_CACHE.put(bundleName, bundle);
322             }
323             return bundle;
324         }
325     }
326 
327     /** @return the line number **/
328     public int getLineNo()
329     {
330         return lineNo;
331     }
332 
333     /** @return the column number **/
334     public int getColumnNo()
335     {
336         return colNo;
337     }
338 
339     /** @return the severity level **/
340     public SeverityLevel getSeverityLevel()
341     {
342         return severityLevel;
343     }
344 
345     /** @return the module identifier. */
346     public String getModuleId()
347     {
348         return moduleId;
349     }
350 
351     /**
352      * Returns the message key to locate the translation, can also be used
353      * in IDE plugins to map error messages to corrective actions.
354      *
355      * @return the message key
356      */
357     public String getKey()
358     {
359         return key;
360     }
361 
362     /** @return the name of the source for this LocalizedMessage */
363     public String getSourceName()
364     {
365         return sourceClass.getName();
366     }
367 
368     /** @param locale the locale to use for localization **/
369     public static void setLocale(Locale locale)
370     {
371         sLocale = locale;
372     }
373 
374     ////////////////////////////////////////////////////////////////////////////
375     // Interface Comparable methods
376     ////////////////////////////////////////////////////////////////////////////
377 
378     /** {@inheritDoc} */
379     @Override
380     public int compareTo(LocalizedMessage other)
381     {
382         if (getLineNo() == other.getLineNo()) {
383             if (getColumnNo() == other.getColumnNo()) {
384                 return getMessage().compareTo(other.getMessage());
385             }
386             return getColumnNo() < other.getColumnNo() ? -1 : 1;
387         }
388 
389         return getLineNo() < other.getLineNo() ? -1 : 1;
390     }
391 
392     /**
393      * <p>
394      * Custom ResourceBundle.Control implementation which allows explicitly read
395      * the properties files as UTF-8
396      * </p>
397      *
398      * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
399      */
400     private static class UTF8Control extends Control
401     {
402         @Override
403         public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
404                  ClassLoader aLoader, boolean aReload) throws IllegalAccessException,
405                   InstantiationException, IOException
406         {
407             // The below is a copy of the default implementation.
408             final String bundleName = toBundleName(aBaseName, aLocale);
409             final String resourceName = toResourceName(bundleName, "properties");
410             ResourceBundle bundle = null;
411             InputStream stream = null;
412             if (aReload) {
413                 final URL url = aLoader.getResource(resourceName);
414                 if (url != null) {
415                     final URLConnection connection = url.openConnection();
416                     if (connection != null) {
417                         connection.setUseCaches(false);
418                         stream = connection.getInputStream();
419                     }
420                 }
421             }
422             else {
423                 stream = aLoader.getResourceAsStream(resourceName);
424             }
425             if (stream != null) {
426                 try (Reader streamReader = new InputStreamReader(stream, "UTF-8")) {
427                     // Only this line is changed to make it to read properties files as UTF-8.
428                     bundle = new PropertyResourceBundle(streamReader);
429                 } finally {
430                     stream.close();
431                 }
432             }
433             return bundle;
434         }
435     }
436 }