com.haulmont.cuba.core.sys.AbstractMessages.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.core.sys.AbstractMessages.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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 com.haulmont.cuba.core.sys;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.haulmont.chile.core.datatypes.FormatStrings;
import com.haulmont.chile.core.datatypes.FormatStringsRegistry;
import com.haulmont.cuba.core.global.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang.text.StrTokenizer;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.haulmont.bali.util.Preconditions.checkNotNullArgument;

/**
 * <code>Messages</code> implementation common for all tiers.
 *
 */
public abstract class AbstractMessages implements Messages {

    public static final String BUNDLE_NAME = "messages";
    public static final String EXT = ".properties";

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

    @Inject
    protected MessageTools messageTools;

    @Inject
    protected FormatStringsRegistry formatStringsRegistry;

    protected Pattern enumSubclassPattern = Pattern.compile("\\$[1-9]");

    protected GlobalConfig globalConfig;

    protected String confDir;

    protected String mainMessagePack;

    protected Map<String, String> strCache = new ConcurrentHashMap<>();

    // Using ConcurrentHashMap instead of synchronized Set for better parallelism
    protected Map<String, String> notFoundCache = new ConcurrentHashMap<>();

    protected Cache<String, Properties> filePropertiesCache = CacheBuilder.newBuilder().build();
    protected Cache<String, Properties> resourcePropertiesCache = CacheBuilder.newBuilder().build();

    protected final static Properties PROPERTIES_NOT_FOUND = new Properties();

    protected abstract Locale getUserLocale();

    protected abstract String searchRemotely(String pack, String key, Locale locale);

    @Inject
    public void setConfiguration(Configuration configuration) {
        globalConfig = configuration.getConfig(GlobalConfig.class);
        confDir = globalConfig.getConfDir().replaceAll("\\\\", "/");
    }

    @PostConstruct
    protected void init() {
        mainMessagePack = AppContext.getProperty("cuba.mainMessagePack");
        if (mainMessagePack == null) {
            throw new DevelopmentException("Property cuba.mainMessagePack is not set");
        }

        log.debug("Main message pack: " + mainMessagePack);

        for (Locale locale : globalConfig.getAvailableLocales().values()) {
            String numberDecimalSeparator = getMainMessage("numberDecimalSeparator", locale);
            String numberGroupingSeparator = getMainMessage("numberGroupingSeparator", locale);
            String integerFormat = getMainMessage("integerFormat", locale);
            String doubleFormat = getMainMessage("doubleFormat", locale);
            String decimalFormat = getMainMessage("decimalFormat", locale);
            String dateFormat = getMainMessage("dateFormat", locale);
            String dateTimeFormat = getMainMessage("dateTimeFormat", locale);
            String timeFormat = getMainMessage("timeFormat", locale);
            String trueString = getMainMessage("trueString", locale);
            String falseString = getMainMessage("falseString", locale);
            if (numberDecimalSeparator.equals("numberDecimalSeparator")
                    || numberGroupingSeparator.equals("numberGroupingSeparator")
                    || integerFormat.equals("integerFormat") || doubleFormat.equals("doubleFormat")
                    || decimalFormat.equals("decimalFormat") || dateFormat.equals("dateFormat")
                    || dateTimeFormat.equals("dateTimeFormat") || timeFormat.equals("timeFormat"))
                log.warn("Localized format strings are not defined. "
                        + "Check cuba.mainMessagePack application property, it must point to a valid set of main message packs.");

            formatStringsRegistry.setFormatStrings(messageTools.trimLocale(locale),
                    new FormatStrings(numberDecimalSeparator.charAt(0), numberGroupingSeparator.charAt(0),
                            integerFormat, doubleFormat, decimalFormat, dateFormat, dateTimeFormat, timeFormat,
                            trueString, falseString));
        }
    }

    @Override
    public MessageTools getTools() {
        return messageTools;
    }

    @Override
    public String getMainMessagePack() {
        return mainMessagePack;
    }

    @Override
    public String getMessage(Class caller, String key) {
        Locale loc = getUserLocale();
        return getMessage(caller, key, loc);
    }

    @Override
    public String formatMessage(Class caller, String key, Object... params) {
        try {
            return String.format(getMessage(caller, key), params);
        } catch (IllegalFormatException e) {
            return key;
        }
    }

