org.pentaho.di.i18n.GlobalMessageUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.di.i18n.GlobalMessageUtil.java

Source

/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2018 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.i18n;

import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang.StringUtils;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.exception.KettleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;

public class GlobalMessageUtil {

    private static final Logger log = LoggerFactory.getLogger(GlobalMessageUtil.class);

    /**
     * Used when the preferred locale (as defined by the user) is not available.
     */
    public static final Locale FAILOVER_LOCALE = Locale.US;

    protected static final LanguageChoice langChoice = LanguageChoice.getInstance();

    protected static final ThreadLocal<Locale> threadLocales = new ThreadLocal();

    public static String formatErrorMessage(String key, String msg) {
        String s2 = key.substring(0, key.indexOf('.') + "ERROR_0000".length() + 1);
        return BaseMessages.getString("MESSUTIL.ERROR_FORMAT_MASK", s2, msg);
    }

    private static String decorateMissingKey(final String key) {
        final StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append('!').append(key).append('!');
        return keyBuilder.toString();
    }

    public static String getString(ResourceBundle bundle, String key) throws MissingResourceException {
        return MessageFormat.format(bundle.getString(key), new Object[] {});
    }

    public static String getErrorString(ResourceBundle bundle, String key) {
        return formatErrorMessage(key, getString(bundle, key));
    }

