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 * Custom ResourceBundle.Control implementation which allows explicitly read
143 * the properties files as UTF-8.
144 */
145 public static class Utf8Control extends Control {
146
147 @Override
148 public ResourceBundle newBundle(String baseName, Locale locale, String format,
149 ClassLoader loader, boolean reload) throws IOException {
150 // The below is a copy of the default implementation.
151 final String bundleName = toBundleName(baseName, locale);
152 final String resourceName = toResourceName(bundleName, "properties");
153 final URL url = loader.getResource(resourceName);
154 ResourceBundle resourceBundle = null;
155 if (url != null) {
156 final URLConnection connection = url.openConnection();
157 if (connection != null) {
158 connection.setUseCaches(!reload);
159 try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
160 StandardCharsets.UTF_8)) {
161 // Only this line is changed to make it read property files as UTF-8.
162 resourceBundle = new PropertyResourceBundle(streamReader);
163 }
164 }
165 }
166 return resourceBundle;
167 }
168
169 }
170
171 }