001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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    /**
054     * A cache that maps bundle names to ResourceBundles.
055     * Avoids repetitive calls to ResourceBundle.getBundle().
056     */
057    private static final Map<String, ResourceBundle> BUNDLE_CACHE =
058        Collections.synchronizedMap(new HashMap<>());
059
060    /** The default severity level if one is not specified. */
061    private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
062
063    /** The locale to localise messages to. **/
064    private static Locale sLocale = Locale.getDefault();
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    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
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    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
145    public LocalizedMessage(int lineNo,
146                            int columnNo,
147                            String bundle,
148                            String key,
149                            Object[] args,
150                            String moduleId,
151                            Class<?> sourceClass,
152                            String customMessage) {
153        this(lineNo,
154                columnNo,
155             bundle,
156             key,
157             args,
158             DEFAULT_SEVERITY,
159             moduleId,
160             sourceClass,
161             customMessage);
162    }
163
164    /**
165     * Creates a new {@code LocalizedMessage} instance.
166     *
167     * @param lineNo line number associated with the message
168     * @param bundle resource bundle name
169     * @param key the key to locate the translation
170     * @param args arguments for the translation
171     * @param severityLevel severity level for the message
172     * @param moduleId the id of the module the message is associated with
173     * @param sourceClass the source class for the message
174     * @param customMessage optional custom message overriding the default
175     */
176    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
177    public LocalizedMessage(int lineNo,
178                            String bundle,
179                            String key,
180                            Object[] args,
181                            SeverityLevel severityLevel,
182                            String moduleId,
183                            Class<?> sourceClass,
184                            String customMessage) {
185        this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
186                sourceClass, customMessage);
187    }
188
189    /**
190     * Creates a new {@code LocalizedMessage} instance. The column number
191     * defaults to 0.
192     *
193     * @param lineNo line number associated with the message
194     * @param bundle name of a resource bundle that contains error messages
195     * @param key the key to locate the translation
196     * @param args arguments for the translation
197     * @param moduleId the id of the module the message is associated with
198     * @param sourceClass the name of the source for the message
199     * @param customMessage optional custom message overriding the default
200     */
201    public LocalizedMessage(
202        int lineNo,
203        String bundle,
204        String key,
205        Object[] args,
206        String moduleId,
207        Class<?> sourceClass,
208        String customMessage) {
209        this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
210                sourceClass, customMessage);
211    }
212
213    // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
214    @Override
215    public boolean equals(Object object) {
216        if (this == object) {
217            return true;
218        }
219        if (object == null || getClass() != object.getClass()) {
220            return false;
221        }
222        final LocalizedMessage localizedMessage = (LocalizedMessage) object;
223        return Objects.equals(lineNo, localizedMessage.lineNo)
224                && Objects.equals(columnNo, localizedMessage.columnNo)
225                && Objects.equals(severityLevel, localizedMessage.severityLevel)
226                && Objects.equals(moduleId, localizedMessage.moduleId)
227                && Objects.equals(key, localizedMessage.key)
228                && Objects.equals(bundle, localizedMessage.bundle)
229                && Objects.equals(sourceClass, localizedMessage.sourceClass)
230                && Objects.equals(customMessage, localizedMessage.customMessage)
231                && Arrays.equals(args, localizedMessage.args);
232    }
233
234    @Override
235    public int hashCode() {
236        return Objects.hash(lineNo, columnNo, severityLevel, moduleId, key, bundle, sourceClass,
237                customMessage, Arrays.hashCode(args));
238    }
239
240    /** Clears the cache. */
241    public static void clearCache() {
242        synchronized (BUNDLE_CACHE) {
243            BUNDLE_CACHE.clear();
244        }
245    }
246
247    /**
248     * Gets the translated message.
249     * @return the translated message
250     */
251    public String getMessage() {
252        String message = getCustomMessage();
253
254        if (message == null) {
255            try {
256                // Important to use the default class loader, and not the one in
257                // the GlobalProperties object. This is because the class loader in
258                // the GlobalProperties is specified by the user for resolving
259                // custom classes.
260                final ResourceBundle resourceBundle = getBundle(bundle);
261                final String pattern = resourceBundle.getString(key);
262                final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
263                message = formatter.format(args);
264            }
265            catch (final MissingResourceException ignored) {
266                // If the Check author didn't provide i18n resource bundles
267                // and logs error messages directly, this will return
268                // the author's original message
269                final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
270                message = formatter.format(args);
271            }
272        }
273        return message;
274    }
275
276    /**
277     * Returns the formatted custom message if one is configured.
278     * @return the formatted custom message or {@code null}
279     *          if there is no custom message
280     */
281    private String getCustomMessage() {
282        String message = null;
283        if (customMessage != null) {
284            final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
285            message = formatter.format(args);
286        }
287        return message;
288    }
289
290    /**
291     * Find a ResourceBundle for a given bundle name. Uses the classloader
292     * of the class emitting this message, to be sure to get the correct
293     * bundle.
294     * @param bundleName the bundle name
295     * @return a ResourceBundle
296     */
297    private ResourceBundle getBundle(String bundleName) {
298        synchronized (BUNDLE_CACHE) {
299            ResourceBundle resourceBundle = BUNDLE_CACHE
300                    .get(bundleName);
301            if (resourceBundle == null) {
302                resourceBundle = ResourceBundle.getBundle(bundleName, sLocale,
303                        sourceClass.getClassLoader(), new Utf8Control());
304                BUNDLE_CACHE.put(bundleName, resourceBundle);
305            }
306            return resourceBundle;
307        }
308    }
309
310    /**
311     * Gets the line number.
312     * @return the line number
313     */
314    public int getLineNo() {
315        return lineNo;
316    }
317
318    /**
319     * Gets the column number.
320     * @return the column number
321     */
322    public int getColumnNo() {
323        return columnNo;
324    }
325
326    /**
327     * Gets the severity level.
328     * @return the severity level
329     */
330    public SeverityLevel getSeverityLevel() {
331        return severityLevel;
332    }
333
334    /**
335     * Returns id of module.
336     * @return the module identifier.
337     */
338    public String getModuleId() {
339        return moduleId;
340    }
341
342    /**
343     * Returns the message key to locate the translation, can also be used
344     * in IDE plugins to map error messages to corrective actions.
345     *
346     * @return the message key
347     */
348    public String getKey() {
349        return key;
350    }
351
352    /**
353     * Gets the name of the source for this LocalizedMessage.
354     * @return the name of the source for this LocalizedMessage
355     */
356    public String getSourceName() {
357        return sourceClass.getName();
358    }
359
360    /**
361     * Sets a locale to use for localization.
362     * @param locale the locale to use for localization
363     */
364    public static void setLocale(Locale locale) {
365        clearCache();
366        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
367            sLocale = Locale.ROOT;
368        }
369        else {
370            sLocale = locale;
371        }
372    }
373
374    ////////////////////////////////////////////////////////////////////////////
375    // Interface Comparable methods
376    ////////////////////////////////////////////////////////////////////////////
377
378    @Override
379    public int compareTo(LocalizedMessage other) {
380        int result = Integer.compare(lineNo, other.lineNo);
381
382        if (lineNo == other.lineNo) {
383            if (columnNo == other.columnNo) {
384                result = getMessage().compareTo(other.getMessage());
385            }
386            else {
387                result = Integer.compare(columnNo, other.columnNo);
388            }
389        }
390        return result;
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    public static class Utf8Control extends Control {
402        @Override
403        public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
404                 ClassLoader aLoader, boolean aReload) throws IOException {
405            // The below is a copy of the default implementation.
406            final String bundleName = toBundleName(aBaseName, aLocale);
407            final String resourceName = toResourceName(bundleName, "properties");
408            InputStream stream = null;
409            if (aReload) {
410                final URL url = aLoader.getResource(resourceName);
411                if (url != null) {
412                    final URLConnection connection = url.openConnection();
413                    if (connection != null) {
414                        connection.setUseCaches(false);
415                        stream = connection.getInputStream();
416                    }
417                }
418            }
419            else {
420                stream = aLoader.getResourceAsStream(resourceName);
421            }
422            ResourceBundle resourceBundle = null;
423            if (stream != null) {
424                final Reader streamReader = new InputStreamReader(stream, "UTF-8");
425                try {
426                    // Only this line is changed to make it to read properties files as UTF-8.
427                    resourceBundle = new PropertyResourceBundle(streamReader);
428                }
429                finally {
430                    stream.close();
431                }
432            }
433            return resourceBundle;
434        }
435    }
436}