001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.InputStreamReader; 024import java.io.Reader; 025import java.io.Serializable; 026import java.net.URL; 027import java.net.URLConnection; 028import java.nio.charset.StandardCharsets; 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 violation that can be localised. The translations come from 043 * message.properties files. The underlying implementation uses 044 * java.text.MessageFormat. 045 * 046 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors 047 */ 048public final class Violation 049 implements Comparable<Violation>, Serializable { 050 051 /** A unique serial version identifier. */ 052 private static final long serialVersionUID = 5675176836184862150L; 053 054 /** 055 * A cache that maps bundle names to ResourceBundles. 056 * Avoids repetitive calls to ResourceBundle.getBundle(). 057 */ 058 private static final Map<String, ResourceBundle> BUNDLE_CACHE = 059 Collections.synchronizedMap(new HashMap<>()); 060 061 /** The default severity level if one is not specified. */ 062 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR; 063 064 /** The locale to localise violations to. **/ 065 private static Locale sLocale = Locale.getDefault(); 066 067 /** The line number. **/ 068 private final int lineNo; 069 /** The column number. **/ 070 private final int columnNo; 071 /** The column char index. **/ 072 private final int columnCharIndex; 073 /** The token type constant. See {@link TokenTypes}. **/ 074 private final int tokenType; 075 076 /** The severity level. **/ 077 private final SeverityLevel severityLevel; 078 079 /** The id of the module generating the violation. */ 080 private final String moduleId; 081 082 /** Key for the violation format. **/ 083 private final String key; 084 085 /** 086 * Arguments for MessageFormat. 087 * 088 * @noinspection NonSerializableFieldInSerializableClass 089 */ 090 private final Object[] args; 091 092 /** Name of the resource bundle to get violations from. **/ 093 private final String bundle; 094 095 /** Class of the source for this Violation. */ 096 private final Class<?> sourceClass; 097 098 /** A custom violation overriding the default violation from the bundle. */ 099 private final String customMessage; 100 101 /** 102 * Creates a new {@code Violation} instance. 103 * 104 * @param lineNo line number associated with the violation 105 * @param columnNo column number associated with the violation 106 * @param columnCharIndex column char index associated with the violation 107 * @param tokenType token type of the event associated with violation. 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 violation 112 * @param moduleId the id of the module the violation is associated with 113 * @param sourceClass the Class that is the source of the violation 114 * @param customMessage optional custom violation overriding the default 115 * @noinspection ConstructorWithTooManyParameters 116 */ 117 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 118 public Violation(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 Violation} instance. 150 * 151 * @param lineNo line number associated with the violation 152 * @param columnNo column number associated with the violation 153 * @param tokenType token type of the event associated with violation. 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 violation 158 * @param moduleId the id of the module the violation is associated with 159 * @param sourceClass the Class that is the source of the violation 160 * @param customMessage optional custom violation overriding the default 161 * @noinspection ConstructorWithTooManyParameters 162 */ 163 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 164 public Violation(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 Violation} instance. 180 * 181 * @param lineNo line number associated with the violation 182 * @param columnNo column number associated with the violation 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 violation 187 * @param moduleId the id of the module the violation is associated with 188 * @param sourceClass the Class that is the source of the violation 189 * @param customMessage optional custom violation overriding the default 190 * @noinspection ConstructorWithTooManyParameters 191 */ 192 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 193 public Violation(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 Violation} instance. 208 * 209 * @param lineNo line number associated with the violation 210 * @param columnNo column number associated with the violation 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 violation is associated with 215 * @param sourceClass the Class that is the source of the violation 216 * @param customMessage optional custom violation overriding the default 217 * @noinspection ConstructorWithTooManyParameters 218 */ 219 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 220 public Violation(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 Violation} instance. 241 * 242 * @param lineNo line number associated with the violation 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 violation 247 * @param moduleId the id of the module the violation is associated with 248 * @param sourceClass the source class for the violation 249 * @param customMessage optional custom violation overriding the default 250 * @noinspection ConstructorWithTooManyParameters 251 */ 252 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 253 public Violation(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 Violation} instance. The column number 267 * defaults to 0. 268 * 269 * @param lineNo line number associated with the violation 270 * @param bundle name of a resource bundle that contains audit event violations 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 violation is associated with 274 * @param sourceClass the name of the source for the violation 275 * @param customMessage optional custom violation overriding the default 276 */ 277 public Violation( 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 /** 290 * Gets the line number. 291 * 292 * @return the line number 293 */ 294 public int getLineNo() { 295 return lineNo; 296 } 297 298 /** 299 * Gets the column number. 300 * 301 * @return the column number 302 */ 303 public int getColumnNo() { 304 return columnNo; 305 } 306 307 /** 308 * Gets the column char index. 309 * 310 * @return the column char index 311 */ 312 public int getColumnCharIndex() { 313 return columnCharIndex; 314 } 315 316 /** 317 * Gets the token type. 318 * 319 * @return the token type 320 */ 321 public int getTokenType() { 322 return tokenType; 323 } 324 325 /** 326 * Gets the severity level. 327 * 328 * @return the severity level 329 */ 330 public SeverityLevel getSeverityLevel() { 331 return severityLevel; 332 } 333 334 /** 335 * Returns id of module. 336 * 337 * @return the module identifier. 338 */ 339 public String getModuleId() { 340 return moduleId; 341 } 342 343 /** 344 * Returns the violation key to locate the translation, can also be used 345 * in IDE plugins to map audit event violations to corrective actions. 346 * 347 * @return the violation key 348 */ 349 public String getKey() { 350 return key; 351 } 352 353 /** 354 * Gets the name of the source for this Violation. 355 * 356 * @return the name of the source for this Violation 357 */ 358 public String getSourceName() { 359 return sourceClass.getName(); 360 } 361 362 /** 363 * Sets a locale to use for localization. 364 * 365 * @param locale the locale to use for localization 366 */ 367 public static void setLocale(Locale locale) { 368 clearCache(); 369 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 370 sLocale = Locale.ROOT; 371 } 372 else { 373 sLocale = locale; 374 } 375 } 376 377 /** Clears the cache. */ 378 public static void clearCache() { 379 BUNDLE_CACHE.clear(); 380 } 381 382 /** 383 * Indicates whether some other object is "equal to" this one. 384 * Suppression on enumeration is needed so code stays consistent. 385 * 386 * @noinspection EqualsCalledOnEnumConstant 387 */ 388 // -@cs[CyclomaticComplexity] equals - a lot of fields to check. 389 @Override 390 public boolean equals(Object object) { 391 if (this == object) { 392 return true; 393 } 394 if (object == null || getClass() != object.getClass()) { 395 return false; 396 } 397 final Violation violation = (Violation) object; 398 return Objects.equals(lineNo, violation.lineNo) 399 && Objects.equals(columnNo, violation.columnNo) 400 && Objects.equals(columnCharIndex, violation.columnCharIndex) 401 && Objects.equals(tokenType, violation.tokenType) 402 && Objects.equals(severityLevel, violation.severityLevel) 403 && Objects.equals(moduleId, violation.moduleId) 404 && Objects.equals(key, violation.key) 405 && Objects.equals(bundle, violation.bundle) 406 && Objects.equals(sourceClass, violation.sourceClass) 407 && Objects.equals(customMessage, violation.customMessage) 408 && Arrays.equals(args, violation.args); 409 } 410 411 @Override 412 public int hashCode() { 413 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId, 414 key, bundle, sourceClass, customMessage, Arrays.hashCode(args)); 415 } 416 417 //////////////////////////////////////////////////////////////////////////// 418 // Interface Comparable methods 419 //////////////////////////////////////////////////////////////////////////// 420 421 @Override 422 public int compareTo(Violation other) { 423 final int result; 424 425 if (lineNo == other.lineNo) { 426 if (columnNo == other.columnNo) { 427 if (Objects.equals(moduleId, other.moduleId)) { 428 result = getViolation().compareTo(other.getViolation()); 429 } 430 else if (moduleId == null) { 431 result = -1; 432 } 433 else if (other.moduleId == null) { 434 result = 1; 435 } 436 else { 437 result = moduleId.compareTo(other.moduleId); 438 } 439 } 440 else { 441 result = Integer.compare(columnNo, other.columnNo); 442 } 443 } 444 else { 445 result = Integer.compare(lineNo, other.lineNo); 446 } 447 return result; 448 } 449 450 /** 451 * Gets the translated violation. 452 * 453 * @return the translated violation 454 */ 455 public String getViolation() { 456 String violation = getCustomViolation(); 457 458 if (violation == null) { 459 try { 460 // Important to use the default class loader, and not the one in 461 // the GlobalProperties object. This is because the class loader in 462 // the GlobalProperties is specified by the user for resolving 463 // custom classes. 464 final ResourceBundle resourceBundle = getBundle(bundle); 465 final String pattern = resourceBundle.getString(key); 466 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 467 violation = formatter.format(args); 468 } 469 catch (final MissingResourceException ignored) { 470 // If the Check author didn't provide i18n resource bundles 471 // and logs audit event violations directly, this will return 472 // the author's original violation 473 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); 474 violation = formatter.format(args); 475 } 476 } 477 return violation; 478 } 479 480 /** 481 * Returns the formatted custom violation if one is configured. 482 * 483 * @return the formatted custom violation or {@code null} 484 * if there is no custom violation 485 */ 486 private String getCustomViolation() { 487 String violation = null; 488 if (customMessage != null) { 489 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); 490 violation = formatter.format(args); 491 } 492 return violation; 493 } 494 495 /** 496 * Find a ResourceBundle for a given bundle name. Uses the classloader 497 * of the class emitting this violation, to be sure to get the correct 498 * bundle. 499 * 500 * @param bundleName the bundle name 501 * @return a ResourceBundle 502 */ 503 private ResourceBundle getBundle(String bundleName) { 504 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> { 505 return ResourceBundle.getBundle( 506 name, sLocale, sourceClass.getClassLoader(), new Utf8Control()); 507 }); 508 } 509 510 /** 511 * <p> 512 * Custom ResourceBundle.Control implementation which allows explicitly read 513 * the properties files as UTF-8. 514 * </p> 515 */ 516 public static class Utf8Control extends Control { 517 518 @Override 519 public ResourceBundle newBundle(String baseName, Locale locale, String format, 520 ClassLoader loader, boolean reload) throws IOException { 521 // The below is a copy of the default implementation. 522 final String bundleName = toBundleName(baseName, locale); 523 final String resourceName = toResourceName(bundleName, "properties"); 524 final URL url = loader.getResource(resourceName); 525 ResourceBundle resourceBundle = null; 526 if (url != null) { 527 final URLConnection connection = url.openConnection(); 528 if (connection != null) { 529 connection.setUseCaches(!reload); 530 try (Reader streamReader = new InputStreamReader(connection.getInputStream(), 531 StandardCharsets.UTF_8)) { 532 // Only this line is changed to make it read property files as UTF-8. 533 resourceBundle = new PropertyResourceBundle(streamReader); 534 } 535 } 536 } 537 return resourceBundle; 538 } 539 540 } 541 542}