    @Override
    public String getMessage(Class caller, String key, Locale locale) {
        return getMessage(getPackName(caller), key, locale);
    }

    @Override
    public String formatMessage(Class caller, String key, Locale locale, Object... params) {
        try {
            return String.format(getMessage(caller, key, locale), params);
        } catch (IllegalFormatException e) {
            return key;
        }
    }

    @Override
    public String getMessage(Enum caller) {
        checkNotNullArgument(caller, "Enum parameter 'caller' is null");

        Locale loc = getUserLocale();
        return getMessage(caller, loc);
    }

    @Override
    public String getMessage(Enum caller, Locale locale) {
        checkNotNullArgument(caller, "Enum parameter 'caller' is null");

        String className = caller.getClass().getName();
        int i = className.lastIndexOf('.');
        if (i > -1)
            className = className.substring(i + 1);
        // If enum has inner subclasses, its class name ends with "$1", "$2", ... suffixes. Cut them off.
        Matcher matcher = enumSubclassPattern.matcher(className);
        if (matcher.find()) {
            className = className.substring(0, matcher.start());
        }

        return getMessage(getPackName(caller.getClass()), className + "." + caller.name(), locale);
    }

    @Override
    public String getMessage(String pack, String key) {
        Locale loc = getUserLocale();
        return getMessage(pack, key, loc);
    }

    @Override
    public String getMainMessage(String key) {
        return getMainMessage(key, getUserLocale());
    }

    @Override
    public String getMainMessage(String key, Locale locale) {
        checkNotNullArgument(key, "Message key is null");
        return internalGetMessage(mainMessagePack, key, locale, key, false);
    }

    @Override
    public String formatMessage(String pack, String key, Object... params) {
        try {
            return String.format(getMessage(pack, key), params);
        } catch (IllegalFormatException e) {
            return key;
        }
    }

    @Override
    public String formatMainMessage(String key, Object... params) {
        try {
            return String.format(getMainMessage(key), params);
        } catch (IllegalFormatException e) {
            return key;
        }
    }

    @Override
    public String getMessage(String packs, String key, Locale locale) {
        checkNotNullArgument(packs, "Messages pack name is null");
        checkNotNullArgument(key, "Message key is null");

        String compositeKey = packs + "/" + key;
        String msg = internalGetMessage(mainMessagePack, compositeKey, locale, null, false);
        if (msg != null)
            return msg;

        return internalGetMessage(packs, key, locale, key, true);
    }

    @Nullable
    @Override
    public String findMessage(String packs, String key, @Nullable Locale locale) {
        checkNotNullArgument(packs, "Messages pack name is null");
        checkNotNullArgument(key, "Message key is null");

        if (locale == null)
            locale = getUserLocale();

        String compositeKey = packs + "/" + key;
        String[] split = mainMessagePack.split(" ");
        String lastMainMessagePack = split[split.length - 1];
        String msg = internalGetMessage(lastMainMessagePack, compositeKey, locale, null, false);
        if (msg != null)
            return msg;

        return internalGetMessage(packs, key, locale, null, true);
    }

    @Override
    public String formatMessage(String pack, String key, Locale locale, Object... params) {
        try {
            return String.format(getMessage(pack, key, locale), params);
        } catch (IllegalFormatException e) {
            return key;
        }
    }

    @Override
    public int getCacheSize() {
        return strCache.size();
    }

    @Override
    public void clearCache() {
        filePropertiesCache.invalidateAll();
        resourcePropertiesCache.invalidateAll();
        strCache.clear();
        notFoundCache.clear();
    }

    protected String internalGetMessage(String packs, String key, Locale locale, String defaultValue,
            boolean searchMainIfNotFound) {
        locale = messageTools.trimLocale(locale);

        String cacheKey = makeCacheKey(packs, key, locale, locale);

        String msg = strCache.get(cacheKey);
        if (msg != null)
            return msg;

        String notFound = notFoundCache.get(cacheKey);
        if (notFound != null)
            return defaultValue;

        msg = searchMessage(packs, key, locale, locale, new HashSet<>());
        if (msg != null) {
            cache(cacheKey, msg);
            return msg;
        }

        if (searchMainIfNotFound) {
            String tmpCacheKey = makeCacheKey(mainMessagePack, key, locale, locale);
            msg = searchMessage(tmpCacheKey, key, locale, locale, new HashSet<>());
            if (msg != null) {
                cache(cacheKey, msg);
                return msg;
            }
        }

        notFoundCache.put(cacheKey, key);
        return defaultValue;
    }

