001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.InputStreamReader;
024import java.io.Reader;
025import java.io.Serializable;
026import java.net.URL;
027import java.net.URLConnection;
028import java.nio.charset.StandardCharsets;
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 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
047 */
048public final class LocalizedMessage
049    implements Comparable<LocalizedMessage>, Serializable {
050
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    /** The column char index. **/
071    private final int columnCharIndex;
072    /** The token type constant. See {@link TokenTypes}. **/
073    private final int tokenType;
074
075    /** The severity level. **/
076    private final SeverityLevel severityLevel;
077
078    /** The id of the module generating the message. */
079    private final String moduleId;
080
081    /** Key for the message format. **/
082    private final String key;
083
084    /** Arguments for MessageFormat.
085     * @noinspection NonSerializableFieldInSerializableClass
086     */
087    private final Object[] args;
088
089    /** Name of the resource bundle to get messages from. **/
090    private final String bundle;
091
092    /** Class of the source for this LocalizedMessage. */
093    private final Class<?> sourceClass;
094
095    /** A custom message overriding the default message from the bundle. */
096    private final String customMessage;
097
098    /**
099     * Creates a new {@code LocalizedMessage} instance.
100     *
101     * @param lineNo line number associated with the message
102     * @param columnNo column number associated with the message
103     * @param columnCharIndex column char index associated with the message
104     * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
105     * @param bundle resource bundle name
106     * @param key the key to locate the translation
107     * @param args arguments for the translation
108     * @param severityLevel severity level for the message
109     * @param moduleId the id of the module the message is associated with
110     * @param sourceClass the Class that is the source of the message
111     * @param customMessage optional custom message overriding the default
112     * @noinspection ConstructorWithTooManyParameters
113     */
114    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
115    public LocalizedMessage(int lineNo,
116                            int columnNo,
117                            int columnCharIndex,
118                            int tokenType,
119                            String bundle,
120                            String key,
121                            Object[] args,
122                            SeverityLevel severityLevel,
123                            String moduleId,
124                            Class<?> sourceClass,
125                            String customMessage) {
126        this.lineNo = lineNo;
127        this.columnNo = columnNo;
128        this.columnCharIndex = columnCharIndex;
129        this.tokenType = tokenType;
130        this.key = key;
131
132        if (args == null) {
133            this.args = null;
134        }
135        else {
136            this.args = Arrays.copyOf(args, args.length);
137        }
138        this.bundle = bundle;
139        this.severityLevel = severityLevel;
140        this.moduleId = moduleId;
141        this.sourceClass = sourceClass;
142        this.customMessage = customMessage;
143    }
144
145    /**
146     * Creates a new {@code LocalizedMessage} instance.
147     *
148     * @param lineNo line number associated with the message
149     * @param columnNo column number associated with the message
150     * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
151     * @param bundle resource bundle name
152     * @param key the key to locate the translation
153     * @param args arguments for the translation
154     * @param severityLevel severity level for the message
155     * @param moduleId the id of the module the message is associated with
156     * @param sourceClass the Class that is the source of the message
157     * @param customMessage optional custom message overriding the default
158     * @noinspection ConstructorWithTooManyParameters
159     */
160    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
161    public LocalizedMessage(int lineNo,
162                            int columnNo,
163                            int tokenType,
164                            String bundle,
165                            String key,
166                            Object[] args,
167                            SeverityLevel severityLevel,
168                            String moduleId,
169                            Class<?> sourceClass,
170                            String customMessage) {
171        this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId,
172                sourceClass, customMessage);
173    }
174
175    /**
176     * Creates a new {@code LocalizedMessage} instance.
177     *
178     * @param lineNo line number associated with the message
179     * @param columnNo column number associated with the message
180     * @param bundle resource bundle name
181     * @param key the key to locate the translation
182     * @param args arguments for the translation
183     * @param severityLevel severity level for the message
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     * @noinspection ConstructorWithTooManyParameters
188     */
189    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
190    public LocalizedMessage(int lineNo,
191                            int columnNo,
192                            String bundle,
193                            String key,
194                            Object[] args,
195                            SeverityLevel severityLevel,
196                            String moduleId,
197                            Class<?> sourceClass,
198                            String customMessage) {
199        this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass,
200                customMessage);
201    }
202
203    /**
204     * Creates a new {@code LocalizedMessage} instance.
205     *
206     * @param lineNo line number associated with the message
207     * @param columnNo column number associated with the message
208     * @param bundle resource bundle name
209     * @param key the key to locate the translation
210     * @param args arguments for the translation
211     * @param moduleId the id of the module the message is associated with
212     * @param sourceClass the Class that is the source of the message
213     * @param customMessage optional custom message overriding the default
214     * @noinspection ConstructorWithTooManyParameters
215     */
216    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
217    public LocalizedMessage(int lineNo,
218                            int columnNo,
219                            String bundle,
220                            String key,
221                            Object[] args,
222                            String moduleId,
223                            Class<?> sourceClass,
224                            String customMessage) {
225        this(lineNo,
226                columnNo,
227             bundle,
228             key,
229             args,
230             DEFAULT_SEVERITY,
231             moduleId,
232             sourceClass,
233             customMessage);
234    }
235
236    /**
237     * Creates a new {@code LocalizedMessage} instance.
238     *
239     * @param lineNo line number associated with the message
240     * @param bundle resource bundle name
241     * @param key the key to locate the translation
242     * @param args arguments for the translation
243     * @param severityLevel severity level for the message
244     * @param moduleId the id of the module the message is associated with
245     * @param sourceClass the source class for the message
246     * @param customMessage optional custom message overriding the default
247     * @noinspection ConstructorWithTooManyParameters
248     */
249    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
250    public LocalizedMessage(int lineNo,
251                            String bundle,
252                            String key,
253                            Object[] args,
254                            SeverityLevel severityLevel,
255                            String moduleId,
256                            Class<?> sourceClass,
257                            String customMessage) {
258        this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
259                sourceClass, customMessage);
260    }
261
262    /**
263     * Creates a new {@code LocalizedMessage} instance. The column number
264     * defaults to 0.
265     *
266     * @param lineNo line number associated with the message
267     * @param bundle name of a resource bundle that contains error messages
268     * @param key the key to locate the translation
269     * @param args arguments for the translation
270     * @param moduleId the id of the module the message is associated with
271     * @param sourceClass the name of the source for the message
272     * @param customMessage optional custom message overriding the default
273     */
274    public LocalizedMessage(
275        int lineNo,
276        String bundle,
277        String key,
278        Object[] args,
279        String moduleId,
280        Class<?> sourceClass,
281        String customMessage) {
282        this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
283                sourceClass, customMessage);
284    }
285
286    // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
287    @Override
288    public boolean equals(Object object) {
289        if (this == object) {
290            return true;
291        }
292        if (object == null || getClass() != object.getClass()) {
293            return false;
294        }
295        final LocalizedMessage localizedMessage = (LocalizedMessage) object;
296        return Objects.equals(lineNo, localizedMessage.lineNo)
297                && Objects.equals(columnNo, localizedMessage.columnNo)
298                && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
299                && Objects.equals(tokenType, localizedMessage.tokenType)
300                && Objects.equals(severityLevel, localizedMessage.severityLevel)
301                && Objects.equals(moduleId, localizedMessage.moduleId)
302                && Objects.equals(key, localizedMessage.key)
303                && Objects.equals(bundle, localizedMessage.bundle)
304                && Objects.equals(sourceClass, localizedMessage.sourceClass)
305                && Objects.equals(customMessage, localizedMessage.customMessage)
306                && Arrays.equals(args, localizedMessage.args);
307    }
308
309    @Override
310    public int hashCode() {
311        return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
312                key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
313    }
314
315    /** Clears the cache. */
316    public static void clearCache() {
317        BUNDLE_CACHE.clear();
318    }
319
320    /**
321     * Gets the translated message.
322     * @return the translated message
323     */
324    public String getMessage() {
325        String message = getCustomMessage();
326
327        if (message == null) {
328            try {
329                // Important to use the default class loader, and not the one in
330                // the GlobalProperties object. This is because the class loader in
331                // the GlobalProperties is specified by the user for resolving
332                // custom classes.
333                final ResourceBundle resourceBundle = getBundle(bundle);
334                final String pattern = resourceBundle.getString(key);
335                final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
336                message = formatter.format(args);
337            }
338            catch (final MissingResourceException ignored) {
339                // If the Check author didn't provide i18n resource bundles
340                // and logs error messages directly, this will return
341                // the author's original message
342                final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
343                message = formatter.format(args);
344            }
345        }
346        return message;
347    }
348
349    /**
350     * Returns the formatted custom message if one is configured.
351     * @return the formatted custom message or {@code null}
352     *          if there is no custom message
353     */
354    private String getCustomMessage() {
355        String message = null;
356        if (customMessage != null) {
357            final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
358            message = formatter.format(args);
359        }
360        return message;
361    }
362
363    /**
364     * Find a ResourceBundle for a given bundle name. Uses the classloader
365     * of the class emitting this message, to be sure to get the correct
366     * bundle.
367     * @param bundleName the bundle name
368     * @return a ResourceBundle
369     */
370    private ResourceBundle getBundle(String bundleName) {
371        return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
372                name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
373    }
374
375    /**
376     * Gets the line number.
377     * @return the line number
378     */
379    public int getLineNo() {
380        return lineNo;
381    }
382
383    /**
384     * Gets the column number.
385     * @return the column number
386     */
387    public int getColumnNo() {
388        return columnNo;
389    }
390
391    /**
392     * Gets the column char index.
393     * @return the column char index
394     */
395    public int getColumnCharIndex() {
396        return columnCharIndex;
397    }
398
399    /**
400     * Gets the token type.
401     * @return the token type
402     */
403    public int getTokenType() {
404        return tokenType;
405    }
406
407    /**
408     * Gets the severity level.
409     * @return the severity level
410     */
411    public SeverityLevel getSeverityLevel() {
412        return severityLevel;
413    }
414
415    /**
416     * Returns id of module.
417     * @return the module identifier.
418     */
419    public String getModuleId() {
420        return moduleId;
421    }
422
423    /**
424     * Returns the message key to locate the translation, can also be used
425     * in IDE plugins to map error messages to corrective actions.
426     *
427     * @return the message key
428     */
429    public String getKey() {
430        return key;
431    }
432
433    /**
434     * Gets the name of the source for this LocalizedMessage.
435     * @return the name of the source for this LocalizedMessage
436     */
437    public String getSourceName() {
438        return sourceClass.getName();
439    }
440
441    /**
442     * Sets a locale to use for localization.
443     * @param locale the locale to use for localization
444     */
445    public static void setLocale(Locale locale) {
446        clearCache();
447        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
448            sLocale = Locale.ROOT;
449        }
450        else {
451            sLocale = locale;
452        }
453    }
454
455    ////////////////////////////////////////////////////////////////////////////
456    // Interface Comparable methods
457    ////////////////////////////////////////////////////////////////////////////
458
459    @Override
460    public int compareTo(LocalizedMessage other) {
461        final int result;
462
463        if (lineNo == other.lineNo) {
464            if (columnNo == other.columnNo) {
465                if (Objects.equals(moduleId, other.moduleId)) {
466                    result = getMessage().compareTo(other.getMessage());
467                }
468                else if (moduleId == null) {
469                    result = -1;
470                }
471                else if (other.moduleId == null) {
472                    result = 1;
473                }
474                else {
475                    result = moduleId.compareTo(other.moduleId);
476                }
477            }
478            else {
479                result = Integer.compare(columnNo, other.columnNo);
480            }
481        }
482        else {
483            result = Integer.compare(lineNo, other.lineNo);
484        }
485        return result;
486    }
487
488    /**
489     * <p>
490     * Custom ResourceBundle.Control implementation which allows explicitly read
491     * the properties files as UTF-8.
492     * </p>
493     */
494    public static class Utf8Control extends Control {
495
496        @Override
497        public ResourceBundle newBundle(String baseName, Locale locale, String format,
498                 ClassLoader loader, boolean reload) throws IOException {
499            // The below is a copy of the default implementation.
500            final String bundleName = toBundleName(baseName, locale);
501            final String resourceName = toResourceName(bundleName, "properties");
502            final URL url = loader.getResource(resourceName);
503            ResourceBundle resourceBundle = null;
504            if (url != null) {
505                final URLConnection connection = url.openConnection();
506                if (connection != null) {
507                    connection.setUseCaches(!reload);
508                    try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
509                            StandardCharsets.UTF_8.name())) {
510                        // Only this line is changed to make it read property files as UTF-8.
511                        resourceBundle = new PropertyResourceBundle(streamReader);
512                    }
513                }
514            }
515            return resourceBundle;
516        }
517
518    }
519
520}