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