001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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;
021
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.Reader;
025import java.net.URL;
026import java.net.URLConnection;
027import java.nio.charset.StandardCharsets;
028import java.text.MessageFormat;
029import java.util.Locale;
030import java.util.MissingResourceException;
031import java.util.PropertyResourceBundle;
032import java.util.ResourceBundle;
033import java.util.ResourceBundle.Control;
034
035import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
036
037/**
038 * Represents a message that can be localised. The translations come from
039 * message.properties files. The underlying implementation uses
040 * java.text.MessageFormat.
041 */
042public class LocalizedMessage {
043
044    /** The locale to localise messages to. **/
045    private static Locale sLocale = Locale.getDefault();
046
047    /** Name of the resource bundle to get messages from. **/
048    private final String bundle;
049
050    /** Class of the source for this message. */
051    private final Class<?> sourceClass;
052
053    /**
054     * Key for the message format.
055     **/
056    private final String key;
057
058    /**
059     * Arguments for java.text.MessageFormat, that is why type is Object[].
060     *
061     * <p>Note: Changing types from Object[] will be huge breaking compatibility, as Module
062     * messages use some type formatting already, so better to keep it as Object[].
063     * </p>
064     */
065    private final Object[] args;
066
067    /**
068     * Creates a new {@code LocalizedMessage} instance.
069     *
070     * @param bundle resource bundle name
071     * @param sourceClass the Class that is the source of the message
072     * @param key the key to locate the translation.
073     * @param args arguments for the translation.
074     */
075    public LocalizedMessage(String bundle, Class<?> sourceClass, String key,
076            Object... args) {
077        this.bundle = bundle;
078        this.sourceClass = sourceClass;
079        this.key = key;
080        if (args == null) {
081            this.args = null;
082        }
083        else {
084            this.args = UnmodifiableCollectionUtil.copyOfArray(args, args.length);
085        }
086    }
087
088    /**
089     * Sets a locale to use for localization.
090     *
091     * @param locale the locale to use for localization
092     */
093    public static void setLocale(Locale locale) {
094        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
095            sLocale = Locale.ROOT;
096        }
097        else {
098            sLocale = locale;
099        }
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}