    public static String getString(ResourceBundle bundle, String key, String param1) {
        try {
            Object[] args = { param1 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getErrorString(ResourceBundle bundle, String key, String param1) {
        return formatErrorMessage(key, getString(bundle, key, param1));
    }

    public static String getString(ResourceBundle bundle, String key, String param1, String param2) {
        try {
            Object[] args = { param1, param2 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getErrorString(ResourceBundle bundle, String key, String param1, String param2) {
        return formatErrorMessage(key, getString(bundle, key, param1, param2));
    }

    public static String getString(ResourceBundle bundle, String key, String param1, String param2, String param3) {
        try {
            Object[] args = { param1, param2, param3 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getErrorString(ResourceBundle bundle, String key, String param1, String param2,
            String param3) {
        return formatErrorMessage(key, getString(bundle, key, param1, param2, param3));
    }

    public static String getString(ResourceBundle bundle, String key, String param1, String param2, String param3,
            String param4) {
        try {
            Object[] args = { param1, param2, param3, param4 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getString(ResourceBundle bundle, String key, String param1, String param2, String param3,
            String param4, String param5) {
        try {
            Object[] args = { param1, param2, param3, param4, param5 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getString(ResourceBundle bundle, String key, String param1, String param2, String param3,
            String param4, String param5, String param6) {
        try {
            Object[] args = { param1, param2, param3, param4, param5, param6 };
            return MessageFormat.format(bundle.getString(key), args);
        } catch (Exception e) {
            return decorateMissingKey(key);
        }
    }

    public static String getErrorString(ResourceBundle bundle, String key, String param1, String param2,
            String param3, String param4) {
        return formatErrorMessage(key, getString(bundle, key, param1, param2, param3, param4));
    }

    public static void setLocale(Locale newLocale) {
        threadLocales.set(newLocale);
    }

    public static Locale getLocale() {
        Locale rtn = threadLocales.get();
        if (rtn != null) {
            return rtn;
        }

        setLocale(langChoice.getDefaultLocale());
        return langChoice.getDefaultLocale();
    }

    /**
     * Returns a {@link LinkedHashSet} of {@link Locale}s for consideration when localizing text. The
     * {@link LinkedHashSet} contains the user selected preferred {@link Locale}, the failover {@link Locale}
     * ({@link Locale#ENGLISH}) and the {@link Locale#ROOT}.
     *
     * @return Returns a {@link LinkedHashSet} of {@link Locale}s for consideration when translating text
     */
    public static LinkedHashSet<Locale> getActiveLocales() {
        // Use a LinkedHashSet to maintain order
        final LinkedHashSet<Locale> activeLocales = new LinkedHashSet<>();
        // Example: messages_fr_FR.properties
        activeLocales.add(langChoice.getDefaultLocale());
        // Example: messages_en_US.properties
        activeLocales.add(FAILOVER_LOCALE);
        // Example: messages.properties
        activeLocales.add(Locale.ROOT);
        return activeLocales;
    }

    /**
     * Calls {@link #calculateString(String[], String, Object[], Class, String, boolean)} with the {@code
     * logNotFoundError} parameter set to {@code true} to ensure proper error logging when the localized string cannot be
     * found.
     */

    public static String calculateString(final String[] pkgNames, final String key, final Object[] parameters,
            final Class<?> resourceClass, final String bundleName) {
        return calculateString(pkgNames, key, parameters, resourceClass, bundleName, false, false);
    }

    public static String calculateString(final String[] pkgNames, final String key, Object[] parameters,
            final Class<?> resourceClass, final String bundleName, final boolean fallbackOnRoot) {
        return calculateString(pkgNames, key, parameters, resourceClass, bundleName, false, fallbackOnRoot);
    }

    /**
     * Returns the localized string for the given {@code key} and {@code parameters} in a bundle defined by the the
     * concatenation of the package names defined in {@code packageName} and @code bundleName} (the first valid
     * combination of {@code packageName} + {@code bundleName} wins), sing the provided {@code resourceClass}'s class
     * loader.
     *
     * @param pkgNames         an array of packages potentially containing the localized messages the first one found to
     *                         contain the messages is the one that is used to localize the message
     * @param key              the message key being looked up
     * @param parameters       parameters within the looked up message
     * @param resourceClass    the class whose class loader is used to getch the resource bundle
     * @param bundleName       the name of the message bundle
     * @param logNotFoundError determines whether an error is logged when the localized string cannot be found - it can be
     *                         used to suppress the log in cases where it is known that various combinations of parameters
     *                         will be tried to fetch the message, to avoid unnecessary error logging.
     * @param fallbackOnRoot   if true, and a {@link ResourceBundle} cannot be found for a given {@link Locale},
     *                         falls back on the ROOT {@link Locale}
     * @return the localized string for the given {@code key} and {@code parameters} in a bundle defined by the the
     * concatenation of the package names defined in {@code packageName} and @code bundleName} (the first valid
     * combination of {@code packageName} + @code bundleName} wins), sing the provided {@code resourceClass}'s class
     * loader
     */
    public static String calculateString(final String[] pkgNames, final String key, final Object[] parameters,
            final Class<?> resourceClass, final String bundleName, final boolean logNotFoundError,
            final boolean fallbackOnRoot) {

        final Set<Locale> activeLocales = getActiveLocales();
        for (final Locale locale : activeLocales) {
            final String string = calculateString(pkgNames, locale, key, parameters, resourceClass, bundleName,
                    fallbackOnRoot);
            if (!isMissingKey(string)) {
                return string;
            }
        }
        if (logNotFoundError) {
            final StringBuilder msg = new StringBuilder();
            msg.append("Message not found in the preferred and failover locale: key=[").append(key)
                    .append("], package=").append(Arrays.asList(pkgNames));
            log.error(Const.getStackTracker(new KettleException(msg.toString())));
        }
        return decorateMissingKey(key);
    }

    private static String calculateString(final String[] pkgNames, final Locale locale, final String key,
            final Object[] parameters, final Class<?> resourceClass, final String bundleName,
            final boolean fallbackOnRoot) {
        for (final String packageName : pkgNames) {
            try {
                return calculateString(packageName, locale, key, parameters, resourceClass, bundleName,
                        fallbackOnRoot);
            } catch (final MissingResourceException e) {
                continue;
            }
        }
        return null;
    }

    static String calculateString(final String packageName, final Locale locale, final String key,
            Object[] parameters, final Class<?> resourceClass, final String bundleName) {
        return calculateString(packageName, locale, key, parameters, resourceClass, bundleName, true);
    }

    @VisibleForTesting
    static String calculateString(final String packageName, final Locale locale, final String key,
            final Object[] parameters, final Class<?> resourceClass, final String bundleName,
            final boolean fallbackOnRoot) throws MissingResourceException {
        try {
            ResourceBundle bundle = getBundle(locale, packageName + "." + bundleName, resourceClass,
                    fallbackOnRoot);
            String unformattedString = bundle.getString(key);
            String string = MessageFormat.format(unformattedString, parameters);
            return string;
        } catch (IllegalArgumentException e) {
            final StringBuilder msg = new StringBuilder();
            msg.append("Format problem with key=[").append(key).append("], locale=[").append(locale)
                    .append("], package=").append(packageName).append(" : ").append(e.toString());
            log.error(msg.toString());
            log.error(Const.getStackTracker(e));
            throw new MissingResourceException(msg.toString(), packageName, key);
        }
    }

    /**
     * Retrieve a resource bundle of the default or fail-over locales.
     *
     * @param packagePath   The package to search in
     * @param resourceClass the class to use to resolve the bundle
     * @return The resource bundle
     * @throws MissingResourceException in case both resource bundles couldn't be found.
     */
    public static ResourceBundle getBundle(final String packagePath, final Class<?> resourceClass)
            throws MissingResourceException {
        final Set<Locale> activeLocales = getActiveLocales();
        for (final Locale locale : activeLocales) {
            try {
                return getBundle(locale, packagePath, resourceClass);
            } catch (MissingResourceException e) {
                final StringBuilder msg = new StringBuilder();
                msg.append("Unable to find properties file for package '").append(packagePath)
                        .append("' and class '").append(resourceClass.getName())
                        .append("' in the available locales: ").append(locale);
                // nothing to do, an exception will be thrown if no bundle is found
                log.warn(msg.toString());
            }
        }
        final StringBuilder msg = new StringBuilder();
        msg.append("Unable to find properties file for package '").append(packagePath).append("' and class '")
                .append(resourceClass.getName()).append("' in the available locales: ")
                .append(Arrays.asList(activeLocales));
        throw new MissingResourceException(msg.toString(), resourceClass.getName(), packagePath);
    }

    public static ResourceBundle getBundle(Locale locale, String packagePath, Class<?> resourceClass) {
        return getBundle(locale, packagePath, resourceClass, true);
    }

    /**
     * Returns a {@link ResourceBundle} corresponding to the given {@link Locale} package and resource class. Falls-back
     * on the ROOT {@link Locale}, if the {@code fallbackOnRoot} flag is true and the requested Locale is not available.
     *
     * @param locale         the {@link Locale} for which the {@link ResourceBundle} is being requested
     * @param packagePath
     * @param resourceClass
     * @param fallbackOnRoot if true, and a {@link ResourceBundle} cannot be found for the requested {@link Locale}, falls
     *                       back on the ROOT {@link Locale}
     * @return a {@link ResourceBundle} corresponding to the given {@link Locale} package and resource class
     */
    public static ResourceBundle getBundle(final Locale locale, final String packagePath,
            final Class<?> resourceClass, final boolean fallbackOnRoot) {
        final GlobalMessageControl control = new GlobalMessageControl(fallbackOnRoot);
        final String resourceName = control.toResourceName(control.toBundleName(packagePath, locale), "properties");

        ResourceBundle bundle;
        try {
            bundle = ResourceBundle.getBundle(packagePath, locale, resourceClass.getClassLoader(),
                    new GlobalMessageControl(fallbackOnRoot));
        } catch (final MissingResourceException e) {
            final StringBuilder msg = new StringBuilder();
            msg.append("Unable to find properties file '").append(resourceName).append("': ").append(e.toString());
            throw new MissingResourceException(msg.toString(), resourceClass.getName(), packagePath);
        }
        return bundle;
    }

    /**
     * Returns a string corresponding to the locale (Example: "en", "en_US").
     *
     * @param locale The {@link Locale} whose string representation it being returned
     * @return a string corresponding to the locale (Example: "en", "en_US").
     */
    protected static String getLocaleString(Locale locale) {
        final StringBuilder localeString = new StringBuilder();
        if (locale != null && !StringUtils.isBlank(locale.getLanguage())) {
            if (!StringUtils.isBlank(locale.getCountry())) {
                // force language to be lower case and country to be upper case
                localeString.append(locale.getLanguage().toLowerCase()).append('_')
                        .append(locale.getCountry().toUpperCase());
            } else {
                // force language to be lower case
                localeString.append(locale.getLanguage().toLowerCase());
            }
        }
        return localeString.toString();
    }

    /**
     * Returns true if the given {@code string} is null or is in the format of a missing key: !key!.
     *
     * @param string
     * @return true if the given {@code string} is null or is in the format of a missing key: !key!.
     */
    protected static boolean isMissingKey(final String string) {
        return string == null
                || (string.trim().startsWith("!") && string.trim().endsWith("!") && !string.trim().equals("!"));
    }

    /**
     * A custom {@link ResourceBundle.Control} implementation that provides the desired fall-back mechanism.
     */
    static class GlobalMessageControl extends ResourceBundle.Control {

        private boolean fallbackOnRoot;

        GlobalMessageControl(final boolean fallbackOnRoot) {
            this.fallbackOnRoot = fallbackOnRoot;
        }

        @Override
        public Locale getFallbackLocale(final String baseName, final Locale locale) {
            // we have our own fall-back mechanism
            return null;
        }

        @Override
        public List<Locale> getCandidateLocales(final String baseName, final Locale locale) {
            // we have our own fall-back mechanism
            final List<Locale> locales = super.getCandidateLocales(baseName, locale);
            // remove the root locale, as we want to handle it ourselves, unless the locale itself is root
            if (!fallbackOnRoot && !locale.equals(Locale.ROOT)) {
                locales.remove(Locale.ROOT);
            }
            return locales;
        }

        @Override
        public ResourceBundle newBundle(final String baseName, final Locale locale, final String format,
                final ClassLoader loader, final boolean reload)
                throws IllegalAccessException, InstantiationException, IOException {
            final String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
            ResourceBundle bundle;
            InputStream stream = null;
            if (reload) {
                final URL url = loader.getResource(resourceName);
                if (url != null) {
                    final URLConnection connection = url.openConnection();
                    if (connection != null) {
                        connection.setUseCaches(false);
                        stream = connection.getInputStream();
                    }
                }
            } else {
                stream = loader.getResourceAsStream(resourceName);
            }
            if (stream == null) {
                // Retry with the system class loader, just in case we are dealing with a messy plug-in.
                stream = ClassLoader.getSystemResourceAsStream(resourceName);
            }
            if (stream != null) {
                try {
                    // use UTF-8 encoding
                    bundle = new PropertyResourceBundle(
                            new InputStreamReader(stream, StandardCharsets.UTF_8.name()));
                } finally {
                    stream.close();
                }
            } else {
                final StringBuilder msg = new StringBuilder();
                msg.append("Unable to find properties file '").append(resourceName).append("'");
                throw new MissingResourceException(msg.toString(), loader.getClass().getName(), baseName);
            }
            return bundle;
        }
    }
}