LocalizedMessage.java

///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;

/**
 * Represents a message that can be localised. The translations come from
 * message.properties files. The underlying implementation uses
 * java.text.MessageFormat.
 */
public class LocalizedMessage {

    /** The locale to localise messages to. **/
    private static Locale sLocale = Locale.getDefault();

    /** Name of the resource bundle to get messages from. **/
    private final String bundle;

    /** Class of the source for this message. */
    private final Class<?> sourceClass;

    /**
     * Key for the message format.
     **/
    private final String key;

    /**
     * Arguments for java.text.MessageFormat, that is why type is Object[].
     *
     * <p>Note: Changing types from Object[] will be huge breaking compatibility, as Module
     * messages use some type formatting already, so better to keep it as Object[].
     * </p>
     */
    private final Object[] args;

    /**
     * Creates a new {@code LocalizedMessage} instance.
     *
     * @param bundle resource bundle name
     * @param sourceClass the Class that is the source of the message
     * @param key the key to locate the translation.
     * @param args arguments for the translation.
     */
    public LocalizedMessage(String bundle, Class<?> sourceClass, String key,
            Object... args) {
        this.bundle = bundle;
        this.sourceClass = sourceClass;
        this.key = key;
        if (args == null) {
            this.args = null;
        }
        else {
            this.args = UnmodifiableCollectionUtil.copyOfArray(args, args.length);
        }
    }

    /**
     * Sets a locale to use for localization.
     *
     * @param locale the locale to use for localization
     */
    public static void setLocale(Locale locale) {
        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
            sLocale = Locale.ROOT;
        }
        else {
            sLocale = locale;
        }
    }

    /**
     * Gets the translated message.
     *
     * @return the translated message.
     */
    public String getMessage() {
        String result;
        try {
            // Important to use the default class loader, and not the one in
            // the GlobalProperties object. This is because the class loader in
            // the GlobalProperties is specified by the user for resolving
            // custom classes.
            final ResourceBundle resourceBundle = getBundle();
            final String pattern = resourceBundle.getString(key);
            final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
            result = formatter.format(args);
        }
        catch (final MissingResourceException ignored) {
            // If the Check author didn't provide i18n resource bundles
            // and logs audit event messages directly, this will return
            // the author's original message
            final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
            result = formatter.format(args);
        }
        return result;
    }

    /**
     * Obtain the ResourceBundle. Uses the classloader
     * of the class emitting this message, to be sure to get the correct
     * bundle.
     *
     * @return a ResourceBundle.
     */
    private ResourceBundle getBundle() {
        return ResourceBundle.getBundle(bundle, sLocale, sourceClass.getClassLoader(),
                new Utf8Control());
    }

    /**
     * <p>
     * Custom ResourceBundle.Control implementation which allows explicitly read
     * the properties files as UTF-8.
     * </p>
     */
    public static class Utf8Control extends Control {

        @Override
        public ResourceBundle newBundle(String baseName, Locale locale, String format,
                 ClassLoader loader, boolean reload) throws IOException {
            // The below is a copy of the default implementation.
            final String bundleName = toBundleName(baseName, locale);
            final String resourceName = toResourceName(bundleName, "properties");
            final URL url = loader.getResource(resourceName);
            ResourceBundle resourceBundle = null;
            if (url != null) {
                final URLConnection connection = url.openConnection();
                if (connection != null) {
                    connection.setUseCaches(!reload);
                    try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
                            StandardCharsets.UTF_8)) {
                        // Only this line is changed to make it read property files as UTF-8.
                        resourceBundle = new PropertyResourceBundle(streamReader);
                    }
                }
            }
            return resourceBundle;
        }

    }

}