From 986d1ad1b2f1c84f0042f9c2bbb1ef3a0d9f7c0f Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Mon, 11 Oct 2010 21:30:22 -0400 Subject: [PATCH] INT-1382 added DynamicExpression, ExpressionSource strategy, and ResourceBundle-based implementation --- .../expression/DynamicExpression.java | 147 +++++ .../expression/ExpressionSource.java | 33 + ...oadableResourceBundleExpressionSource.java | 572 ++++++++++++++++++ .../expression/DynamicExpressionTests.java | 70 +++ .../expression/expressions.properties | 1 + 5 files changed, 823 insertions(+) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/expression/DynamicExpression.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionSource.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/expression/ReloadableResourceBundleExpressionSource.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/expression/DynamicExpressionTests.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/expression/expressions.properties diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/DynamicExpression.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/DynamicExpression.java new file mode 100644 index 0000000000..2857c6fcc8 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/DynamicExpression.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2010 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 org.springframework.integration.expression; + +import java.util.Locale; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; + +/** + * An implementation of {@link Expression} that delegates to an {@link ExpressionSource} + * for resolving the actual Expression instance per-invocation at runtime. + * + * @author Mark Fisher + * @since 2.0 + */ +public class DynamicExpression implements Expression { + + private final String key; + + private final ExpressionSource expressionSource; + + + public DynamicExpression(String key, ExpressionSource expressionSource) { + Assert.notNull(key, "key must not be null"); + Assert.notNull(expressionSource, "expressionSource must not be null"); + this.key = key; + this.expressionSource = expressionSource; + } + + + public Object getValue() throws EvaluationException { + return this.resolveExpression().getValue(); + } + + public Object getValue(Object rootObject) throws EvaluationException { + return this.resolveExpression().getValue(rootObject); + } + + public T getValue(Class desiredResultType) throws EvaluationException { + return this.resolveExpression().getValue(desiredResultType); + } + + public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + return this.resolveExpression().getValue(rootObject, desiredResultType); + } + + public Object getValue(EvaluationContext context) throws EvaluationException { + return this.resolveExpression().getValue(context); + } + + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + return this.getValue(context, rootObject); + } + + public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { + return this.getValue(context, desiredResultType); + } + + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) throws EvaluationException { + return this.getValue(context, rootObject, desiredResultType); + } + + public Class getValueType() throws EvaluationException { + return this.resolveExpression().getValueType(); + } + + public Class getValueType(Object rootObject) throws EvaluationException { + return this.resolveExpression().getValueType(rootObject); + } + + public Class getValueType(EvaluationContext context) throws EvaluationException { + return this.resolveExpression().getValueType(context); + } + + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + return this.resolveExpression().getValueType(context, rootObject); + } + + public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + return this.resolveExpression().getValueTypeDescriptor(); + } + + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + return this.resolveExpression().getValueTypeDescriptor(rootObject); + } + + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + return this.resolveExpression().getValueTypeDescriptor(context); + } + + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException { + return this.resolveExpression().getValueTypeDescriptor(context, rootObject); + } + + public boolean isWritable(EvaluationContext context) throws EvaluationException { + return this.resolveExpression().isWritable(context); + } + + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return this.resolveExpression().isWritable(context, rootObject); + } + + public boolean isWritable(Object rootObject) throws EvaluationException { + return this.isWritable(rootObject); + } + + public void setValue(EvaluationContext context, Object value) throws EvaluationException { + this.resolveExpression().setValue(context, value); + } + + public void setValue(Object rootObject, Object value) throws EvaluationException { + this.resolveExpression().setValue(rootObject, value); + } + + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + this.resolveExpression().setValue(context, rootObject, value); + } + + public String getExpressionString() { + return this.resolveExpression().getExpressionString(); + } + + private Expression resolveExpression() { + Locale locale = LocaleContextHolder.getLocale(); + return this.expressionSource.getExpression(this.key, locale); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionSource.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionSource.java new file mode 100644 index 0000000000..fca593da59 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionSource.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2010 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 org.springframework.integration.expression; + +import java.util.Locale; + +import org.springframework.expression.Expression; + +/** + * Strategy interface for retrieving Expressions. + * + * @author Mark Fisher + * @since 2.0 + */ +public interface ExpressionSource { + + Expression getExpression(String key, Locale locale); + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ReloadableResourceBundleExpressionSource.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ReloadableResourceBundleExpressionSource.java new file mode 100644 index 0000000000..fd70724c98 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ReloadableResourceBundleExpressionSource.java @@ -0,0 +1,572 @@ +/* + * Copyright 2002-2010 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 org.springframework.integration.expression; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * {@link ExpressionSource} implementation that accesses resource bundles using specified basenames. + * This class uses {@link java.util.Properties} instances as its custom data structure for expressions, + * loading them via a {@link org.springframework.util.PropertiesPersister} strategy: The default + * strategy is capable of loading properties files with a specific character encoding, if desired. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + * @see #setCacheSeconds + * @see #setBasenames + * @see #setDefaultEncoding + * @see #setFileEncodings + * @see #setPropertiesPersister + * @see #setResourceLoader + * @see org.springframework.util.DefaultPropertiesPersister + * @see org.springframework.core.io.DefaultResourceLoader + * @see java.util.ResourceBundle + */ +public class ReloadableResourceBundleExpressionSource implements ExpressionSource, ResourceLoaderAware { + + private static final String PROPERTIES_SUFFIX = ".properties"; + + private static final String XML_SUFFIX = ".xml"; + + private static final Log logger = LogFactory.getLog(ReloadableResourceBundleExpressionSource.class); + + + private volatile String[] basenames = new String[0]; + + private volatile String defaultEncoding; + + private volatile Properties fileEncodings; + + private volatile boolean fallbackToSystemLocale = true; + + private volatile long cacheMillis = -1; + + private volatile PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); + + private volatile ResourceLoader resourceLoader = new DefaultResourceLoader(); + + /** Cache to hold filename lists per Locale */ + private final Map>> cachedFilenames = + new HashMap>>(); + + /** Cache to hold already loaded properties per filename */ + private final Map cachedProperties = new HashMap(); + + /** Cache to hold merged loaded properties per locale */ + private final Map cachedMergedProperties = new HashMap(); + + private final ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + + + /** + * Set a single basename, following the basic ResourceBundle convention of + * not specifying file extension or language codes, but referring to a Spring + * resource location: e.g. "META-INF/expressions" for "META-INF/expressions.properties", + * "META-INF/expressions_en.properties", etc. + *

XML properties files are also supported: .g. "META-INF/expressions" will find + * and load "META-INF/expressions.xml", "META-INF/expressions_en.xml", etc as well. + * @param basename the single basename + * @see #setBasenames + * @see org.springframework.core.io.ResourceEditor + * @see java.util.ResourceBundle + */ + public void setBasename(String basename) { + setBasenames(new String[] {basename}); + } + + /** + * Set an array of basenames, each following the basic ResourceBundle convention + * of not specifying file extension or language codes, but referring to a Spring + * resource location: e.g. "META-INF/expressions" for "META-INF/expressions.properties", + * "META-INF/expressions_en.properties", etc. + *

XML properties files are also supported: .g. "META-INF/expressions" will find + * and load "META-INF/expressions.xml", "META-INF/expressions_en.xml", etc as well. + *

The associated resource bundles will be checked sequentially when resolving + * an expression key. Note that expression definitions in a previous resource + * bundle will override ones in a later bundle, due to the sequential lookup. + * @param basenames an array of basenames + * @see #setBasename + * @see java.util.ResourceBundle + */ + public void setBasenames(String[] basenames) { + if (basenames != null) { + this.basenames = new String[basenames.length]; + for (int i = 0; i < basenames.length; i++) { + String basename = basenames[i]; + Assert.hasText(basename, "Basename must not be empty"); + this.basenames[i] = basename.trim(); + } + } + else { + this.basenames = new String[0]; + } + } + + /** + * Set the default charset to use for parsing properties files. + * Used if no file-specific charset is specified for a file. + *

Default is none, using the java.util.Properties + * default encoding. + *

Only applies to classic properties files, not to XML files. + * @param defaultEncoding the default charset + * @see #setFileEncodings + * @see org.springframework.util.PropertiesPersister#load + */ + public void setDefaultEncoding(String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Set per-file charsets to use for parsing properties files. + *

Only applies to classic properties files, not to XML files. + * @param fileEncodings Properties with filenames as keys and charset + * names as values. Filenames have to match the basename syntax, + * with optional locale-specific appendices: e.g. "META-INF/expressions" + * or "META-INF/expressions_en". + * @see #setBasenames + * @see org.springframework.util.PropertiesPersister#load + */ + public void setFileEncodings(Properties fileEncodings) { + this.fileEncodings = fileEncodings; + } + + /** + * Set whether to fall back to the system Locale if no files for a specific + * Locale have been found. Default is "true"; if this is turned off, the only + * fallback will be the default file (e.g. "expressions.properties" for + * basename "expressions"). + *

Falling back to the system Locale is the default behavior of + * java.util.ResourceBundle. However, this is often not + * desirable in an application server environment, where the system Locale + * is not relevant to the application at all: Set this flag to "false" + * in such a scenario. + */ + public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { + this.fallbackToSystemLocale = fallbackToSystemLocale; + } + + /** + * Set the number of seconds to cache loaded properties files. + *

    + *
  • Default is "-1", indicating to cache forever (just like + * java.util.ResourceBundle). + *
  • A positive number will cache loaded properties files for the given + * number of seconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + *
  • A value of "0" will check the last-modified timestamp of the file on + * every expression access. Do not use this in a production environment! + *
+ */ + public void setCacheSeconds(int cacheSeconds) { + this.cacheMillis = (cacheSeconds * 1000); + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + *

The default is a DefaultPropertiesPersister. + * @see org.springframework.util.DefaultPropertiesPersister + */ + public void setPropertiesPersister(PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); + } + + /** + * Set the ResourceLoader to use for loading bundle properties files. + *

The default is a DefaultResourceLoader. Will get overridden by the + * ApplicationContext if running in a context, as it implements the + * ResourceLoaderAware interface. Can be manually overridden when + * running outside of an ApplicationContext. + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.context.ResourceLoaderAware + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + + /** + * Resolves the given key in the retrieved bundle files to an Expression. + */ + public Expression getExpression(String key, Locale locale) { + String expressionString = this.getExpressionString(key, locale); + if (expressionString != null) { + return this.parser.parseExpression(expressionString); + } + return null; + } + + private String getExpressionString(String key, Locale locale) { + if (this.cacheMillis < 0) { + PropertiesHolder propHolder = getMergedProperties(locale); + String result = propHolder.getProperty(key); + if (result != null) { + return result; + } + } + else { + for (String basename : this.basenames) { + List filenames = calculateAllFilenames(basename, locale); + for (String filename : filenames) { + PropertiesHolder propHolder = getProperties(filename); + String result = propHolder.getProperty(key); + if (result != null) { + return result; + } + } + } + } + return null; + } + + /** + * Get a PropertiesHolder that contains the actually visible properties + * for a Locale, after merging all specified resource bundles. + * Either fetches the holder from the cache or freshly loads it. + *

Only used when caching resource bundle contents forever, i.e. + * with cacheSeconds < 0. Therefore, merged properties are always + * cached forever. + */ + private PropertiesHolder getMergedProperties(Locale locale) { + synchronized (this.cachedMergedProperties) { + PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale); + if (mergedHolder != null) { + return mergedHolder; + } + Properties mergedProps = new Properties(); + mergedHolder = new PropertiesHolder(mergedProps, -1); + for (int i = this.basenames.length - 1; i >= 0; i--) { + List filenames = calculateAllFilenames(this.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()); + } + } + } + this.cachedMergedProperties.put(locale, mergedHolder); + return mergedHolder; + } + } + + /** + * Calculate all filenames for the given bundle basename and Locale. + * Will calculate filenames for the given Locale, the system Locale + * (if applicable), and the default file. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + * @see #setFallbackToSystemLocale + * @see #calculateFilenamesForLocale + */ + private List calculateAllFilenames(String basename, Locale locale) { + synchronized (this.cachedFilenames) { + Map> localeMap = this.cachedFilenames.get(basename); + if (localeMap != null) { + List filenames = localeMap.get(locale); + if (filenames != null) { + return filenames; + } + } + List filenames = new ArrayList(7); + filenames.addAll(calculateFilenamesForLocale(basename, locale)); + if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) { + List fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault()); + for (String fallbackFilename : fallbackFilenames) { + if (!filenames.contains(fallbackFilename)) { + // Entry for fallback locale that isn't already in filenames list. + filenames.add(fallbackFilename); + } + } + } + filenames.add(basename); + if (localeMap != null) { + localeMap.put(locale, filenames); + } + else { + localeMap = new HashMap>(); + localeMap.put(locale, filenames); + this.cachedFilenames.put(basename, localeMap); + } + return filenames; + } + } + + /** + * Calculate the filenames for the given bundle basename and Locale, + * appending language code, country code, and variant code. + * E.g.: basename "expressions", Locale "de_AT_oo" -> "expressions_de_AT_OO", + * "expressions_de_AT", "expressions_de". + *

Follows the rules defined by {@link java.util.Locale#toString()}. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + */ + private List calculateFilenamesForLocale(String basename, Locale locale) { + List result = new ArrayList(3); + String language = locale.getLanguage(); + String country = locale.getCountry(); + String variant = locale.getVariant(); + StringBuilder temp = new StringBuilder(basename); + + temp.append('_'); + if (language.length() > 0) { + temp.append(language); + result.add(0, temp.toString()); + } + + temp.append('_'); + if (country.length() > 0) { + temp.append(country); + result.add(0, temp.toString()); + } + + if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) { + temp.append('_').append(variant); + result.add(0, temp.toString()); + } + + return result; + } + + + /** + * Get a PropertiesHolder for the given filename, either from the + * cache or freshly loaded. + * @param filename the bundle filename (basename + Locale) + * @return the current PropertiesHolder for the bundle + */ + private PropertiesHolder getProperties(String filename) { + synchronized (this.cachedProperties) { + PropertiesHolder propHolder = this.cachedProperties.get(filename); + if (propHolder != null && + (propHolder.getRefreshTimestamp() < 0 || + propHolder.getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) { + return propHolder; + } + return refreshProperties(filename, propHolder); + } + } + + /** + * Refresh the PropertiesHolder for the given bundle filename. + * The holder can be null if not cached before, or a timed-out cache entry + * (potentially getting re-validated against the current last-modified timestamp). + * @param filename the bundle filename (basename + Locale) + * @param propHolder the current PropertiesHolder for the bundle + */ + private PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) { + long refreshTimestamp = (this.cacheMillis < 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 (this.cacheMillis >= 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 is 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; + } + + /** + * Load the properties from the given resource. + * @param resource the resource to load from + * @param filename the original bundle filename (basename + Locale) + * @return the populated Properties instance + * @throws IOException if properties loading failed + */ + private Properties loadProperties(Resource resource, String filename) throws IOException { + InputStream is = resource.getInputStream(); + Properties props = new Properties(); + try { + if (resource.getFilename().endsWith(XML_SUFFIX)) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.loadFromXml(props, is); + } + else { + String encoding = null; + if (this.fileEncodings != null) { + encoding = this.fileEncodings.getProperty(filename); + } + if (encoding == null) { + encoding = this.defaultEncoding; + } + if (encoding != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); + } + this.propertiesPersister.load(props, new InputStreamReader(is, encoding)); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.load(props, is); + } + } + return props; + } + finally { + is.close(); + } + } + + + /** + * Clear the resource bundle cache. + * Subsequent resolve calls will lead to reloading of the properties files. + */ + public void clearCache() { + logger.debug("Clearing entire resource bundle cache"); + synchronized (this.cachedProperties) { + this.cachedProperties.clear(); + } + synchronized (this.cachedMergedProperties) { + this.cachedMergedProperties.clear(); + } + } + + @Override + public String toString() { + return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; + } + + + /** + * PropertiesHolder for caching. + * Stores the last-modified timestamp of the source file for efficient + * change detection, and the timestamp of the last refresh attempt + * (updated every time the cache entry gets re-validated). + */ + private class PropertiesHolder { + + private Properties properties; + + private long fileTimestamp = -1; + + private long refreshTimestamp = -1; + + + public PropertiesHolder(Properties properties, long fileTimestamp) { + this.properties = properties; + this.fileTimestamp = fileTimestamp; + } + + public PropertiesHolder() { + } + + public Properties getProperties() { + return properties; + } + + public long getFileTimestamp() { + return fileTimestamp; + } + + public void setRefreshTimestamp(long refreshTimestamp) { + this.refreshTimestamp = refreshTimestamp; + } + + public long getRefreshTimestamp() { + return refreshTimestamp; + } + + public String getProperty(String code) { + if (this.properties == null) { + return null; + } + return this.properties.getProperty(code); + } + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/DynamicExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/expression/DynamicExpressionTests.java new file mode 100644 index 0000000000..a99535905d --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/DynamicExpressionTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2010 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 org.springframework.integration.expression; + +import static org.junit.Assert.assertEquals; + +import java.io.FileOutputStream; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; + +/** + * @author Mark Fisher + * @since 2.0 + */ +public class DynamicExpressionTests { + + private static final String key = "test.greeting"; + + private static final String basename = "org/springframework/integration/expression/expressions"; + + private static final String filepath = basename + ".properties"; + + + @After + public void resetFile() { + writeExpressionStringToFile("'Hello World!'"); + } + + + @Test + public void expressionUpdate() throws Exception { + ReloadableResourceBundleExpressionSource source = new ReloadableResourceBundleExpressionSource(); + source.setBasename(basename); + source.setCacheSeconds(0); + DynamicExpression expression = new DynamicExpression(key, source); + assertEquals("Hello World!", expression.getValue()); + writeExpressionStringToFile("toUpperCase()"); + assertEquals("FOO", expression.getValue("foo")); + } + + + private static void writeExpressionStringToFile(String expressionString) { + ClassPathResource resource = new ClassPathResource(filepath); + byte[] bytes = new String(key + "=" + expressionString).getBytes(); + try { + new FileOutputStream(resource.getFile()).write(bytes); + } + catch (Exception e) { + throw new IllegalStateException("failed to write expression string to file", e); + } + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/expressions.properties b/spring-integration-core/src/test/java/org/springframework/integration/expression/expressions.properties new file mode 100644 index 0000000000..584df3a98a --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/expressions.properties @@ -0,0 +1 @@ +test.greeting='Hello World!' \ No newline at end of file