/* * Copyright (C) 2014 the original author or authors. * * 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 ro.pippo.core; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.pippo.core.route.RouteContext; import ro.pippo.core.util.ClasspathUtils; import ro.pippo.core.util.StringUtils; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; /** * Loads and caches message resource files based on the registered languages in * application.properties. * <p/> * This class is based on MessagesImpl.java from the Ninja Web Framework. * * @author James Moger */ public class Messages { private static final Logger log = LoggerFactory.getLogger(Messages.class); private final Map<String, Properties> languageMessages; private final Languages languages; public Messages(Languages languages) { this.languages = languages; this.languageMessages = loadRegisteredMessageResources(); } /** * Gets the requested localized message. * <p/> * <p> * The current Request and Response are used to help determine the messages * resource to use. * <ol> * <li>Exact locale match, return the registered locale message * <li>Language match, but not a locale match, return the registered * language message * <li>Return the default resource message * </ol> * <p> * The message can be formatted with optional arguments using the * {@link java.text.MessageFormat} syntax. * </p> * <p> * If the key does not exist in the messages resource, then the key name is * returned. * </p> * * @param key * @param routeContext * @param args * @return the message or the key if the key does not exist */ public String get(String key, RouteContext routeContext, Object... args) { String language = languages.getLanguageOrDefault(routeContext); return get(key, language, args); } /** * Gets the requested localized message. * <p/> * <ol> * <li>Exact locale match, return the registered locale message * <li>Language match but not a locale match, return the registered language * message * <li>Return the default resource message * </ol> * <p> * The message can be formatted with optional arguments using the * {@link java.text.MessageFormat} syntax. * </p> * <p> * If the key does not exist in the messages resource, then the key name is * returned. * </p> * * @param key * @param language * @param args * @return the message or the key if the key does not exist */ public String get(String key, String language, Object... args) { Properties messages = getMessagesForLanguage(language); String value = messages.getProperty(key); if (value != null) { return formatMessage(value, language, args); } else { log.warn("Failed to find '{}' in Messages", key); return key; } } /** * Gets the requested localized message. * <p/> * <p> * The current Request and Response are used to help determine the messages * resource to use. * <ol> * <li>Exact locale match, return the registered locale message * <li>Language match, but not a locale match, return the registered * language message * <li>Return the supplied default message * </ol> * <p> * The message can be formatted with optional arguments using the * {@link java.text.MessageFormat} syntax. * </p> * <p> * If the key does not exist in the messages resource, then the key name is * returned. * </p> * * @param key * @param defaultMessage * @param routeContext * @param args * @return the message or the key if the key does not exist */ public String getWithDefault(String key, String defaultMessage, RouteContext routeContext, Object... args) { String language = languages.getLanguageOrDefault(routeContext); return getWithDefault(key, defaultMessage, language, args); } /** * Gets the requested localized message. * <p/> * <p> * The current Request and Response are used to help determine the messages * resource to use. * <ol> * <li>Exact locale match, return the registered locale message * <li>Language match, but not a locale match, return the registered * language message * <li>Return supplied default message * </ol> * <p> * The message can be formatted with optional arguments using the * {@link java.text.MessageFormat} syntax. * </p> * * @param key * @param defaultMessage * @param args * @return the message or the key if the key does not exist */ public String getWithDefault(String key, String defaultMessage, String language, Object... args) { String value = get(key, language, args); if (value.equals(key)) { // key does not exist, format default message value = formatMessage(defaultMessage, language, args); } return value; } /** * Returns all localized messages. * <p> * The current Request and Response are used to help determine the messages * resource to use. * <ol> * <li>Exact locale match, return the registered locale messages * <li>Language match but not a locale match, return the registered language * messages * <li>Return the default messages * </ol> * </p> * * @param routeContext * @return all localized messages */ public Map<String, String> getAll(RouteContext routeContext) { String language = languages.getLanguageOrDefault(routeContext); return getAll(language); } /** * Returns all localized messages. * <ol> * <li>Exact locale match, return the registered locale messages * <li>Language match but not a locale match, return the registered language * messages * <li>Return the default messages * </ol> * * @param language * @return all localized messages */ public Map<String, String> getAll(String language) { Properties messages = getMessagesForLanguage(language); Map<String, String> map = new TreeMap<>(); for (Map.Entry<Object, Object> entry : messages.entrySet()) { map.put(entry.getKey().toString(), entry.getValue().toString()); } return map; } /** * Loads Pippo internal messages & application messages and returns the merger. * * @return all messages */ private Map<String, Properties> loadRegisteredMessageResources() { Map<String, Properties> internalMessages = loadRegisteredMessageResources("pippo/pippo-messages%s.properties"); Map<String, Properties> applicationMessages = loadRegisteredMessageResources("conf/messages%s.properties"); Map<String, Properties> allMessages = new TreeMap<>(); Set<String> merged = new HashSet<>(); // create aggregate messages for (Map.Entry<String, Properties> entry : internalMessages.entrySet()) { String language = entry.getKey(); Properties messages = entry.getValue(); allMessages.put(language, messages); if (applicationMessages.containsKey(language)) { // override internal messages with application messages messages.putAll(applicationMessages.get(language)); } merged.add(language); } // bring in the application languages which do not have an internal counterpart Set<String> unmerged = new HashSet<>(applicationMessages.keySet()); unmerged.removeAll(merged); for (String language : unmerged) { allMessages.put(language, applicationMessages.get(language)); } return allMessages; } /** * Loads all registered message resources. */ private Map<String, Properties> loadRegisteredMessageResources(String name) { Map<String, Properties> messageResources = new TreeMap<>(); // Load default messages Properties defaultMessages = loadMessages(String.format(name, "")); if (defaultMessages == null) { log.error("Could not locate the default messages resource '{}', please create it.", String.format(name, "")); } else { messageResources.put("", defaultMessages); } // Load the registered language resources List<String> registeredLanguages = languages.getRegisteredLanguages(); for (String language : registeredLanguages) { // First step: Load complete language eg. en-US Properties messages = loadMessages(String.format(name, "_" + language)); Properties messagesLangOnly = null; // If the language has a country code load the default values for // the language. For example missing keys in en-US will // be filled-in by the default language. String langComponent = languages.getLanguageComponent(language); if (!langComponent.equals(language)) { // see if we have already loaded the language messages messagesLangOnly = messageResources.get(langComponent); if (messagesLangOnly == null) { // load the language messages messagesLangOnly = loadMessages(String.format(name, "_" + langComponent)); } } // If a language is registered in application.properties it should // be there. if (messages == null) { log.error( "Could not locate the '{}' messages resource '{}' specified in '{}'.", language, String.format(name, "_" + language), PippoConstants.SETTING_APPLICATION_LANGUAGES); } else { // add a new language // start with the default messages Properties compositeMessages = new Properties(defaultMessages); // put all the language component messages "en" if (messagesLangOnly != null) { compositeMessages.putAll(messagesLangOnly); // cache language component messages if (!messageResources.containsKey(langComponent)) { Properties langResources = new Properties(); langResources.putAll(compositeMessages); messageResources.put(langComponent, langResources); } } // put all the language specific messages "en-US" compositeMessages.putAll(messages); // and add the composite messages to the hashmap with the // mapping. messageResources.put(language.toLowerCase(), compositeMessages); } } return Collections.unmodifiableMap(messageResources); } /** * Attempts to load a message resource. */ private Properties loadMessages(String fileOrUrl) { URL url = ClasspathUtils.locateOnClasspath(fileOrUrl); if (url != null) { try (InputStreamReader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) { Properties messages = new Properties(); messages.load(reader); return messages; } catch (IOException e) { log.error("Failed to load {}", fileOrUrl, e); } } return null; } /** * Retrieves the messages from an arbitrary one or two component language * String ("en-US", or "en" or "de"...). * <p/> * * @param language A one or two component language code such as "en", "en-US", or * "en-US,en;q=0.8,de;q=0.6". * @return The messages for the requested language or the default messages. */ private Properties getMessagesForLanguage(String language) { if (StringUtils.isNullOrEmpty(language)) { return languageMessages.get(""); } String supportedLanguage = languages.getLanguageOrDefault(language); if (StringUtils.isNullOrEmpty(supportedLanguage)) { log.debug("Messages for '{}' were requested. Using default messages.", language); return languageMessages.get(""); } // try the supported language Properties messages = languageMessages.get(supportedLanguage); if (messages != null) { return messages; } // check the supported language component String langComponent = languages.getLanguageComponent(supportedLanguage); messages = languageMessages.get(langComponent); if (messages != null) { return messages; } // return the default messages resource return languageMessages.get(""); } /** * Optionally formats a message for the requested language with * {@link java.text.MessageFormat}. * * @param message * @param language * @param args * @return the message */ private String formatMessage(String message, String language, Object... args) { if (args != null && args.length > 0) { // only format a message if we have arguments Locale locale = languages.getLocaleOrDefault(language); MessageFormat messageFormat = new MessageFormat(message, locale); return messageFormat.format(args); } return message; } }