001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 the original author or authors.
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////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.api;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.Reader;
026import java.io.Serializable;
027import java.net.URL;
028import java.net.URLConnection;
029import java.text.MessageFormat;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.Locale;
034import java.util.Map;
035import java.util.MissingResourceException;
036import java.util.Objects;
037import java.util.PropertyResourceBundle;
038import java.util.ResourceBundle;
039import java.util.ResourceBundle.Control;
040
041/**
042 * Represents a message that can be localised. The translations come from
043 * message.properties files. The underlying implementation uses
044 * java.text.MessageFormat.
045 *
046 * @author Oliver Burn
047 * @author lkuehne
048 */
049public final class LocalizedMessage
050    implements Comparable<LocalizedMessage>, Serializable {
051    /** Required for serialization. */
052    private static final long serialVersionUID = 5675176836184862150L;
053
054    /** The locale to localise messages to. **/
055    private static Locale sLocale = Locale.getDefault();
056
057    /**
058     * A cache that maps bundle names to ResourceBundles.
059     * Avoids repetitive calls to ResourceBundle.getBundle().
060     */
061    private static final Map<String, ResourceBundle> BUNDLE_CACHE =
062        Collections.synchronizedMap(new HashMap<String, ResourceBundle>());
063
064    /** The default severity level if one is not specified. */
065    private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
066
067    /** The line number. **/
068    private final int lineNo;
069    /** The column number. **/
070    private final int columnNo;
071
072    /** The severity level. **/
073    private final SeverityLevel severityLevel;
074
075    /** The id of the module generating the message. */
076    private final String moduleId;
077
078    /** Key for the message format. **/
079    private final String key;
080
081    /** Arguments for MessageFormat. **/
082    private final Object[] args;
083
084    /** Name of the resource bundle to get messages from. **/
085    private final String bundle;
086
087    /** Class of the source for this LocalizedMessage. */
088    private final Class<?> sourceClass;
089
090    /** A custom message overriding the default message from the bundle. */
091    private final String customMessage;
092
093    /**
094     * Creates a new {@code LocalizedMessage} instance.
095     *
096     * @param lineNo line number associated with the message
097     * @param columnNo column number associated with the message
098     * @param bundle resource bundle name
099     * @param key the key to locate the translation
100     * @param args arguments for the translation
101     * @param severityLevel severity level for the message
102     * @param moduleId the id of the module the message is associated with
103     * @param sourceClass the Class that is the source of the message
104     * @param customMessage optional custom message overriding the default
105     */
106    public LocalizedMessage(int lineNo,
107                            int columnNo,
108                            String bundle,
109                            String key,
110                            Object[] args,
111                            SeverityLevel severityLevel,
112                            String moduleId,
113                            Class<?> sourceClass,
114                            String customMessage) {
115        this.lineNo = lineNo;
116        this.columnNo = columnNo;
117        this.key = key;
118
119        if (args == null) {
120            this.args = null;
121        }
122        else {
123            this.args = Arrays.copyOf(args, args.length);
124        }
125        this.bundle = bundle;
126        this.severityLevel = severityLevel;
127        this.moduleId = moduleId;
128        this.sourceClass = sourceClass;
129        this.customMessage = customMessage;
130    }
131
132    /**
133     * Creates a new {@code LocalizedMessage} instance.
134     *
135     * @param lineNo line number associated with the message
136     * @param columnNo column number associated with the message
137     * @param bundle resource bundle name
138     * @param key the key to locate the translation
139     * @param args arguments for the translation
140     * @param moduleId the id of the module the message is associated with
141     * @param sourceClass the Class that is the source of the message
142     * @param customMessage optional custom message overriding the default
143     */
144    public LocalizedMessage(int lineNo,
145                            int columnNo,
146                            String bundle,
147                            String key,
148                            Object[] args,
149                            String moduleId,
150                            Class<?> sourceClass,
151                            String customMessage) {
152        this(lineNo,
153                columnNo,
154             bundle,
155             key,
156             args,
157             DEFAULT_SEVERITY,
158             moduleId,
159             sourceClass,
160             customMessage);
161    }
162
163    /**
164     * Creates a new {@code LocalizedMessage} instance.
165     *
166     * @param lineNo line number associated with the message
167     * @param bundle resource bundle name
168     * @param key the key to locate the translation
169     * @param args arguments for the translation
170     * @param severityLevel severity level for the message
171     * @param moduleId the id of the module the message is associated with
172     * @param sourceClass the source class for the message
173     * @param customMessage optional custom message overriding the default
174     */
175    public LocalizedMessage(int lineNo,
176                            String bundle,
177                            String key,
178                            Object[] args,
179                            SeverityLevel severityLevel,
180                            String moduleId,
181                            Class<?> sourceClass,
182                            String customMessage) {
183        this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
184                sourceClass, customMessage);
185    }
186
187    /**
188     * Creates a new {@code LocalizedMessage} instance. The column number
189     * defaults to 0.
190     *
191     * @param lineNo line number associated with the message
192     * @param bundle name of a resource bundle that contains error messages
193     * @param key the key to locate the translation
194     * @param args arguments for the translation
195     * @param moduleId the id of the module the message is associated with
196     * @param sourceClass the name of the source for the message
197     * @param customMessage optional custom message overriding the default
198     */
199    public LocalizedMessage(
200        int lineNo,
201        String bundle,
202        String key,
203        Object[] args,
204        String moduleId,
205        Class<?> sourceClass,
206        String customMessage) {
207        this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
208                sourceClass, customMessage);
209    }
210
211    @Override
212    public boolean equals(Object object) {
213        if (this == object) {
214            return true;
215        }
216        if (object == null || getClass() != object.getClass()) {
217            return false;
218        }
219        final LocalizedMessage localizedMessage = (LocalizedMessage) object;
220        return Objects.equals(lineNo, localizedMessage.lineNo)
221                && Objects.equals(columnNo, localizedMessage.columnNo)
222                && Objects.equals(severityLevel, localizedMessage.severityLevel)
223                && Objects.equals(moduleId, localizedMessage.moduleId)
224                && Objects.equals(key, localizedMessage.key)
225                && Objects.equals(bundle, localizedMessage.bundle)
226                && Objects.equals(sourceClass, localizedMessage.sourceClass)
227                && Objects.equals(customMessage, localizedMessage.customMessage)
228                && Arrays.equals(args, localizedMessage.args);
229    }
230
231    @Override
232    public int hashCode() {
233        return Objects.hash(lineNo, columnNo, severityLevel, moduleId, key, bundle, sourceClass,
234                customMessage, Arrays.hashCode(args));
235    }
236
237    /** Clears the cache. */
238    public static void clearCache() {
239        synchronized (BUNDLE_CACHE) {
240            BUNDLE_CACHE.clear();
241        }
242    }
243
244    /**
245     * Gets the translated message.
246     * @return the translated message
247     */
248    public String getMessage() {
249        String message = getCustomMessage();
250
251        if (message == null) {
252            try {
253                // Important to use the default class loader, and not the one in
254                // the GlobalProperties object. This is because the class loader in
255                // the GlobalProperties is specified by the user for resolving
256                // custom classes.
257                final ResourceBundle resourceBundle = getBundle(bundle);
258                final String pattern = resourceBundle.getString(key);
259                message = MessageFormat.format(pattern, args);
260            }
261            catch (final MissingResourceException ignored) {
262                // If the Check author didn't provide i18n resource bundles
263                // and logs error messages directly, this will return
264                // the author's original message
265                message = MessageFormat.format(key, args);
266            }
267        }
268        return message;
269    }
270
271    /**
272     * Returns the formatted custom message if one is configured.
273     * @return the formatted custom message or {@code null}
274     *          if there is no custom message
275     */
276    private String getCustomMessage() {
277
278        if (customMessage == null) {
279            return null;
280        }
281
282        return MessageFormat.format(customMessage, args);
283    }
284
285    /**
286     * Find a ResourceBundle for a given bundle name. Uses the classloader
287     * of the class emitting this message, to be sure to get the correct
288     * bundle.
289     * @param bundleName the bundle name
290     * @return a ResourceBundle
291     */
292    private ResourceBundle getBundle(String bundleName) {
293        synchronized (BUNDLE_CACHE) {
294            ResourceBundle resourceBundle = BUNDLE_CACHE
295                    .get(bundleName);
296            if (resourceBundle == null) {
297                resourceBundle = ResourceBundle.getBundle(bundleName, sLocale,
298                        sourceClass.getClassLoader(), new UTF8Control());
299                BUNDLE_CACHE.put(bundleName, resourceBundle);
300            }
301            return resourceBundle;
302        }
303    }
304
305    /**
306     * Gets the line number.
307     * @return the line number
308     */
309    public int getLineNo() {
310        return lineNo;
311    }
312
313    /**
314     * Gets the column number.
315     * @return the column number
316     */
317    public int getColumnNo() {
318        return columnNo;
319    }
320
321    /**
322     * Gets the severity level.
323     * @return the severity level
324     */
325    public SeverityLevel getSeverityLevel() {
326        return severityLevel;
327    }
328
329    /**
330     * @return the module identifier.
331     */
332    public String getModuleId() {
333        return moduleId;
334    }
335
336    /**
337     * Returns the message key to locate the translation, can also be used
338     * in IDE plugins to map error messages to corrective actions.
339     *
340     * @return the message key
341     */
342    public String getKey() {
343        return key;
344    }
345
346    /**
347     * Gets the name of the source for this LocalizedMessage.
348     * @return the name of the source for this LocalizedMessage
349     */
350    public String getSourceName() {
351        return sourceClass.getName();
352    }
353
354    /**
355     * Sets a locale to use for localization.
356     * @param locale the locale to use for localization
357     */
358    public static void setLocale(Locale locale) {
359        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
360            sLocale = Locale.ROOT;
361        }
362        else {
363            sLocale = locale;
364        }
365    }
366
367    ////////////////////////////////////////////////////////////////////////////
368    // Interface Comparable methods
369    ////////////////////////////////////////////////////////////////////////////
370
371    @Override
372    public int compareTo(LocalizedMessage other) {
373        int result = Integer.compare(lineNo, other.lineNo);
374
375        if (lineNo == other.lineNo) {
376            if (columnNo == other.columnNo) {
377                result = getMessage().compareTo(other.getMessage());
378            }
379            else {
380                result = Integer.compare(columnNo, other.columnNo);
381            }
382        }
383        return result;
384    }
385
386    /**
387     * <p>
388     * Custom ResourceBundle.Control implementation which allows explicitly read
389     * the properties files as UTF-8
390     * </p>
391     *
392     * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
393     */
394    protected static class UTF8Control extends Control {
395        @Override
396        public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
397                 ClassLoader aLoader, boolean aReload) throws IOException {
398            // The below is a copy of the default implementation.
399            final String bundleName = toBundleName(aBaseName, aLocale);
400            final String resourceName = toResourceName(bundleName, "properties");
401            InputStream stream = null;
402            if (aReload) {
403                final URL url = aLoader.getResource(resourceName);
404                if (url != null) {
405                    final URLConnection connection = url.openConnection();
406                    if (connection != null) {
407                        connection.setUseCaches(false);
408                        stream = connection.getInputStream();
409                    }
410                }
411            }
412            else {
413                stream = aLoader.getResourceAsStream(resourceName);
414            }
415            ResourceBundle resourceBundle = null;
416            if (stream != null) {
417                final Reader streamReader = new InputStreamReader(stream, "UTF-8");
418                try {
419                    // Only this line is changed to make it to read properties files as UTF-8.
420                    resourceBundle = new PropertyResourceBundle(streamReader);
421                }
422                finally {
423                    stream.close();
424                }
425            }
426            return resourceBundle;
427        }
428    }
429}