    @Nullable
    protected String searchMessage(String packs, String key, Locale locale, Locale truncatedLocale,
            Set<String> passedPacks) {
        StrTokenizer tokenizer = new StrTokenizer(packs);
        //noinspection unchecked
        List<String> list = tokenizer.getTokenList();
        Collections.reverse(list);
        for (String pack : list) {
            if (!enterPack(pack, locale, truncatedLocale, passedPacks))
                continue;

            String msg = searchOnePack(pack, key, locale, truncatedLocale, passedPacks);
            if (msg != null)
                return msg;

            Locale tmpLocale = truncatedLocale;
            while (tmpLocale != null) {
                tmpLocale = truncateLocale(tmpLocale);
                msg = searchOnePack(pack, key, locale, tmpLocale, passedPacks);
                if (msg != null)
                    return msg;
            }
        }
        if (log.isTraceEnabled()) {
            String packName = new StrBuilder().appendWithSeparators(list, ",").toString();
            log.trace("Resource '" + makeCacheKey(packName, key, locale, locale) + "' not found");
        }
        return null;
    }

    private Locale truncateLocale(Locale locale) {
        if (locale == null || StringUtils.isEmpty(locale.getCountry()))
            return null;
        return Locale.forLanguageTag(locale.getLanguage());
    }

    protected boolean enterPack(String pack, Locale locale, Locale truncatedLocale, Set<String> passedPacks) {
        String k = truncatedLocale == null ? pack + "/default" : pack + "/" + (locale == null ? "default" : locale);
        return passedPacks.add(k);
    }

    protected String searchOnePack(String pack, String key, Locale locale, Locale truncatedLocale,
            Set<String> passedPacks) {
        String cacheKey = makeCacheKey(pack, key, locale, truncatedLocale);

        String msg = strCache.get(cacheKey);
        if (msg != null)
            return msg;

        msg = searchFiles(pack, key, locale, truncatedLocale, passedPacks);
        if (msg == null) {
            msg = searchClasspath(pack, key, locale, truncatedLocale, passedPacks);
        }
        if (msg == null && locale.equals(truncatedLocale)) {
            msg = searchRemotely(pack, key, locale);
            if (msg != null) {
                cache(cacheKey, msg);
            }
        }
        return msg;
    }

    protected void cache(String key, String msg) {
        if (!strCache.containsKey(key))
            strCache.put(key, msg);
    }

    protected String searchFiles(String pack, String key, Locale locale, Locale truncatedLocale,
            Set<String> passedPacks) {
        StopWatch stopWatch = new Slf4JStopWatch("Messages.searchFiles");
        try {
            String cacheKey = makeCacheKey(pack, key, locale, truncatedLocale);

            String msg = strCache.get(cacheKey);
            if (msg != null)
                return msg;

            log.trace("searchFiles: " + cacheKey);

            String packPath = confDir + "/" + pack.replaceAll("\\.", "/");
            while (packPath != null && !packPath.equals(confDir)) {
                Properties properties = loadPropertiesFromFile(packPath, locale, truncatedLocale);
                if (properties != PROPERTIES_NOT_FOUND) {
                    msg = getMessageFromProperties(pack, key, locale, truncatedLocale, properties, passedPacks);
                    if (msg != null)
                        return msg;
                }
                // not found, keep searching
                int pos = packPath.lastIndexOf("/");
                if (pos < 0)
                    packPath = null;
                else
                    packPath = packPath.substring(0, pos);
            }
            return null;
        } finally {
            stopWatch.stop();
        }
    }

