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 */ 049public final class LocalizedMessage 050 implements Comparable<LocalizedMessage>, Serializable { 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 071 /** The severity level. **/ 072 private final SeverityLevel severityLevel; 073 074 /** The id of the module generating the message. */ 075 private final String moduleId; 076 077 /** Key for the message format. **/ 078 private final String key; 079 080 /** Arguments for MessageFormat. **/ 081 private final Object[] args; 082 083 /** Name of the resource bundle to get messages from. **/ 084 private final String bundle; 085 086 /** Class of the source for this LocalizedMessage. */ 087 private final Class<?> sourceClass; 088 089 /** A custom message overriding the default message from the bundle. */ 090 private final String customMessage; 091 092 /** 093 * Creates a new {@code LocalizedMessage} instance. 094 * 095 * @param lineNo line number associated with the message 096 * @param columnNo column number associated with the message 097 * @param bundle resource bundle name 098 * @param key the key to locate the translation 099 * @param args arguments for the translation 100 * @param severityLevel severity level for the message 101 * @param moduleId the id of the module the message is associated with 102 * @param sourceClass the Class that is the source of the message 103 * @param customMessage optional custom message overriding the default 104 */ 105 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 106 public LocalizedMessage(int lineNo, 107 int columnNo, 108 String bundle, 109 String key, 110 Object[] args, 111 SeverityLevel severityLevel, 112 String moduleId, 113 Class<?> sourceClass, 114 String customMessage) { 115 this.lineNo = lineNo; 116 this.columnNo = columnNo; 117 this.key = key; 118 119 if (args == null) { 120 this.args = null; 121 } 122 else { 123 this.args = Arrays.copyOf(args, args.length); 124 } 125 this.bundle = bundle; 126 this.severityLevel = severityLevel; 127 this.moduleId = moduleId; 128 this.sourceClass = sourceClass; 129 this.customMessage = customMessage; 130 } 131 132 /** 133 * Creates a new {@code LocalizedMessage} instance. 134 * 135 * @param lineNo line number associated with the message 136 * @param columnNo column number associated with the message 137 * @param bundle resource bundle name 138 * @param key the key to locate the translation 139 * @param args arguments for the translation 140 * @param moduleId the id of the module the message is associated with 141 * @param sourceClass the Class that is the source of the message 142 * @param customMessage optional custom message overriding the default 143 */ 144 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 145 public LocalizedMessage(int lineNo, 146 int columnNo, 147 String bundle, 148 String key, 149 Object[] args, 150 String moduleId, 151 Class<?> sourceClass, 152 String customMessage) { 153 this(lineNo, 154 columnNo, 155 bundle, 156 key, 157 args, 158 DEFAULT_SEVERITY, 159 moduleId, 160 sourceClass, 161 customMessage); 162 } 163 164 /** 165 * Creates a new {@code LocalizedMessage} instance. 166 * 167 * @param lineNo line number associated with the message 168 * @param bundle resource bundle name 169 * @param key the key to locate the translation 170 * @param args arguments for the translation 171 * @param severityLevel severity level for the message 172 * @param moduleId the id of the module the message is associated with 173 * @param sourceClass the source class for the message 174 * @param customMessage optional custom message overriding the default 175 */ 176 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 177 public LocalizedMessage(int lineNo, 178 String bundle, 179 String key, 180 Object[] args, 181 SeverityLevel severityLevel, 182 String moduleId, 183 Class<?> sourceClass, 184 String customMessage) { 185 this(lineNo, 0, bundle, key, args, severityLevel, moduleId, 186 sourceClass, customMessage); 187 } 188 189 /** 190 * Creates a new {@code LocalizedMessage} instance. The column number 191 * defaults to 0. 192 * 193 * @param lineNo line number associated with the message 194 * @param bundle name of a resource bundle that contains error messages 195 * @param key the key to locate the translation 196 * @param args arguments for the translation 197 * @param moduleId the id of the module the message is associated with 198 * @param sourceClass the name of the source for the message 199 * @param customMessage optional custom message overriding the default 200 */ 201 public LocalizedMessage( 202 int lineNo, 203 String bundle, 204 String key, 205 Object[] args, 206 String moduleId, 207 Class<?> sourceClass, 208 String customMessage) { 209 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId, 210 sourceClass, customMessage); 211 } 212 213 // -@cs[CyclomaticComplexity] equals - a lot of fields to check. 214 @Override 215 public boolean equals(Object object) { 216 if (this == object) { 217 return true; 218 } 219 if (object == null || getClass() != object.getClass()) { 220 return false; 221 } 222 final LocalizedMessage localizedMessage = (LocalizedMessage) object; 223 return Objects.equals(lineNo, localizedMessage.lineNo) 224 && Objects.equals(columnNo, localizedMessage.columnNo) 225 && Objects.equals(severityLevel, localizedMessage.severityLevel) 226 && Objects.equals(moduleId, localizedMessage.moduleId) 227 && Objects.equals(key, localizedMessage.key) 228 && Objects.equals(bundle, localizedMessage.bundle) 229 && Objects.equals(sourceClass, localizedMessage.sourceClass) 230 && Objects.equals(customMessage, localizedMessage.customMessage) 231 && Arrays.equals(args, localizedMessage.args); 232 } 233 234 @Override 235 public int hashCode() { 236 return Objects.hash(lineNo, columnNo, severityLevel, moduleId, key, bundle, sourceClass, 237 customMessage, Arrays.hashCode(args)); 238 } 239 240 /** Clears the cache. */ 241 public static void clearCache() { 242 synchronized (BUNDLE_CACHE) { 243 BUNDLE_CACHE.clear(); 244 } 245 } 246 247 /** 248 * Gets the translated message. 249 * @return the translated message 250 */ 251 public String getMessage() { 252 String message = getCustomMessage(); 253 254 if (message == null) { 255 try { 256 // Important to use the default class loader, and not the one in 257 // the GlobalProperties object. This is because the class loader in 258 // the GlobalProperties is specified by the user for resolving 259 // custom classes. 260 final ResourceBundle resourceBundle = getBundle(bundle); 261 final String pattern = resourceBundle.getString(key); 262 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 263 message = formatter.format(args); 264 } 265 catch (final MissingResourceException ignored) { 266 // If the Check author didn't provide i18n resource bundles 267 // and logs error messages directly, this will return 268 // the author's original message 269 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); 270 message = formatter.format(args); 271 } 272 } 273 return message; 274 } 275 276 /** 277 * Returns the formatted custom message if one is configured. 278 * @return the formatted custom message or {@code null} 279 * if there is no custom message 280 */ 281 private String getCustomMessage() { 282 283 if (customMessage == null) { 284 return null; 285 } 286 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); 287 return formatter.format(args); 288 } 289 290 /** 291 * Find a ResourceBundle for a given bundle name. Uses the classloader 292 * of the class emitting this message, to be sure to get the correct 293 * bundle. 294 * @param bundleName the bundle name 295 * @return a ResourceBundle 296 */ 297 private ResourceBundle getBundle(String bundleName) { 298 synchronized (BUNDLE_CACHE) { 299 ResourceBundle resourceBundle = BUNDLE_CACHE 300 .get(bundleName); 301 if (resourceBundle == null) { 302 resourceBundle = ResourceBundle.getBundle(bundleName, sLocale, 303 sourceClass.getClassLoader(), new Utf8Control()); 304 BUNDLE_CACHE.put(bundleName, resourceBundle); 305 } 306 return resourceBundle; 307 } 308 } 309 310 /** 311 * Gets the line number. 312 * @return the line number 313 */ 314 public int getLineNo() { 315 return lineNo; 316 } 317 318 /** 319 * Gets the column number. 320 * @return the column number 321 */ 322 public int getColumnNo() { 323 return columnNo; 324 } 325 326 /** 327 * Gets the severity level. 328 * @return the severity level 329 */ 330 public SeverityLevel getSeverityLevel() { 331 return severityLevel; 332 } 333 334 /** 335 * @return the module identifier. 336 */ 337 public String getModuleId() { 338 return moduleId; 339 } 340 341 /** 342 * Returns the message key to locate the translation, can also be used 343 * in IDE plugins to map error messages to corrective actions. 344 * 345 * @return the message key 346 */ 347 public String getKey() { 348 return key; 349 } 350 351 /** 352 * Gets the name of the source for this LocalizedMessage. 353 * @return the name of the source for this LocalizedMessage 354 */ 355 public String getSourceName() { 356 return sourceClass.getName(); 357 } 358 359 /** 360 * Sets a locale to use for localization. 361 * @param locale the locale to use for localization 362 */ 363 public static void setLocale(Locale locale) { 364 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 365 sLocale = Locale.ROOT; 366 } 367 else { 368 sLocale = locale; 369 } 370 } 371 372 //////////////////////////////////////////////////////////////////////////// 373 // Interface Comparable methods 374 //////////////////////////////////////////////////////////////////////////// 375 376 @Override 377 public int compareTo(LocalizedMessage other) { 378 int result = Integer.compare(lineNo, other.lineNo); 379 380 if (lineNo == other.lineNo) { 381 if (columnNo == other.columnNo) { 382 result = getMessage().compareTo(other.getMessage()); 383 } 384 else { 385 result = Integer.compare(columnNo, other.columnNo); 386 } 387 } 388 return result; 389 } 390 391 /** 392 * <p> 393 * Custom ResourceBundle.Control implementation which allows explicitly read 394 * the properties files as UTF-8 395 * </p> 396 * 397 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 398 */ 399 protected static class Utf8Control extends Control { 400 @Override 401 public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat, 402 ClassLoader aLoader, boolean aReload) throws IOException { 403 // The below is a copy of the default implementation. 404 final String bundleName = toBundleName(aBaseName, aLocale); 405 final String resourceName = toResourceName(bundleName, "properties"); 406 InputStream stream = null; 407 if (aReload) { 408 final URL url = aLoader.getResource(resourceName); 409 if (url != null) { 410 final URLConnection connection = url.openConnection(); 411 if (connection != null) { 412 connection.setUseCaches(false); 413 stream = connection.getInputStream(); 414 } 415 } 416 } 417 else { 418 stream = aLoader.getResourceAsStream(resourceName); 419 } 420 ResourceBundle resourceBundle = null; 421 if (stream != null) { 422 final Reader streamReader = new InputStreamReader(stream, "UTF-8"); 423 try { 424 // Only this line is changed to make it to read properties files as UTF-8. 425 resourceBundle = new PropertyResourceBundle(streamReader); 426 } 427 finally { 428 stream.close(); 429 } 430 } 431 return resourceBundle; 432 } 433 } 434}