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