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