001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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    /**
287     * Indicates whether some other object is "equal to" this one.
288     * Suppression on enumeration is needed so code stays consistent.
289     * @noinspection EqualsCalledOnEnumConstant
290     */
291    // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
292    @Override
293    public boolean equals(Object object) {
294        if (this == object) {
295            return true;
296        }
297        if (object == null || getClass() != object.getClass()) {
298            return false;
299        }
300        final LocalizedMessage localizedMessage = (LocalizedMessage) object;
301        return Objects.equals(lineNo, localizedMessage.lineNo)
302                && Objects.equals(columnNo, localizedMessage.columnNo)
303                && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
304                && Objects.equals(tokenType, localizedMessage.tokenType)
305                && Objects.equals(severityLevel, localizedMessage.severityLevel)
306                && Objects.equals(moduleId, localizedMessage.moduleId)
307                && Objects.equals(key, localizedMessage.key)
308                && Objects.equals(bundle, localizedMessage.bundle)
309                && Objects.equals(sourceClass, localizedMessage.sourceClass)
310                && Objects.equals(customMessage, localizedMessage.customMessage)
311                && Arrays.equals(args, localizedMessage.args);
312    }
313
314    @Override
315    public int hashCode() {
316        return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
317                key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
318    }
319
320    /** Clears the cache. */
321    public static void clearCache() {
322        BUNDLE_CACHE.clear();
323    }
324
325    /**
326     * Gets the translated message.
327     * @return the translated message
328     */
329    public String getMessage() {
330        String message = getCustomMessage();
331
332        if (message == null) {
333            try {
334                // Important to use the default class loader, and not the one in
335                // the GlobalProperties object. This is because the class loader in
336                // the GlobalProperties is specified by the user for resolving
337                // custom classes.
338                final ResourceBundle resourceBundle = getBundle(bundle);
339                final String pattern = resourceBundle.getString(key);
340                final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
341                message = formatter.format(args);
342            }
343            catch (final MissingResourceException ignored) {
344                // If the Check author didn't provide i18n resource bundles
345                // and logs error messages directly, this will return
346                // the author's original message
347                final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
348                message = formatter.format(args);
349            }
350        }
351        return message;
352    }
353
354    /**
355     * Returns the formatted custom message if one is configured.
356     * @return the formatted custom message or {@code null}
357     *          if there is no custom message
358     */
359    private String getCustomMessage() {
360        String message = null;
361        if (customMessage != null) {
362            final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
363            message = formatter.format(args);
364        }
365        return message;
366    }
367
368    /**
369     * Find a ResourceBundle for a given bundle name. Uses the classloader
370     * of the class emitting this message, to be sure to get the correct
371     * bundle.
372     * @param bundleName the bundle name
373     * @return a ResourceBundle
374     */
375    private ResourceBundle getBundle(String bundleName) {
376        return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
377                name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
378    }
379
380    /**
381     * Gets the line number.
382     * @return the line number
383     */
384    public int getLineNo() {
385        return lineNo;
386    }
387
388    /**
389     * Gets the column number.
390     * @return the column number
391     */
392    public int getColumnNo() {
393        return columnNo;
394    }
395
396    /**
397     * Gets the column char index.
398     * @return the column char index
399     */
400    public int getColumnCharIndex() {
401        return columnCharIndex;
402    }
403
404    /**
405     * Gets the token type.
406     * @return the token type
407     */
408    public int getTokenType() {
409        return tokenType;
410    }
411
412    /**
413     * Gets the severity level.
414     * @return the severity level
415     */
416    public SeverityLevel getSeverityLevel() {
417        return severityLevel;
418    }
419
420    /**
421     * Returns id of module.
422     * @return the module identifier.
423     */
424    public String getModuleId() {
425        return moduleId;
426    }
427
428    /**
429     * Returns the message key to locate the translation, can also be used
430     * in IDE plugins to map error messages to corrective actions.
431     *
432     * @return the message key
433     */
434    public String getKey() {
435        return key;
436    }
437
438    /**
439     * Gets the name of the source for this LocalizedMessage.
440     * @return the name of the source for this LocalizedMessage
441     */
442    public String getSourceName() {
443        return sourceClass.getName();
444    }
445
446    /**
447     * Sets a locale to use for localization.
448     * @param locale the locale to use for localization
449     */
450    public static void setLocale(Locale locale) {
451        clearCache();
452        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
453            sLocale = Locale.ROOT;
454        }
455        else {
456            sLocale = locale;
457        }
458    }
459
460    ////////////////////////////////////////////////////////////////////////////
461    // Interface Comparable methods
462    ////////////////////////////////////////////////////////////////////////////
463
464    @Override
465    public int compareTo(LocalizedMessage other) {
466        final int result;
467
468        if (lineNo == other.lineNo) {
469            if (columnNo == other.columnNo) {
470                if (Objects.equals(moduleId, other.moduleId)) {
471                    result = getMessage().compareTo(other.getMessage());
472                }
473                else if (moduleId == null) {
474                    result = -1;
475                }
476                else if (other.moduleId == null) {
477                    result = 1;
478                }
479                else {
480                    result = moduleId.compareTo(other.moduleId);
481                }
482            }
483            else {
484                result = Integer.compare(columnNo, other.columnNo);
485            }
486        }
487        else {
488            result = Integer.compare(lineNo, other.lineNo);
489        }
490        return result;
491    }
492
493    /**
494     * <p>
495     * Custom ResourceBundle.Control implementation which allows explicitly read
496     * the properties files as UTF-8.
497     * </p>
498     */
499    public static class Utf8Control extends Control {
500
501        @Override
502        public ResourceBundle newBundle(String baseName, Locale locale, String format,
503                 ClassLoader loader, boolean reload) throws IOException {
504            // The below is a copy of the default implementation.
505            final String bundleName = toBundleName(baseName, locale);
506            final String resourceName = toResourceName(bundleName, "properties");
507            final URL url = loader.getResource(resourceName);
508            ResourceBundle resourceBundle = null;
509            if (url != null) {
510                final URLConnection connection = url.openConnection();
511                if (connection != null) {
512                    connection.setUseCaches(!reload);
513                    try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
514                            StandardCharsets.UTF_8.name())) {
515                        // Only this line is changed to make it read property files as UTF-8.
516                        resourceBundle = new PropertyResourceBundle(streamReader);
517                    }
518                }
519            }
520            return resourceBundle;
521        }
522
523    }
524
525}