yakworks.i18n.icu.ICUBundleMessageSource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-icu4j Show documentation
Show all versions of spring-icu4j Show documentation
Spring MessageSource Service for icu4j
/*
* Copyright 2002-2018 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 yakworks.i18n.icu;
import com.ibm.icu.text.MessageFormat;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* ICU4j Overrides, Lost of copy paste horseplay as so much in ReloadableResourceBundleMessageSource is
* private and final. The core issue here is that we need to return com.ibm.icu.text.MessageFormat and not java.text.MessageFormat
*/
public class ICUBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private static final String XML_SUFFIX = ".xml";
@Nullable
private Properties fileEncodings;
private boolean concurrentRefresh = true;
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
private ResourceLoader resourceLoader = new DefaultResourceLoader();
// Cache to hold already loaded properties per filename
private final ConcurrentMap cachedProperties = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
private final ConcurrentMap cachedMergedProperties = new ConcurrentHashMap<>();
@Override
@Nullable
protected java.text.MessageFormat resolveCode(String code, Locale locale) {
throw new UnsupportedOperationException("Use resolveCodeICU for ibm.icu");
}
/**
* Resolves the given message code as key in the retrieved bundle files,
* using a cached MessageFormat instance per message code.
*/
@Nullable
protected MessageFormat resolveCodeICU(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormatICU(code, locale);
if (result != null) {
return result;
}
}
else {
for (String basename : getBasenameSet()) {
List filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormatICU(code, locale);
if (result != null) {
return result;
}
}
}
}
return null;
}
protected void mergePluginProperties(final Locale locale, Properties mergedProps) {
//empty, can be overridden for loading defualts first
}
@Override
protected PropertiesHolder getMergedProperties(Locale locale) {
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = newProperties();
//this method does nothing here but is called for users to override. for example in grails we want to merge plugins first
mergePluginProperties(locale, mergedProps);
long latestTimestamp = -1;
String[] basenames = StringUtils.toStringArray(getBasenameSet());
for (int i = basenames.length - 1; i >= 0; i--) {
List filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() != null) {
mergedProps.putAll(propHolder.getProperties());
if (propHolder.getFileTimestamp() > latestTimestamp) {
latestTimestamp = propHolder.getFileTimestamp();
}
}
}
}
mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
@Override
protected PropertiesHolder getProperties(String filename) {
PropertiesHolder propHolder = this.cachedProperties.get(filename);
long originalTimestamp = -2;
if (propHolder != null) {
originalTimestamp = propHolder.getRefreshTimestamp();
if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) {
// Up to date
return propHolder;
}
}
else {
propHolder = new PropertiesHolder();
PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder);
if (existingHolder != null) {
propHolder = existingHolder;
}
}
// At this point, we need to refresh...
if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
// A populated but stale holder -> could keep using it.
if (!propHolder.refreshLock.tryLock()) {
// Getting refreshed by another thread already ->
// let's return the existing properties for the time being.
return propHolder;
}
}
else {
propHolder.refreshLock.lock();
}
try {
PropertiesHolder existingHolder = this.cachedProperties.get(filename);
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
return existingHolder;
}
return refreshPropertiesICU(filename, propHolder);
}
finally {
propHolder.refreshLock.unlock();
}
}
public PropertiesHolder refreshPropertiesICU(String filename, @Nullable PropertiesHolder propHolder) {
long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if (!resource.exists()) {
resource = this.resourceLoader.getResource(filename + XML_SUFFIX);
}
if (resource.exists()) {
long fileTimestamp = -1;
if (getCacheMillis() >= 0) {
// Last-modified timestamp of file will just be read if caching with timeout.
try {
fileTimestamp = resource.lastModified();
if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
if (logger.isDebugEnabled()) {
logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
}
propHolder.setRefreshTimestamp(refreshTimestamp);
return propHolder;
}
}
catch (IOException ex) {
// Probably a class path resource: cache it forever.
if (logger.isDebugEnabled()) {
logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex);
}
fileTimestamp = -1;
}
}
try {
Properties props = loadProperties(resource, filename);
propHolder = new PropertiesHolder(props, fileTimestamp);
}
catch (IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex);
}
// Empty holder representing "not valid".
propHolder = new PropertiesHolder();
}
}
else {
// Resource does not exist.
if (logger.isDebugEnabled()) {
logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");
}
// Empty holder representing "not found".
propHolder = new PropertiesHolder();
}
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
return propHolder;
}
public void clearCache() {
logger.debug("Clearing entire resource bundle cache");
this.cachedProperties.clear();
this.cachedMergedProperties.clear();
}
/**
* Override
*/
protected class PropertiesHolder extends ReloadableResourceBundleMessageSource.PropertiesHolder {
private final ReentrantLock refreshLock = new ReentrantLock();
/** Cache to hold already generated MessageFormats per message code. */
private final ConcurrentMap> cachedMessageFormats =
new ConcurrentHashMap<>();
public PropertiesHolder() {
super();
}
public PropertiesHolder(Properties properties, long fileTimestamp) {
super(properties, fileTimestamp);
}
@Override
@Nullable
public java.text.MessageFormat getMessageFormat(String code, Locale locale) {
throw new UnsupportedOperationException("Use getMessageFormatICU for ibm.icu");
}
@Nullable
public com.ibm.icu.text.MessageFormat getMessageFormatICU(String code, Locale locale) {
if (this.getProperties() == null) {
return null;
}
Map localeMap = this.cachedMessageFormats.get(code);
if (localeMap != null) {
com.ibm.icu.text.MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
String msg = this.getProperties().getProperty(code);
if (msg != null) {
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
if (existing != null) {
localeMap = existing;
}
}
com.ibm.icu.text.MessageFormat result = createMessageFormatICU(msg, locale);
localeMap.put(locale, result);
return result;
}
return null;
}
}
protected com.ibm.icu.text.MessageFormat createMessageFormatICU(String msg, Locale locale) {
return new com.ibm.icu.text.MessageFormat(msg, locale);
}
@Override
protected java.text.MessageFormat createMessageFormat(String msg, Locale locale) {
throw new UnsupportedOperationException("Use createMessageFormatICU for ibm.icu");
}
}