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