View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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;
21  
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.net.URL;
26  import java.net.URLConnection;
27  import java.nio.charset.StandardCharsets;
28  import java.text.MessageFormat;
29  import java.util.Locale;
30  import java.util.MissingResourceException;
31  import java.util.PropertyResourceBundle;
32  import java.util.ResourceBundle;
33  import java.util.ResourceBundle.Control;
34  
35  import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
36  
37  /**
38   * Represents a message that can be localised. The translations come from
39   * message.properties files. The underlying implementation uses
40   * java.text.MessageFormat.
41   */
42  public class LocalizedMessage {
43  
44      /** The locale to localise messages to. **/
45      private static Locale sLocale = Locale.getDefault();
46  
47      /** Name of the resource bundle to get messages from. **/
48      private final String bundle;
49  
50      /** Class of the source for this message. */
51      private final Class<?> sourceClass;
52  
53      /**
54       * Key for the message format.
55       **/
56      private final String key;
57  
58      /**
59       * Arguments for java.text.MessageFormat, that is why type is Object[].
60       *
61       * <p>Note: Changing types from Object[] will be huge breaking compatibility, as Module
62       * messages use some type formatting already, so better to keep it as Object[].
63       * </p>
64       */
65      private final Object[] args;
66  
67      /**
68       * Creates a new {@code LocalizedMessage} instance.
69       *
70       * @param bundle resource bundle name
71       * @param sourceClass the Class that is the source of the message
72       * @param key the key to locate the translation.
73       * @param args arguments for the translation.
74       */
75      public LocalizedMessage(String bundle, Class<?> sourceClass, String key,
76              Object... args) {
77          this.bundle = bundle;
78          this.sourceClass = sourceClass;
79          this.key = key;
80          if (args == null) {
81              this.args = null;
82          }
83          else {
84              this.args = UnmodifiableCollectionUtil.copyOfArray(args, args.length);
85          }
86      }
87  
88      /**
89       * Sets a locale to use for localization.
90       *
91       * @param locale the locale to use for localization
92       */
93      public static void setLocale(Locale locale) {
94          if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
95              sLocale = Locale.ROOT;
96          }
97          else {
98              sLocale = locale;
99          }
100     }
101 
102     /**
103      * Gets the translated message.
104      *
105      * @return the translated message.
106      */
107     public String getMessage() {
108         String result;
109         try {
110             // Important to use the default class loader, and not the one in
111             // the GlobalProperties object. This is because the class loader in
112             // the GlobalProperties is specified by the user for resolving
113             // custom classes.
114             final ResourceBundle resourceBundle = getBundle();
115             final String pattern = resourceBundle.getString(key);
116             final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
117             result = formatter.format(args);
118         }
119         catch (final MissingResourceException ignored) {
120             // If the Check author didn't provide i18n resource bundles
121             // and logs audit event messages directly, this will return
122             // the author's original message
123             final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
124             result = formatter.format(args);
125         }
126         return result;
127     }
128 
129     /**
130      * Obtain the ResourceBundle. Uses the classloader
131      * of the class emitting this message, to be sure to get the correct
132      * bundle.
133      *
134      * @return a ResourceBundle.
135      */
136     private ResourceBundle getBundle() {
137         return ResourceBundle.getBundle(bundle, sLocale, sourceClass.getClassLoader(),
138                 new Utf8Control());
139     }
140 
141     /**
142      * <p>
143      * Custom ResourceBundle.Control implementation which allows explicitly read
144      * the properties files as UTF-8.
145      * </p>
146      */
147     public static class Utf8Control extends Control {
148 
149         @Override
150         public ResourceBundle newBundle(String baseName, Locale locale, String format,
151                  ClassLoader loader, boolean reload) throws IOException {
152             // The below is a copy of the default implementation.
153             final String bundleName = toBundleName(baseName, locale);
154             final String resourceName = toResourceName(bundleName, "properties");
155             final URL url = loader.getResource(resourceName);
156             ResourceBundle resourceBundle = null;
157             if (url != null) {
158                 final URLConnection connection = url.openConnection();
159                 if (connection != null) {
160                     connection.setUseCaches(!reload);
161                     try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
162                             StandardCharsets.UTF_8)) {
163                         // Only this line is changed to make it read property files as UTF-8.
164                         resourceBundle = new PropertyResourceBundle(streamReader);
165                     }
166                 }
167             }
168             return resourceBundle;
169         }
170 
171     }
172 
173 }