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