    protected String searchClasspath(String pack, String key, Locale locale, Locale truncatedLocale,
            Set<String> passedPacks) {
        StopWatch stopWatch = new Slf4JStopWatch("Messages.searchClasspath");
        try {
            String cacheKey = makeCacheKey(pack, key, locale, truncatedLocale);

            String msg = strCache.get(cacheKey);
            if (msg != null)
                return msg;

            log.trace("searchClasspath: " + cacheKey);

            String packPath = "/" + pack.replaceAll("\\.", "/");
            while (packPath != null) {
                Properties properties = loadPropertiesFromResource(packPath, locale, truncatedLocale);
                if (properties != PROPERTIES_NOT_FOUND) {
                    msg = getMessageFromProperties(pack, key, locale, truncatedLocale, properties, passedPacks);
                    if (msg != null)
                        return msg;
                }
                // not found, keep searching
                int pos = packPath.lastIndexOf("/");
                if (pos < 0)
                    packPath = null;
                else
                    packPath = packPath.substring(0, pos);
            }
            return null;
        } finally {
            stopWatch.stop();
        }
    }

    @Nullable
    protected String getMessageFromProperties(String pack, String key, Locale locale, Locale truncatedLocale,
            Properties properties, Set<String> passedPacks) {
        String message;
        message = properties.getProperty(key);
        if (message != null) {
            cache(makeCacheKey(pack, key, locale, truncatedLocale), message);
            if (truncatedLocale == null)
                cache(makeCacheKey(pack, key, locale, null), message);
        }

        if (message == null) {
            // process includes after to support overriding
            message = searchIncludes(properties, key, locale, truncatedLocale, passedPacks);
        }
        return message;
    }

    @Nullable
    protected String searchIncludes(Properties properties, String key, Locale locale, Locale truncatedLocale,
            Set<String> passedPacks) {
        String includesProperty = properties.getProperty("@include");
        if (includesProperty != null) {
            // multiple includes separated by comma
            String[] includes = StringUtils.split(includesProperty, " ,");
            if (includes != null && includes.length > 0) {
                ArrayUtils.reverse(includes);
                for (String includePath : includes) {
                    includePath = StringUtils.trimToNull(includePath);
                    if (includePath != null) {
                        String message = searchMessage(includePath, key, locale, truncatedLocale, passedPacks);
                        if (message != null) {
                            return message;
                        }
                    }
                }
            }
        }
        return null;
    }

    protected Properties loadPropertiesFromFile(String packPath, Locale locale, Locale truncatedLocale) {
        final String fileName = packPath + "/" + BUNDLE_NAME + getLocaleSuffix(truncatedLocale) + EXT;
        try {
            return filePropertiesCache.get(fileName, new Callable<Properties>() {
                @Override
                public Properties call() throws Exception {
                    File file = new File(fileName);
                    if (file.exists()) {
                        try (FileInputStream stream = new FileInputStream(file);
                                InputStreamReader reader = new InputStreamReader(stream,
                                        StandardCharsets.UTF_8.name())) {
                            Properties properties = new Properties();
                            properties.load(reader);
                            return properties;
                        }
                    }
                    return PROPERTIES_NOT_FOUND;
                }
            });
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    protected Properties loadPropertiesFromResource(String packPath, Locale locale, Locale truncatedLocale) {
        final String name = packPath + "/" + BUNDLE_NAME + getLocaleSuffix(truncatedLocale) + EXT;
        try {
            return resourcePropertiesCache.get(name, new Callable<Properties>() {
                @Override
                public Properties call() throws Exception {
                    InputStream stream = getClass().getResourceAsStream(name);
                    if (stream != null) {
                        try (InputStreamReader reader = new InputStreamReader(stream,
                                StandardCharsets.UTF_8.name())) {
                            Properties properties = new Properties();
                            properties.load(reader);
                            return properties;
                        } finally {
                            IOUtils.closeQuietly(stream);
                        }
                    }
                    return PROPERTIES_NOT_FOUND;
                }
            });
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    protected String getLocaleSuffix(Locale locale) {
        return (locale != null ? "_" + locale : "");
    }

    protected String makeCacheKey(String pack, String key, @Nullable Locale locale,
            @Nullable Locale truncatedLocale) {
        if (truncatedLocale == null)
            return pack + "/default/" + key;

        return pack + "/" + (locale == null ? "default" : locale) + "/" + key;
    }

    protected String getPackName(Class c) {
        String className = c.getName();
        int pos = className.lastIndexOf(".");
        if (pos > 0)
            return className.substring(0, pos);
        else
            return "";
    }
}