001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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.Arrays;
030import java.util.Locale;
031import java.util.MissingResourceException;
032import java.util.PropertyResourceBundle;
033import java.util.ResourceBundle;
034import java.util.ResourceBundle.Control;
035
036/**
037 * Represents a message that can be localised. The translations come from
038 * message.properties files. The underlying implementation uses
039 * java.text.MessageFormat.
040 */
041public class LocalizedMessage {
042
043    /** The locale to localise messages to. **/
044    private static Locale sLocale = Locale.getDefault();
045
046    /** Name of the resource bundle to get messages from. **/
047    private final String bundle;
048
049    /** Class of the source for this message. */
050    private final Class<?> sourceClass;
051
052    /**
053     * Key for the message format.
054     **/
055    private final String key;
056
057    /**
058     * Arguments for java.text.MessageFormat, that is why type is Object[].
059     *
060     * <p>Note: Changing types from Object[] will be huge breaking compatibility, as Module
061     * messages use some type formatting already, so better to keep it as Object[].
062     * </p>
063     */
064    private final Object[] args;
065
066    /**
067     * Creates a new {@code LocalizedMessage} instance.
068     *
069     * @param bundle resource bundle name
070     * @param sourceClass the Class that is the source of the message
071     * @param key the key to locate the translation.
072     * @param args arguments for the translation.
073     */
074    public LocalizedMessage(String bundle, Class<?> sourceClass, String key,
075            Object... args) {
076        this.bundle = bundle;
077        this.sourceClass = sourceClass;
078        this.key = key;
079        if (args == null) {
080            this.args = null;
081        }
082        else {
083            this.args = Arrays.copyOf(args, args.length);
084        }
085    }
086
087    /**
088     * Sets a locale to use for localization.
089     *
090     * @param locale the locale to use for localization
091     */
092    public static void setLocale(Locale locale) {
093        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
094            sLocale = Locale.ROOT;
095        }
096        else {
097            sLocale = locale;
098        }
099    }
100
101    /**
102     * Gets the translated message.
103     *
104     * @return the translated message.
105     */
106    public String getMessage() {
107        String result;
108        try {
109            // Important to use the default class loader, and not the one in
110            // the GlobalProperties object. This is because the class loader in
111            // the GlobalProperties is specified by the user for resolving
112            // custom classes.
113            final ResourceBundle resourceBundle = getBundle();
114            final String pattern = resourceBundle.getString(key);
115            final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
116            result = formatter.format(args);
117        }
118        catch (final MissingResourceException ignored) {
119            // If the Check author didn't provide i18n resource bundles
120            // and logs audit event messages directly, this will return
121            // the author's original message
122            final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
123            result = formatter.format(args);
124        }
125        return result;
126    }
127
128    /**
129     * Obtain the ResourceBundle. Uses the classloader
130     * of the class emitting this message, to be sure to get the correct
131     * bundle.
132     *
133     * @return a ResourceBundle.
134     */
135    private ResourceBundle getBundle() {
136        return ResourceBundle.getBundle(bundle, sLocale, sourceClass.getClassLoader(),
137                new Utf8Control());
138    }
139
140    /**
141     * <p>
142     * Custom ResourceBundle.Control implementation which allows explicitly read
143     * the properties files as UTF-8.
144     * </p>
145     */
146    public static class Utf8Control extends Control {
147
148        @Override
149        public ResourceBundle newBundle(String baseName, Locale locale, String format,
150                 ClassLoader loader, boolean reload) throws IOException {
151            // The below is a copy of the default implementation.
152            final String bundleName = toBundleName(baseName, locale);
153            final String resourceName = toResourceName(bundleName, "properties");
154            final URL url = loader.getResource(resourceName);
155            ResourceBundle resourceBundle = null;
156            if (url != null) {
157                final URLConnection connection = url.openConnection();
158                if (connection != null) {
159                    connection.setUseCaches(!reload);
160                    try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
161                            StandardCharsets.UTF_8)) {
162                        // Only this line is changed to make it read property files as UTF-8.
163                        resourceBundle = new PropertyResourceBundle(streamReader);
164                    }
165                }
166            }
167            return resourceBundle;
168        }
169
170    }
171
172}