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