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.nio.charset.StandardCharsets;
030import java.text.MessageFormat;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.Locale;
035import java.util.Map;
036import java.util.MissingResourceException;
037import java.util.Objects;
038import java.util.PropertyResourceBundle;
039import java.util.ResourceBundle;
040import java.util.ResourceBundle.Control;
041
042/**
043 * Represents a message that can be localised. The translations come from
044 * message.properties files. The underlying implementation uses
045 * java.text.MessageFormat.
046 *
047 * @author Oliver Burn
048 * @author lkuehne
049 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
050 */
051public final class LocalizedMessage
052    implements Comparable<LocalizedMessage>, Serializable {
053    private static final long serialVersionUID = 5675176836184862150L;
054
055    /**
056     * A cache that maps bundle names to ResourceBundles.
057     * Avoids repetitive calls to ResourceBundle.getBundle().
058     */
059    private static final Map<String, ResourceBundle> BUNDLE_CACHE =
060        Collections.synchronizedMap(new HashMap<>());
061
062    /** The default severity level if one is not specified. */
063    private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
064
065    /** The locale to localise messages to. **/
066    private static Locale sLocale = Locale.getDefault();
067
068    /** The line number. **/
069    private final int lineNo;
070    /** The column number. **/
071    private final int columnNo;
072    /** The column char index. **/
073    private final int columnCharIndex;
074    /** The token type constant. See {@link TokenTypes}. **/
075    private final int tokenType;
076
077    /** The severity level. **/
078    private final SeverityLevel severityLevel;
079
080    /** The id of the module generating the message. */
081    private final String moduleId;
082
083    /** Key for the message format. **/
084    private final String key;
085
086    /** Arguments for MessageFormat.
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 error 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     * @noinspection ConstructorWithTooManyParameters
276     */
277    public LocalizedMessage(
278        int lineNo,
279        String bundle,
280        String key,
281        Object[] args,
282        String moduleId,
283        Class<?> sourceClass,
284        String customMessage) {
285        this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
286                sourceClass, customMessage);
287    }
288
289    // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
290    @Override
291    public boolean equals(Object object) {
292        if (this == object) {
293            return true;
294        }
295        if (object == null || getClass() != object.getClass()) {
296            return false;
297        }
298        final LocalizedMessage localizedMessage = (LocalizedMessage) object;
299        return Objects.equals(lineNo, localizedMessage.lineNo)
300                && Objects.equals(columnNo, localizedMessage.columnNo)
301                && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
302                && Objects.equals(tokenType, localizedMessage.tokenType)
303                && Objects.equals(severityLevel, localizedMessage.severityLevel)
304                && Objects.equals(moduleId, localizedMessage.moduleId)
305                && Objects.equals(key, localizedMessage.key)
306                && Objects.equals(bundle, localizedMessage.bundle)
307                && Objects.equals(sourceClass, localizedMessage.sourceClass)
308                && Objects.equals(customMessage, localizedMessage.customMessage)
309                && Arrays.equals(args, localizedMessage.args);
310    }
311
312    @Override
313    public int hashCode() {
314        return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
315                key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
316    }
317
318    /** Clears the cache. */
319    public static void clearCache() {
320        BUNDLE_CACHE.clear();
321    }
322
323    /**
324     * Gets the translated message.
325     * @return the translated message
326     */
327    public String getMessage() {
328        String message = getCustomMessage();
329
330        if (message == null) {
331            try {
332                // Important to use the default class loader, and not the one in
333                // the GlobalProperties object. This is because the class loader in
334                // the GlobalProperties is specified by the user for resolving
335                // custom classes.
336                final ResourceBundle resourceBundle = getBundle(bundle);
337                final String pattern = resourceBundle.getString(key);
338                final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
339                message = formatter.format(args);
340            }
341            catch (final MissingResourceException ignored) {
342                // If the Check author didn't provide i18n resource bundles
343                // and logs error messages directly, this will return
344                // the author's original message
345                final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
346                message = formatter.format(args);
347            }
348        }
349        return message;
350    }
351
352    /**
353     * Returns the formatted custom message if one is configured.
354     * @return the formatted custom message or {@code null}
355     *          if there is no custom message
356     */
357    private String getCustomMessage() {
358        String message = null;
359        if (customMessage != null) {
360            final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
361            message = formatter.format(args);
362        }
363        return message;
364    }
365
366    /**
367     * Find a ResourceBundle for a given bundle name. Uses the classloader
368     * of the class emitting this message, to be sure to get the correct
369     * bundle.
370     * @param bundleName the bundle name
371     * @return a ResourceBundle
372     */
373    private ResourceBundle getBundle(String bundleName) {
374        return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
375                name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
376    }
377
378    /**
379     * Gets the line number.
380     * @return the line number
381     */
382    public int getLineNo() {
383        return lineNo;
384    }
385
386    /**
387     * Gets the column number.
388     * @return the column number
389     */
390    public int getColumnNo() {
391        return columnNo;
392    }
393
394    /**
395     * Gets the column char index.
396     * @return the column char index
397     */
398    public int getColumnCharIndex() {
399        return columnCharIndex;
400    }
401
402    /**
403     * Gets the token type.
404     * @return the token type
405     */
406    public int getTokenType() {
407        return tokenType;
408    }
409
410    /**
411     * Gets the severity level.
412     * @return the severity level
413     */
414    public SeverityLevel getSeverityLevel() {
415        return severityLevel;
416    }
417
418    /**
419     * Returns id of module.
420     * @return the module identifier.
421     */
422    public String getModuleId() {
423        return moduleId;
424    }
425
426    /**
427     * Returns the message key to locate the translation, can also be used
428     * in IDE plugins to map error messages to corrective actions.
429     *
430     * @return the message key
431     */
432    public String getKey() {
433        return key;
434    }
435
436    /**
437     * Gets the name of the source for this LocalizedMessage.
438     * @return the name of the source for this LocalizedMessage
439     */
440    public String getSourceName() {
441        return sourceClass.getName();
442    }
443
444    /**
445     * Sets a locale to use for localization.
446     * @param locale the locale to use for localization
447     */
448    public static void setLocale(Locale locale) {
449        clearCache();
450        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
451            sLocale = Locale.ROOT;
452        }
453        else {
454            sLocale = locale;
455        }
456    }
457
458    ////////////////////////////////////////////////////////////////////////////
459    // Interface Comparable methods
460    ////////////////////////////////////////////////////////////////////////////
461
462    @Override
463    public int compareTo(LocalizedMessage other) {
464        final int result;
465
466        if (lineNo == other.lineNo) {
467            if (columnNo == other.columnNo) {
468                if (Objects.equals(moduleId, other.moduleId)) {
469                    result = getMessage().compareTo(other.getMessage());
470                }
471                else if (moduleId == null) {
472                    result = -1;
473                }
474                else if (other.moduleId == null) {
475                    result = 1;
476                }
477                else {
478                    result = moduleId.compareTo(other.moduleId);
479                }
480            }
481            else {
482                result = Integer.compare(columnNo, other.columnNo);
483            }
484        }
485        else {
486            result = Integer.compare(lineNo, other.lineNo);
487        }
488        return result;
489    }
490
491    /**
492     * <p>
493     * Custom ResourceBundle.Control implementation which allows explicitly read
494     * the properties files as UTF-8.
495     * </p>
496     *
497     * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
498     * @noinspection IOResourceOpenedButNotSafelyClosed
499     */
500    public static class Utf8Control extends Control {
501        @Override
502        public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
503                 ClassLoader aLoader, boolean aReload) throws IOException {
504            // The below is a copy of the default implementation.
505            final String bundleName = toBundleName(aBaseName, aLocale);
506            final String resourceName = toResourceName(bundleName, "properties");
507            InputStream stream = null;
508            if (aReload) {
509                final URL url = aLoader.getResource(resourceName);
510                if (url != null) {
511                    final URLConnection connection = url.openConnection();
512                    if (connection != null) {
513                        connection.setUseCaches(false);
514                        stream = connection.getInputStream();
515                    }
516                }
517            }
518            else {
519                stream = aLoader.getResourceAsStream(resourceName);
520            }
521            ResourceBundle resourceBundle = null;
522            if (stream != null) {
523                final Reader streamReader = new InputStreamReader(stream,
524                        StandardCharsets.UTF_8.name());
525                try {
526                    // Only this line is changed to make it to read properties files as UTF-8.
527                    resourceBundle = new PropertyResourceBundle(streamReader);
528                }
529                finally {
530                    stream.close();
531                }
532            }
533            return resourceBundle;
534        }
535    }
536}