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