Coverage Report - com.puppycrawl.tools.checkstyle.api.LocalizedMessage
 
Classes in this File Line Coverage Branch Coverage Complexity
LocalizedMessage
100%
93/93
100%
46/46
0
LocalizedMessage$Utf8Control
100%
21/21
100%
8/8
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.api;
 21  
 
 22  
 import java.io.IOException;
 23  
 import java.io.InputStream;
 24  
 import java.io.InputStreamReader;
 25  
 import java.io.Reader;
 26  
 import java.io.Serializable;
 27  
 import java.net.URL;
 28  
 import java.net.URLConnection;
 29  
 import java.nio.charset.StandardCharsets;
 30  
 import java.text.MessageFormat;
 31  
 import java.util.Arrays;
 32  
 import java.util.Collections;
 33  
 import java.util.HashMap;
 34  
 import java.util.Locale;
 35  
 import java.util.Map;
 36  
 import java.util.MissingResourceException;
 37  
 import java.util.Objects;
 38  
 import java.util.PropertyResourceBundle;
 39  
 import java.util.ResourceBundle;
 40  
 import java.util.ResourceBundle.Control;
 41  
 
 42  
 /**
 43  
  * Represents a message that can be localised. The translations come from
 44  
  * message.properties files. The underlying implementation uses
 45  
  * java.text.MessageFormat.
 46  
  *
 47  
  * @author Oliver Burn
 48  
  * @author lkuehne
 49  
  * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
 50  
  */
 51  1762144
 public final class LocalizedMessage
 52  
     implements Comparable<LocalizedMessage>, Serializable {
 53  
     private static final long serialVersionUID = 5675176836184862150L;
 54  
 
 55  
     /**
 56  
      * A cache that maps bundle names to ResourceBundles.
 57  
      * Avoids repetitive calls to ResourceBundle.getBundle().
 58  
      */
 59  11
     private static final Map<String, ResourceBundle> BUNDLE_CACHE =
 60  11
         Collections.synchronizedMap(new HashMap<>());
 61  
 
 62  
     /** The default severity level if one is not specified. */
 63  11
     private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
 64  
 
 65  
     /** The locale to localise messages to. **/
 66  11
     private static Locale sLocale = Locale.getDefault();
 67  
 
 68  
     /** The line number. **/
 69  
     private final int lineNo;
 70  
     /** The column number. **/
 71  
     private final int columnNo;
 72  
     /** The column char index. **/
 73  
     private final int columnCharIndex;
 74  
     /** The token type constant. See {@link TokenTypes}. **/
 75  
     private final int tokenType;
 76  
 
 77  
     /** The severity level. **/
 78  
     private final SeverityLevel severityLevel;
 79  
 
 80  
     /** The id of the module generating the message. */
 81  
     private final String moduleId;
 82  
 
 83  
     /** Key for the message format. **/
 84  
     private final String key;
 85  
 
 86  
     /** Arguments for MessageFormat.
 87  
      * @noinspection NonSerializableFieldInSerializableClass
 88  
      */
 89  
     private final Object[] args;
 90  
 
 91  
     /** Name of the resource bundle to get messages from. **/
 92  
     private final String bundle;
 93  
 
 94  
     /** Class of the source for this LocalizedMessage. */
 95  
     private final Class<?> sourceClass;
 96  
 
 97  
     /** A custom message overriding the default message from the bundle. */
 98  
     private final String customMessage;
 99  
 
 100  
     /**
 101  
      * Creates a new {@code LocalizedMessage} instance.
 102  
      *
 103  
      * @param lineNo line number associated with the message
 104  
      * @param columnNo column number associated with the message
 105  
      * @param columnCharIndex column char index associated with the message
 106  
      * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
 107  
      * @param bundle resource bundle name
 108  
      * @param key the key to locate the translation
 109  
      * @param args arguments for the translation
 110  
      * @param severityLevel severity level for the message
 111  
      * @param moduleId the id of the module the message is associated with
 112  
      * @param sourceClass the Class that is the source of the message
 113  
      * @param customMessage optional custom message overriding the default
 114  
      * @noinspection ConstructorWithTooManyParameters
 115  
      */
 116  
     // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
 117  
     public LocalizedMessage(int lineNo,
 118  
                             int columnNo,
 119  
                             int columnCharIndex,
 120  
                             int tokenType,
 121  
                             String bundle,
 122  
                             String key,
 123  
                             Object[] args,
 124  
                             SeverityLevel severityLevel,
 125  
                             String moduleId,
 126  
                             Class<?> sourceClass,
 127  74107
                             String customMessage) {
 128  74107
         this.lineNo = lineNo;
 129  74107
         this.columnNo = columnNo;
 130  74107
         this.columnCharIndex = columnCharIndex;
 131  74107
         this.tokenType = tokenType;
 132  74107
         this.key = key;
 133  
 
 134  74107
         if (args == null) {
 135  1702
             this.args = null;
 136  
         }
 137  
         else {
 138  72405
             this.args = Arrays.copyOf(args, args.length);
 139  
         }
 140  74107
         this.bundle = bundle;
 141  74107
         this.severityLevel = severityLevel;
 142  74107
         this.moduleId = moduleId;
 143  74107
         this.sourceClass = sourceClass;
 144  74107
         this.customMessage = customMessage;
 145  74107
     }
 146  
 
 147  
     /**
 148  
      * Creates a new {@code LocalizedMessage} instance.
 149  
      *
 150  
      * @param lineNo line number associated with the message
 151  
      * @param columnNo column number associated with the message
 152  
      * @param tokenType token type of the event associated with the message. See {@link TokenTypes}
 153  
      * @param bundle resource bundle name
 154  
      * @param key the key to locate the translation
 155  
      * @param args arguments for the translation
 156  
      * @param severityLevel severity level for the message
 157  
      * @param moduleId the id of the module the message is associated with
 158  
      * @param sourceClass the Class that is the source of the message
 159  
      * @param customMessage optional custom message overriding the default
 160  
      * @noinspection ConstructorWithTooManyParameters
 161  
      */
 162  
     // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
 163  
     public LocalizedMessage(int lineNo,
 164  
                             int columnNo,
 165  
                             int tokenType,
 166  
                             String bundle,
 167  
                             String key,
 168  
                             Object[] args,
 169  
                             SeverityLevel severityLevel,
 170  
                             String moduleId,
 171  
                             Class<?> sourceClass,
 172  
                             String customMessage) {
 173  72632
         this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId,
 174  
                 sourceClass, customMessage);
 175  72632
     }
 176  
 
 177  
     /**
 178  
      * Creates a new {@code LocalizedMessage} instance.
 179  
      *
 180  
      * @param lineNo line number associated with the message
 181  
      * @param columnNo column number associated with the message
 182  
      * @param bundle resource bundle name
 183  
      * @param key the key to locate the translation
 184  
      * @param args arguments for the translation
 185  
      * @param severityLevel severity level for the message
 186  
      * @param moduleId the id of the module the message is associated with
 187  
      * @param sourceClass the Class that is the source of the message
 188  
      * @param customMessage optional custom message overriding the default
 189  
      * @noinspection ConstructorWithTooManyParameters
 190  
      */
 191  
     // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
 192  
     public LocalizedMessage(int lineNo,
 193  
                             int columnNo,
 194  
                             String bundle,
 195  
                             String key,
 196  
                             Object[] args,
 197  
                             SeverityLevel severityLevel,
 198  
                             String moduleId,
 199  
                             Class<?> sourceClass,
 200  
                             String customMessage) {
 201  72614
         this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass,
 202  
                 customMessage);
 203  72614
     }
 204  
 
 205  
     /**
 206  
      * Creates a new {@code LocalizedMessage} instance.
 207  
      *
 208  
      * @param lineNo line number associated with the message
 209  
      * @param columnNo column number associated with the message
 210  
      * @param bundle resource bundle name
 211  
      * @param key the key to locate the translation
 212  
      * @param args arguments for the translation
 213  
      * @param moduleId the id of the module the message is associated with
 214  
      * @param sourceClass the Class that is the source of the message
 215  
      * @param customMessage optional custom message overriding the default
 216  
      * @noinspection ConstructorWithTooManyParameters
 217  
      */
 218  
     // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
 219  
     public LocalizedMessage(int lineNo,
 220  
                             int columnNo,
 221  
                             String bundle,
 222  
                             String key,
 223  
                             Object[] args,
 224  
                             String moduleId,
 225  
                             Class<?> sourceClass,
 226  
                             String customMessage) {
 227  30
         this(lineNo,
 228  
                 columnNo,
 229  
              bundle,
 230  
              key,
 231  
              args,
 232  
              DEFAULT_SEVERITY,
 233  
              moduleId,
 234  
              sourceClass,
 235  
              customMessage);
 236  30
     }
 237  
 
 238  
     /**
 239  
      * Creates a new {@code LocalizedMessage} instance.
 240  
      *
 241  
      * @param lineNo line number associated with the message
 242  
      * @param bundle resource bundle name
 243  
      * @param key the key to locate the translation
 244  
      * @param args arguments for the translation
 245  
      * @param severityLevel severity level for the message
 246  
      * @param moduleId the id of the module the message is associated with
 247  
      * @param sourceClass the source class for the message
 248  
      * @param customMessage optional custom message overriding the default
 249  
      * @noinspection ConstructorWithTooManyParameters
 250  
      */
 251  
     // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
 252  
     public LocalizedMessage(int lineNo,
 253  
                             String bundle,
 254  
                             String key,
 255  
                             Object[] args,
 256  
                             SeverityLevel severityLevel,
 257  
                             String moduleId,
 258  
                             Class<?> sourceClass,
 259  
                             String customMessage) {
 260  67429
         this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
 261  
                 sourceClass, customMessage);
 262  67429
     }
 263  
 
 264  
     /**
 265  
      * Creates a new {@code LocalizedMessage} instance. The column number
 266  
      * defaults to 0.
 267  
      *
 268  
      * @param lineNo line number associated with the message
 269  
      * @param bundle name of a resource bundle that contains error messages
 270  
      * @param key the key to locate the translation
 271  
      * @param args arguments for the translation
 272  
      * @param moduleId the id of the module the message is associated with
 273  
      * @param sourceClass the name of the source for the message
 274  
      * @param customMessage optional custom message overriding the default
 275  
      * @noinspection ConstructorWithTooManyParameters
 276  
      */
 277  
     public LocalizedMessage(
 278  
         int lineNo,
 279  
         String bundle,
 280  
         String key,
 281  
         Object[] args,
 282  
         String moduleId,
 283  
         Class<?> sourceClass,
 284  
         String customMessage) {
 285  1781
         this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
 286  
                 sourceClass, customMessage);
 287  1781
     }
 288  
 
 289  
     // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
 290  
     @Override
 291  
     public boolean equals(Object object) {
 292  290
         if (this == object) {
 293  2
             return true;
 294  
         }
 295  288
         if (object == null || getClass() != object.getClass()) {
 296  4
             return false;
 297  
         }
 298  284
         final LocalizedMessage localizedMessage = (LocalizedMessage) object;
 299  568
         return Objects.equals(lineNo, localizedMessage.lineNo)
 300  247
                 && Objects.equals(columnNo, localizedMessage.columnNo)
 301  239
                 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex)
 302  232
                 && Objects.equals(tokenType, localizedMessage.tokenType)
 303  225
                 && Objects.equals(severityLevel, localizedMessage.severityLevel)
 304  216
                 && Objects.equals(moduleId, localizedMessage.moduleId)
 305  207
                 && Objects.equals(key, localizedMessage.key)
 306  198
                 && Objects.equals(bundle, localizedMessage.bundle)
 307  189
                 && Objects.equals(sourceClass, localizedMessage.sourceClass)
 308  180
                 && Objects.equals(customMessage, localizedMessage.customMessage)
 309  171
                 && Arrays.equals(args, localizedMessage.args);
 310  
     }
 311  
 
 312  
     @Override
 313  
     public int hashCode() {
 314  158
         return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
 315  79
                 key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
 316  
     }
 317  
 
 318  
     /** Clears the cache. */
 319  
     public static void clearCache() {
 320  2031
         BUNDLE_CACHE.clear();
 321  2031
     }
 322  
 
 323  
     /**
 324  
      * Gets the translated message.
 325  
      * @return the translated message
 326  
      */
 327  
     public String getMessage() {
 328  148596
         String message = getCustomMessage();
 329  
 
 330  148595
         if (message == null) {
 331  
             try {
 332  
                 // Important to use the default class loader, and not the one in
 333  
                 // the GlobalProperties object. This is because the class loader in
 334  
                 // the GlobalProperties is specified by the user for resolving
 335  
                 // custom classes.
 336  148555
                 final ResourceBundle resourceBundle = getBundle(bundle);
 337  148532
                 final String pattern = resourceBundle.getString(key);
 338  147962
                 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
 339  147962
                 message = formatter.format(args);
 340  
             }
 341  593
             catch (final MissingResourceException ignored) {
 342  
                 // If the Check author didn't provide i18n resource bundles
 343  
                 // and logs error messages directly, this will return
 344  
                 // the author's original message
 345  593
                 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
 346  593
                 message = formatter.format(args);
 347  147962
             }
 348  
         }
 349  148595
         return message;
 350  
     }
 351  
 
 352  
     /**
 353  
      * Returns the formatted custom message if one is configured.
 354  
      * @return the formatted custom message or {@code null}
 355  
      *          if there is no custom message
 356  
      */
 357  
     private String getCustomMessage() {
 358  148596
         String message = null;
 359  148596
         if (customMessage != null) {
 360  41
             final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
 361  40
             message = formatter.format(args);
 362  
         }
 363  148595
         return message;
 364  
     }
 365  
 
 366  
     /**
 367  
      * Find a ResourceBundle for a given bundle name. Uses the classloader
 368  
      * of the class emitting this message, to be sure to get the correct
 369  
      * bundle.
 370  
      * @param bundleName the bundle name
 371  
      * @return a ResourceBundle
 372  
      */
 373  
     private ResourceBundle getBundle(String bundleName) {
 374  153547
         return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle(
 375  2496
                 name, sLocale, sourceClass.getClassLoader(), new Utf8Control()));
 376  
     }
 377  
 
 378  
     /**
 379  
      * Gets the line number.
 380  
      * @return the line number
 381  
      */
 382  
     public int getLineNo() {
 383  74117
         return lineNo;
 384  
     }
 385  
 
 386  
     /**
 387  
      * Gets the column number.
 388  
      * @return the column number
 389  
      */
 390  
     public int getColumnNo() {
 391  76445
         return columnNo;
 392  
     }
 393  
 
 394  
     /**
 395  
      * Gets the column char index.
 396  
      * @return the column char index
 397  
      */
 398  
     public int getColumnCharIndex() {
 399  1
         return columnCharIndex;
 400  
     }
 401  
 
 402  
     /**
 403  
      * Gets the token type.
 404  
      * @return the token type
 405  
      */
 406  
     public int getTokenType() {
 407  13
         return tokenType;
 408  
     }
 409  
 
 410  
     /**
 411  
      * Gets the severity level.
 412  
      * @return the severity level
 413  
      */
 414  
     public SeverityLevel getSeverityLevel() {
 415  144155
         return severityLevel;
 416  
     }
 417  
 
 418  
     /**
 419  
      * Returns id of module.
 420  
      * @return the module identifier.
 421  
      */
 422  
     public String getModuleId() {
 423  719
         return moduleId;
 424  
     }
 425  
 
 426  
     /**
 427  
      * Returns the message key to locate the translation, can also be used
 428  
      * in IDE plugins to map error messages to corrective actions.
 429  
      *
 430  
      * @return the message key
 431  
      */
 432  
     public String getKey() {
 433  2
         return key;
 434  
     }
 435  
 
 436  
     /**
 437  
      * Gets the name of the source for this LocalizedMessage.
 438  
      * @return the name of the source for this LocalizedMessage
 439  
      */
 440  
     public String getSourceName() {
 441  403
         return sourceClass.getName();
 442  
     }
 443  
 
 444  
     /**
 445  
      * Sets a locale to use for localization.
 446  
      * @param locale the locale to use for localization
 447  
      */
 448  
     public static void setLocale(Locale locale) {
 449  2016
         clearCache();
 450  2016
         if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
 451  2010
             sLocale = Locale.ROOT;
 452  
         }
 453  
         else {
 454  6
             sLocale = locale;
 455  
         }
 456  2016
     }
 457  
 
 458  
     ////////////////////////////////////////////////////////////////////////////
 459  
     // Interface Comparable methods
 460  
     ////////////////////////////////////////////////////////////////////////////
 461  
 
 462  
     @Override
 463  
     public int compareTo(LocalizedMessage other) {
 464  
         final int result;
 465  
 
 466  1762147
         if (lineNo == other.lineNo) {
 467  1970
             if (columnNo == other.columnNo) {
 468  1207
                 if (Objects.equals(moduleId, other.moduleId)) {
 469  1203
                     result = getMessage().compareTo(other.getMessage());
 470  
                 }
 471  4
                 else if (moduleId == null) {
 472  1
                     result = -1;
 473  
                 }
 474  3
                 else if (other.moduleId == null) {
 475  2
                     result = 1;
 476  
                 }
 477  
                 else {
 478  1
                     result = moduleId.compareTo(other.moduleId);
 479  
                 }
 480  
             }
 481  
             else {
 482  763
                 result = Integer.compare(columnNo, other.columnNo);
 483  
             }
 484  
         }
 485  
         else {
 486  1760177
             result = Integer.compare(lineNo, other.lineNo);
 487  
         }
 488  1762146
         return result;
 489  
     }
 490  
 
 491  
     /**
 492  
      * <p>
 493  
      * Custom ResourceBundle.Control implementation which allows explicitly read
 494  
      * the properties files as UTF-8.
 495  
      * </p>
 496  
      *
 497  
      * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
 498  
      * @noinspection IOResourceOpenedButNotSafelyClosed
 499  
      */
 500  10351
     public static class Utf8Control extends Control {
 501  
         @Override
 502  
         public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat,
 503  
                  ClassLoader aLoader, boolean aReload) throws IOException {
 504  
             // The below is a copy of the default implementation.
 505  154
             final String bundleName = toBundleName(aBaseName, aLocale);
 506  154
             final String resourceName = toResourceName(bundleName, "properties");
 507  154
             InputStream stream = null;
 508  154
             if (aReload) {
 509  3
                 final URL url = aLoader.getResource(resourceName);
 510  3
                 if (url != null) {
 511  2
                     final URLConnection connection = url.openConnection();
 512  2
                     if (connection != null) {
 513  1
                         connection.setUseCaches(false);
 514  1
                         stream = connection.getInputStream();
 515  
                     }
 516  
                 }
 517  3
             }
 518  
             else {
 519  151
                 stream = aLoader.getResourceAsStream(resourceName);
 520  
             }
 521  154
             ResourceBundle resourceBundle = null;
 522  154
             if (stream != null) {
 523  39
                 final Reader streamReader = new InputStreamReader(stream,
 524  39
                         StandardCharsets.UTF_8.name());
 525  
                 try {
 526  
                     // Only this line is changed to make it to read properties files as UTF-8.
 527  39
                     resourceBundle = new PropertyResourceBundle(streamReader);
 528  
                 }
 529  
                 finally {
 530  39
                     stream.close();
 531  39
                 }
 532  
             }
 533  154
             return resourceBundle;
 534  
         }
 535  
     }
 536  
 }