//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.api; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; import java.net.URL; import java.net.URLConnection; import java.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import java.util.ResourceBundle.Control; /** * Represents a message that can be localised. The translations come from * message.properties files. The underlying implementation uses * java.text.MessageFormat. * * @author Oliver Burn * @author lkuehne */ public final class LocalizedMessage implements Comparable<LocalizedMessage>, Serializable { private static final long serialVersionUID = 5675176836184862150L; /** * A cache that maps bundle names to ResourceBundles. * Avoids repetitive calls to ResourceBundle.getBundle(). */ private static final Map<String, ResourceBundle> BUNDLE_CACHE = Collections.synchronizedMap(new HashMap<>()); /** The default severity level if one is not specified. */ private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR; /** The locale to localise messages to. **/ private static Locale sLocale = Locale.getDefault(); /** The line number. **/ private final int lineNo; /** The column number. **/ private final int columnNo; /** The severity level. **/ private final SeverityLevel severityLevel; /** The id of the module generating the message. */ private final String moduleId; /** Key for the message format. **/ private final String key; /** Arguments for MessageFormat. **/ private final Object[] args; /** Name of the resource bundle to get messages from. **/ private final String bundle; /** Class of the source for this LocalizedMessage. */ private final Class<?> sourceClass; /** A custom message overriding the default message from the bundle. */ private final String customMessage; /** * Creates a new {@code LocalizedMessage} instance. * * @param lineNo line number associated with the message * @param columnNo column number associated with the message * @param bundle resource bundle name * @param key the key to locate the translation * @param args arguments for the translation * @param severityLevel severity level for the message * @param moduleId the id of the module the message is associated with * @param sourceClass the Class that is the source of the message * @param customMessage optional custom message overriding the default */ // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. public LocalizedMessage(int lineNo, int columnNo, String bundle, String key, Object[] args, SeverityLevel severityLevel, String moduleId, Class<?> sourceClass, String customMessage) { this.lineNo = lineNo; this.columnNo = columnNo; this.key = key; if (args == null) { this.args = null; } else { this.args = Arrays.copyOf(args, args.length); } this.bundle = bundle; this.severityLevel = severityLevel; this.moduleId = moduleId; this.sourceClass = sourceClass; this.customMessage = customMessage; } /** * Creates a new {@code LocalizedMessage} instance. * * @param lineNo line number associated with the message * @param columnNo column number associated with the message * @param bundle resource bundle name * @param key the key to locate the translation * @param args arguments for the translation * @param moduleId the id of the module the message is associated with * @param sourceClass the Class that is the source of the message * @param customMessage optional custom message overriding the default */ // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. public LocalizedMessage(int lineNo, int columnNo, String bundle, String key, Object[] args, String moduleId, Class<?> sourceClass, String customMessage) { this(lineNo, columnNo, bundle, key, args, DEFAULT_SEVERITY, moduleId, sourceClass, customMessage); } /** * Creates a new {@code LocalizedMessage} instance. * * @param lineNo line number associated with the message * @param bundle resource bundle name * @param key the key to locate the translation * @param args arguments for the translation * @param severityLevel severity level for the message * @param moduleId the id of the module the message is associated with * @param sourceClass the source class for the message * @param customMessage optional custom message overriding the default */ // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. public LocalizedMessage(int lineNo, String bundle, String key, Object[] args, SeverityLevel severityLevel, String moduleId, Class<?> sourceClass, String customMessage) { this(lineNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass, customMessage); } /** * Creates a new {@code LocalizedMessage} instance. The column number * defaults to 0. * * @param lineNo line number associated with the message * @param bundle name of a resource bundle that contains error messages * @param key the key to locate the translation * @param args arguments for the translation * @param moduleId the id of the module the message is associated with * @param sourceClass the name of the source for the message * @param customMessage optional custom message overriding the default */ public LocalizedMessage( int lineNo, String bundle, String key, Object[] args, String moduleId, Class<?> sourceClass, String customMessage) { this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId, sourceClass, customMessage); } // -@cs[CyclomaticComplexity] equals - a lot of fields to check. @Override public boolean equals(Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } final LocalizedMessage localizedMessage = (LocalizedMessage) object; return Objects.equals(lineNo, localizedMessage.lineNo) && Objects.equals(columnNo, localizedMessage.columnNo) && Objects.equals(severityLevel, localizedMessage.severityLevel) && Objects.equals(moduleId, localizedMessage.moduleId) && Objects.equals(key, localizedMessage.key) && Objects.equals(bundle, localizedMessage.bundle) && Objects.equals(sourceClass, localizedMessage.sourceClass) && Objects.equals(customMessage, localizedMessage.customMessage) && Arrays.equals(args, localizedMessage.args); } @Override public int hashCode() { return Objects.hash(lineNo, columnNo, severityLevel, moduleId, key, bundle, sourceClass, customMessage, Arrays.hashCode(args)); } /** Clears the cache. */ public static void clearCache() { synchronized (BUNDLE_CACHE) { BUNDLE_CACHE.clear(); } } /** * Gets the translated message. * @return the translated message */ public String getMessage() { String message = getCustomMessage(); if (message == null) { try { // Important to use the default class loader, and not the one in // the GlobalProperties object. This is because the class loader in // the GlobalProperties is specified by the user for resolving // custom classes. final ResourceBundle resourceBundle = getBundle(bundle); final String pattern = resourceBundle.getString(key); final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); message = formatter.format(args); } catch (final MissingResourceException ignored) { // If the Check author didn't provide i18n resource bundles // and logs error messages directly, this will return // the author's original message final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); message = formatter.format(args); } } return message; } /** * Returns the formatted custom message if one is configured. * @return the formatted custom message or {@code null} * if there is no custom message */ private String getCustomMessage() { String message = null; if (customMessage != null) { final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); message = formatter.format(args); } return message; } /** * Find a ResourceBundle for a given bundle name. Uses the classloader * of the class emitting this message, to be sure to get the correct * bundle. * @param bundleName the bundle name * @return a ResourceBundle */ private ResourceBundle getBundle(String bundleName) { synchronized (BUNDLE_CACHE) { ResourceBundle resourceBundle = BUNDLE_CACHE .get(bundleName); if (resourceBundle == null) { resourceBundle = ResourceBundle.getBundle(bundleName, sLocale, sourceClass.getClassLoader(), new Utf8Control()); BUNDLE_CACHE.put(bundleName, resourceBundle); } return resourceBundle; } } /** * Gets the line number. * @return the line number */ public int getLineNo() { return lineNo; } /** * Gets the column number. * @return the column number */ public int getColumnNo() { return columnNo; } /** * Gets the severity level. * @return the severity level */ public SeverityLevel getSeverityLevel() { return severityLevel; } /** * @return the module identifier. */ public String getModuleId() { return moduleId; } /** * Returns the message key to locate the translation, can also be used * in IDE plugins to map error messages to corrective actions. * * @return the message key */ public String getKey() { return key; } /** * Gets the name of the source for this LocalizedMessage. * @return the name of the source for this LocalizedMessage */ public String getSourceName() { return sourceClass.getName(); } /** * Sets a locale to use for localization. * @param locale the locale to use for localization */ public static void setLocale(Locale locale) { clearCache(); if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { sLocale = Locale.ROOT; } else { sLocale = locale; } } //////////////////////////////////////////////////////////////////////////// // Interface Comparable methods //////////////////////////////////////////////////////////////////////////// @Override public int compareTo(LocalizedMessage other) { int result = Integer.compare(lineNo, other.lineNo); if (lineNo == other.lineNo) { if (columnNo == other.columnNo) { result = getMessage().compareTo(other.getMessage()); } else { result = Integer.compare(columnNo, other.columnNo); } } return result; } /** * <p> * Custom ResourceBundle.Control implementation which allows explicitly read * the properties files as UTF-8. * </p> * * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> */ public static class Utf8Control extends Control { @Override public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat, ClassLoader aLoader, boolean aReload) throws IOException { // The below is a copy of the default implementation. final String bundleName = toBundleName(aBaseName, aLocale); final String resourceName = toResourceName(bundleName, "properties"); InputStream stream = null; if (aReload) { final URL url = aLoader.getResource(resourceName); if (url != null) { final URLConnection connection = url.openConnection(); if (connection != null) { connection.setUseCaches(false); stream = connection.getInputStream(); } } } else { stream = aLoader.getResourceAsStream(resourceName); } ResourceBundle resourceBundle = null; if (stream != null) { final Reader streamReader = new InputStreamReader(stream, "UTF-8"); try { // Only this line is changed to make it to read properties files as UTF-8. resourceBundle = new PropertyResourceBundle(streamReader); } finally { stream.close(); } } return resourceBundle; } } }