/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.messagesource.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import org.openmrs.messagesource.MutableMessageSource;
import org.openmrs.messagesource.PresentationMessage;
import org.openmrs.module.Module;
import org.openmrs.module.ModuleFactory;
import org.openmrs.util.LocaleUtility;
import org.openmrs.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* ResourceBundleMessageSource extends ReloadableResourceBundleMessageSource to provide the
* additional features of a MutableMessageSource.
*/
public class MutableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource implements MutableMessageSource {
private Logger log = LoggerFactory.getLogger(getClass());
/**
* Local reference to basenames used to search for properties files.
*/
private String[] basenames = new String[0];
private int cacheMilliseconds = -1;
private long lastCached = System.currentTimeMillis();
/** Cached list of available locales. */
private Collection<Locale> locales;
/**
* @see org.openmrs.messagesource.MessageSourceService#getLocales()
*/
@Override
public Collection<Locale> getLocales() {
long now = System.currentTimeMillis();
if (locales == null || cacheMilliseconds <= 0 || now - cacheMilliseconds > lastCached) {
locales = findLocales();
lastCached = now;
}
return locales;
}
@Override
public void setCacheSeconds(int cacheSeconds) {
this.cacheMilliseconds = cacheSeconds * 1000;
super.setCacheSeconds(cacheSeconds);
}
/**
* This method looks at the current property files and deduces what locales are available from
* those
*
* @see #getLocales()
* @see #findPropertiesFiles()
*/
private Collection<Locale> findLocales() {
Collection<Locale> foundLocales = new HashSet<Locale>();
for (Resource propertiesFile : findPropertiesFiles()) {
String filename = propertiesFile.getFilename();
Locale parsedLocale = parseLocaleFrom(filename);
foundLocales.add(parsedLocale);
}
if (foundLocales.isEmpty()) {
log.warn("no locales found.");
}
return foundLocales;
}
/**
* Utility method for deriving a locale from a filename, presumed to have an embedded locale
* specification near the end. For instance messages_it.properties if the filename is
* messages.properties, the Locale is presumed to be the default set for Java
*
* @param filename the name to parse
* @return Locale derived from the given string
*/
private Locale parseLocaleFrom(String filename) {
Locale parsedLocale = null;
// trim off leading basename
filename = filename.substring("messages".length());
// trim off extension
String localespec = filename.substring(0, filename.indexOf('.'));
if ("".equals(localespec)) {
parsedLocale = Locale.getDefault();
} else {
localespec = localespec.substring(1); // trim off leading '_'
parsedLocale = LocaleUtility.fromSpecification(localespec);
}
return parsedLocale;
}
/**
* Returns all available messages.
*
* @see org.openmrs.messagesource.MessageSourceService#getPresentations()
*/
@Override
public Collection<PresentationMessage> getPresentations() {
Collection<PresentationMessage> presentations = new Vector<PresentationMessage>();
for (Resource propertiesFile : findPropertiesFiles()) {
Locale currentLocale = parseLocaleFrom(propertiesFile.getFilename());
Properties props = new Properties();
try {
OpenmrsUtil.loadProperties(props, propertiesFile.getInputStream());
for (Map.Entry<Object, Object> property : props.entrySet()) {
presentations.add(new PresentationMessage(property.getKey().toString(), currentLocale, property
.getValue().toString(), ""));
}
}
catch (Exception e) {
// skip over errors in loading a single file
log.error("Unable to load properties from file: " + propertiesFile.getFilename(), e);
}
}
return presentations;
}
/**
* Override to obtain a local reference to the basenames.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setBasename(java.lang.String)
*/
@Override
public void setBasename(String basename) {
super.setBasename(basename);
this.basenames = new String[] { basename };
}
/**
* Override to obtain a local reference to the basenames.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setBasenames(java.lang.String[])
*/
@Override
public void setBasenames(String... basenames) {
if (basenames == null) {
this.basenames = new String[0];
} else {
this.basenames = Arrays.copyOf(basenames, basenames.length);
}
//add module file urls to basenames used for locating message properties files
Collection<Module> modules = ModuleFactory.getStartedModules();
if (!modules.isEmpty()) {
String[] names = new String[this.basenames.length + modules.size()];
System.arraycopy(this.basenames, 0, names, 0, this.basenames.length);
int index = this.basenames.length;
for (Module module : modules) {
names[index] = "jar:file:" + module.getFile().getAbsolutePath() + "!/messages";
index++;
}
basenames = names;
}
super.setBasenames(basenames);
}
/**
* @see org.openmrs.messagesource.MutableMessageSource#addPresentation(org.openmrs.messagesource.PresentationMessage)
*/
@Override
public void addPresentation(PresentationMessage message) {
Resource propertyFile = findPropertiesFileFor(message.getCode());
if (propertyFile != null) {
Properties props = new Properties();
try {
OpenmrsUtil.loadProperties(props, propertyFile.getInputStream());
props.setProperty(message.getCode(), message.getMessage());
//TODO properties files are now in api jar files which cannot be modified. TRUNK-4097
//We should therefore remove this method implementation or stop claiming that we are a mutable resource
//OpenmrsUtil.storeProperties(props, propertyFile.getInputStream(), "OpenMRS Application Messages");
}
catch (Exception e) {
log.error("Error generated", e);
}
}
}
/**
* @see org.openmrs.messagesource.MutableMessageSource#removePresentation(org.openmrs.messagesource.PresentationMessage)
*/
@Override
public void removePresentation(PresentationMessage message) {
Resource propertyFile = findPropertiesFileFor(message.getCode());
if (propertyFile != null) {
Properties props = new Properties();
try {
OpenmrsUtil.loadProperties(props, propertyFile.getInputStream());
props.remove(message.getCode());
//TODO properties files are now in api jar files which cannot be modified. TRUNK-4097
//We should therefore remove this method implementation or stop claiming that we are a mutable resource
//OpenmrsUtil.storeProperties(props, propertyFile, PROPERTIES_FILE_COMMENT);
}
catch (Exception e) {
log.error("Error generated", e);
}
}
}
/**
* Convenience method to scan the available properties files, looking for the one that has a
* definition for the given code.
*
* @param code
* @return the file which defines the code, or null if not found
*/
private Resource findPropertiesFileFor(String code) {
Properties props = new Properties();
Resource foundFile = null;
for (Resource propertiesFile : findPropertiesFiles()) {
props.clear();
try {
OpenmrsUtil.loadProperties(props, propertiesFile.getInputStream());
}
catch (Exception e) {
log.error("Error generated", e);
}
if (props.containsKey(code)) {
foundFile = propertiesFile;
break;
}
}
return foundFile;
}
/**
* Searches the filesystem for message properties files. ABKTODO: consider caching this, rather
* than searching every time
*
* @return an array of property file names
*/
private Resource[] findPropertiesFiles() {
Resource[] propertiesFiles = new Resource[]{};
try {
String pattern = "classpath*:messages*.properties";
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(OpenmrsClassLoader.getInstance());
propertiesFiles = resourceResolver.getResources(pattern);
}
catch (IOException e) {
log.error("Error generated", e);
}
if (log.isWarnEnabled() && (propertiesFiles.length == 0)) {
log.warn("No properties files found.");
}
return propertiesFiles;
}
/**
* @see org.openmrs.messagesource.MutableMessageSource#merge(MutableMessageSource, boolean)
*/
@Override
public void merge(MutableMessageSource fromSource, boolean overwrite) {
// collect all existing properties
Resource[] propertiesFiles = findPropertiesFiles();
Map<Locale, List<Resource>> localeToFilesMap = new HashMap<Locale, List<Resource>>();
Map<Resource, Properties> fileToPropertiesMap = new HashMap<Resource, Properties>();
for (Resource propertiesFile : propertiesFiles) {
Properties props = new Properties();
Locale propsLocale = parseLocaleFrom(propertiesFile.getFilename());
List<Resource> propList = localeToFilesMap.get(propsLocale);
if (propList == null) {
propList = new ArrayList<Resource>();
localeToFilesMap.put(propsLocale, propList);
}
propList.add(propertiesFile);
try {
OpenmrsUtil.loadProperties(props, propertiesFile.getInputStream());
fileToPropertiesMap.put(propertiesFile, props);
}
catch (Exception e) {
// skip over errors in loading a single file
log.error("Unable to load properties from file: " + propertiesFile.getFilename(), e);
}
}
// merge in the new properties
for (PresentationMessage message : fromSource.getPresentations()) {
Locale messageLocale = message.getLocale();
List<Resource> filelist = localeToFilesMap.get(messageLocale);
if (filelist != null) {
Properties propertyDestination = null;
boolean propExists = false;
for (Resource propertiesFile : filelist) {
Properties possibleDestination = fileToPropertiesMap.get(propertiesFile);
if (possibleDestination.containsKey(message.getCode())) {
propertyDestination = possibleDestination;
propExists = true;
break;
} else if (propertyDestination == null) {
propertyDestination = possibleDestination;
}
}
if ((propExists && overwrite) || !propExists) {
propertyDestination.put(message.getCode(), message.getMessage());
}
} else {
// no properties files for this locale
}
message.getCode();
}
}
/**
* @see org.openmrs.messagesource.MutableMessageSource#getPresentation(java.lang.String,
* java.util.Locale)
*/
@Override
public PresentationMessage getPresentation(String key, Locale forLocale) {
// TODO Auto-generated method stub
return null;
}
/**
* @see org.openmrs.messagesource.MutableMessageSource#getPresentationsInLocale(java.util.Locale)
*/
@Override
public Collection<PresentationMessage> getPresentationsInLocale(Locale locale) {
// TODO Auto-generated method stub
return null;
}
}