/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.i18n;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.scooterframework.admin.PropertyReader;
import com.scooterframework.common.logging.LogUtil;
/**
* <p>
* MessageResourcesManager is responsible for managing loading a locale's
* messages. It loads message resource files from the designated <tt>config/locales</tt>
* directory path based on the given <tt>base</tt> name.
* </p>
*
* <p>
* The way this class loads messages is the same as Java's <tt>ResourceBundle</tt>
* operates. It first looks through the specified Locale's language, country
* and variant, then through the default Locale's language, country and
* variant and finally using just the <tt>base</tt>:
* <pre>
* base + "_" + localeLanguage + "_" + localeCountry + "_" + localeVariant
* base + "_" + localeLanguage + "_" + localeCountry
* base + "_" + localeLanguage
* base + "_" + defaultLanguage + "_" + defaultCountry + "_" + defaultVariant
* base + "_" + defaultLanguage + "_" + defaultCountry
* base + "_" + defaultLanguage
* base
* </pre>
* </p>
*
* @author (Fei) John Chen
*/
public class MessageResourcesManager {
protected LogUtil log = LogUtil.getLogger(this.getClass().getName());
/*
* Resource file extension
*/
private static final String resourceFileExtension = ".properties";
/*
* Path to config directory
*/
private String configPath;
/*
* Base name of message resource files.
*/
private String baseName;
/*
* map of all loaded locales and keys
*/
private static Map<String, Locale> loadedLocalesKeyMap = new ConcurrentHashMap<String, Locale>();
/*
* map of all message files.
*/
private static Map<String, File> allMessageFiles = new ConcurrentHashMap<String, File>();
/*
* map of message file and its messages.
*/
private static Map<String, Properties> fileMessagesMap = new ConcurrentHashMap<String, Properties>();
/*
* map of locale files map in theory.
*/
private static Map<String, List<String>> localeFilesInTheoryMap = new ConcurrentHashMap<String, List<String>>();
/*
* map of locale files map in reality.
*/
private static Map<String, List<String>> localeFilesInRealityMap = new ConcurrentHashMap<String, List<String>>();
/*
* map of locale message map.
*/
private static Map<String, Properties> localeMsgMap = new ConcurrentHashMap<String, Properties>();
public MessageResourcesManager(String configPath, String baseName) {
this.configPath = configPath;
this.baseName = baseName;
loadAllFiles(configPath, baseName);
if (allMessageFiles == null || allMessageFiles.size() == 0) {
throw new IllegalArgumentException("There is no file under directory [" + configPath +
"] that starts with " + baseName + ".");
}
}
/**
* Returns path to the directory where all message files are stored.
*/
public String getConfigPath() {
return configPath;
}
/**
* Returns all message properties files as a map with file name as key and
* file object as value.
*
* @return map of files
*/
public Map<String, File> getAllFiles() {
return allMessageFiles;
}
public Set<Map.Entry<String, Locale>> getAllLoadedLocalesSet() {
return loadedLocalesKeyMap.entrySet();
}
/**
* Returns a message associated with the <tt>key</tt> and the
* <tt>values</tt> in a specific <tt>locale</tt>.
*
* <p>If there is no message associated with the <tt>key</tt> in messages
* property files, this method returns <tt>null</tt>.</p>
*
* @param key a message key in messages resource files
* @param locale a specific locale object
* @return a message string
*/
public String getMessage(String key, Locale locale) {
if (!hasLoaded(locale)) {
loadLocale(locale);
}
Properties prop = localeMsgMap.get(getLocaleKey(locale));
return (prop != null)?prop.getProperty(key):null;
}
public void loadLocale(Locale locale) {
if (locale == null) throw new IllegalArgumentException("Input locale cannot be null in loadLocale().");
if (hasLoaded(locale)) {
return;
}
String key = getLocaleKey(locale);
localeFilesInTheoryMap.put(key, getFilesInTheory(locale));
localeFilesInRealityMap.put(key, getFilesInReality(locale));
localeMsgMap.put(key, getMessages(locale));
loadedLocalesKeyMap.put(key, locale);
log.debug("loaded locale: " + key);
}
private boolean hasLoaded(Locale locale) {
return loadedLocalesKeyMap.containsKey(getLocaleKey(locale));
}
private String getLocaleKey(Locale locale) {
return (locale != null)?locale.toString():null;
}
private void loadAllFiles(String configPath, String baseName) {
File dir = new File(configPath);
if (!dir.isDirectory()) {
URL url = getClass().getClassLoader().getResource(configPath);
if (url != null) {
dir = new File(url.getFile());
if (!dir.isDirectory())
throw new IllegalArgumentException("Config path [" + configPath + "] must be a directory path.");
}
}
File[] files = dir.listFiles();
if (files == null) {
throw new IllegalArgumentException("There is no file in directory [" + configPath + "].");
}
int length = files.length;
for (int i = 0; i < length; i++) {
File file = files[i];
if (file.isFile() && file.getName().startsWith(baseName)) {
allMessageFiles.put(file.getName(), file);
}
}
}
private List<String> getFilesInTheory(Locale locale) {
List<String> list = new ArrayList<String>();
String base = baseName;
list.add(getFileName(base));
Locale defaultLocale = Locale.getDefault();
if (!getLocaleKey(defaultLocale).equals(getLocaleKey(locale))) {
populateList(list, base, defaultLocale);
}
populateList(list, base, locale);
return list;
}
private void populateList(List<String> list, String base, Locale locale) {
String language = locale.getLanguage();
String country = locale.getCountry();
String variant = locale.getVariant();
language = (language != null)?language:"";
country = (country != null)?country:"";
String file = "";
if (!"".equals(language)) {
file = getFileName(base + "_" + language);
list.add(file);
}
if (!"".equals(country)) {
file = getFileName(base + "_" + language + "_" + country);
list.add(file);
}
if (variant != null && !"".equals(variant)) {
file = getFileName(base + "_" + language + "_" + country + "_" + variant);
}
}
private String getFileName(String name) {
return name + resourceFileExtension;
}
private List<String> getFilesInReality(Locale locale) {
List<String> list = new ArrayList<String>();
List<String> tFiles = localeFilesInTheoryMap.get(getLocaleKey(locale));
if (tFiles != null) {
for (String file : tFiles) {
if (allMessageFiles.containsKey(file)) {
list.add(file);
}
}
}
return list;
}
private Properties getMessages(Locale locale) {
String key = getLocaleKey(locale);
Properties messages = localeMsgMap.get(key);
if (messages != null) return messages;
messages = new Properties();
List<String> files = (List<String>)localeFilesInRealityMap.get(key);
if (files != null) {
for (String fileName : files) {
Properties props = loadPropertiesFromFile(fileName);
messages.putAll(props);
}
}
localeMsgMap.put(key, messages);
return messages;
}
private Properties loadPropertiesFromFile(String fileName) {
if (hasLoadedFile(fileName)) {
return (Properties)fileMessagesMap.get(fileName);
}
Properties props = PropertyReader.loadOrderedPropertiesFromFile((File)allMessageFiles.get(fileName));
fileMessagesMap.put(fileName, props);
return props;
}
private boolean hasLoadedFile(String fileName) {
return fileMessagesMap.containsKey(fileName);
}
/**
* Reloads locales related to the updated file.
*
* @param file
*/
void fileUpdated(File file) {
//remove the file from fileMessagesMap
fileMessagesMap.remove(file.getName());
//find all locales that use the file
List<Locale> affectedLocales = new ArrayList<Locale>();
for (Map.Entry<String, List<String>> entry : localeFilesInRealityMap.entrySet()) {
String localeKey = entry.getKey();
List<String> files = localeFilesInRealityMap.get(localeKey);
if (files.contains(file.getName())) {
affectedLocales.add(loadedLocalesKeyMap.get(localeKey));
}
}
//reload for each affected locale
for (Locale locale : affectedLocales) {
String key = getLocaleKey(locale);
localeMsgMap.remove(key);
localeMsgMap.put(key, getMessages(locale));
}
}
/**
* Reloads locales after a new file is added.
*
* @param file
*/
void fileAdded(File file) {
allMessageFiles.put(file.getName(), file);
//reload all loaded locales
for (Map.Entry<String, Locale> entry : loadedLocalesKeyMap.entrySet()) {
String key = entry.getKey();
Locale locale = entry.getValue();
localeFilesInRealityMap.put(key, getFilesInReality(locale));
localeMsgMap.remove(key);
localeMsgMap.put(key, getMessages(locale));
}
}
/**
* Reloads locales after a new file is deleted.
*
* @param file
*/
void fileDeleted(File file) {
allMessageFiles.remove(file.getName());
fileMessagesMap.remove(file.getName());
//reload all loaded locales
for (Map.Entry<String, Locale> entry : loadedLocalesKeyMap.entrySet()) {
String key = entry.getKey();
Locale locale = entry.getValue();
localeFilesInRealityMap.put(key, getFilesInReality(locale));
localeMsgMap.remove(key);
localeMsgMap.put(key, getMessages(locale));
}
}
}