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