001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.api;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.io.Reader;
025import java.io.Serializable;
026import java.net.URL;
027import java.net.URLConnection;
028import java.text.MessageFormat;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Locale;
033import java.util.Map;
034import java.util.MissingResourceException;
035import java.util.PropertyResourceBundle;
036import java.util.ResourceBundle;
037import java.util.ResourceBundle.Control;
038
039
040/**
041 * Represents a message that can be localised. The translations come from
042 * message.properties files. The underlying implementation uses
043 * java.text.MessageFormat.
044 *
045 * @author Oliver Burn
046 * @author lkuehne
047 * @version 1.0
048 */
049public final class LocalizedMessage
050    implements Comparable<LocalizedMessage>, Serializable
051{
052    /** Required for serialization. */
053    private static final long serialVersionUID = 5675176836184862150L;
054
055    /** hash function multiplicand */
056    private static final int HASH_MULT = 29;
057
058    /** the locale to localise messages to **/
059    private static Locale sLocale = Locale.getDefault();
060
061    /**
062     * A cache that maps bundle names to RessourceBundles.
063     * Avoids repetitive calls to ResourceBundle.getBundle().
064     */
065    private static final Map<String, ResourceBundle> BUNDLE_CACHE =
066        Collections.synchronizedMap(new HashMap<String, ResourceBundle>());
067
068    /** the line number **/
069    private final int lineNo;
070    /** the column number **/
071    private final int colNo;
072
073    /** the severity level **/
074    private final SeverityLevel severityLevel;
075
076    /** the id of the module generating the message. */
077    private final String moduleId;
078
079    /** the default severity level if one is not specified */
080    private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
081
082    /** key for the message format **/
083    private final String key;
084
085    /** arguments for MessageFormat **/
086    private final Object[] args;
087
088    /** name of the resource bundle to get messages from **/
089    private final String bundle;
090
091    /** class of the source for this LocalizedMessage */
092    private final Class<?> sourceClass;
093
094    /** a custom message overriding the default message from the bundle. */
095    private final String customMessage;
096
097    @Override
098    public boolean equals(Object object)
099    {
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}