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