1. /*
  2. * Copyright 2002-2004 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.springframework.context.support;
  17. import java.io.IOException;
  18. import java.io.InputStream;
  19. import java.io.InputStreamReader;
  20. import java.text.MessageFormat;
  21. import java.util.ArrayList;
  22. import java.util.HashMap;
  23. import java.util.List;
  24. import java.util.Locale;
  25. import java.util.Map;
  26. import java.util.Properties;
  27. import org.springframework.context.ResourceLoaderAware;
  28. import org.springframework.core.io.DefaultResourceLoader;
  29. import org.springframework.core.io.Resource;
  30. import org.springframework.core.io.ResourceLoader;
  31. import org.springframework.util.DefaultPropertiesPersister;
  32. import org.springframework.util.PropertiesPersister;
  33. import org.springframework.util.StringUtils;
  34. /**
  35. * MessageSource that accesses the ResourceBundles with the specified basenames.
  36. * This class uses java.util.Properties instances as its internal data structure
  37. * for messages, loading them via a PropertiesPersister strategy: The default
  38. * strategy can load properties files with a specific charset.
  39. *
  40. * <p>In contrast to ResourceBundleMessageSource, this class supports reloading
  41. * of properties files through the "cacheSeconds" setting, and also through
  42. * programmatically clearing the properties cache. Since application servers do
  43. * typically cache all files loaded from the classpath, it is necessary to store
  44. * resources somewhere else (for example, in the "WEB-INF" directory of a web app).
  45. * Otherwise changes of files in the classpath are not reflected in the application.
  46. *
  47. * <p>Note that the "basename" respectively "basenames" property has a different
  48. * convention here: It follows the basic ResourceBundle rule of not specifying
  49. * file extension or language codes, but can refer to any Spring resource location
  50. * (instead of being restricted to classpath resources). With a "classpath:" prefix,
  51. * resources can still be loaded from the classpath, but "cacheSeconds" values
  52. * other than "-1" (caching forever) will not work in this case.
  53. *
  54. * <p>This MessageSource can easily be used outside an ApplicationContext: It uses
  55. * a DefaultResourceLoader as default, getting overridden with the ApplicationContext
  56. * if running in a context. It does not have any other specific dependencies.
  57. *
  58. * @author Thomas Achleitner
  59. * @author Juergen Hoeller
  60. * @see #setCacheSeconds
  61. * @see #setBasenames
  62. * @see #setDefaultEncoding
  63. * @see #setFileEncodings
  64. * @see #setPropertiesPersister
  65. * @see #setResourceLoader
  66. * @see org.springframework.util.DefaultPropertiesPersister
  67. * @see org.springframework.core.io.DefaultResourceLoader
  68. * @see ResourceBundleMessageSource
  69. * @see java.util.ResourceBundle
  70. */
  71. public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
  72. implements ResourceLoaderAware {
  73. public static final String PROPERTIES_SUFFIX = ".properties";
  74. private String[] basenames;
  75. private String defaultEncoding;
  76. private Properties fileEncodings;
  77. private boolean fallbackToSystemLocale = true;
  78. private long cacheMillis = -1;
  79. /** Cache to hold filename lists per Locale */
  80. private final Map cachedFilenames = new HashMap();
  81. /** Cache to hold already loaded properties per filename */
  82. private final Map cachedProperties = new HashMap();
  83. private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
  84. private ResourceLoader resourceLoader = new DefaultResourceLoader();
  85. /**
  86. * Set a single basename, following the basic ResourceBundle convention of
  87. * not specifying file extension or language codes, but in contrast to
  88. * ResourceBundleMessageSource referring to a Spring resource location:
  89. * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties",
  90. * "WEB-INF/messages_en.properties", etc.
  91. * @param basename the single basename
  92. * @see #setBasenames
  93. * @see org.springframework.core.io.ResourceEditor
  94. * @see java.util.ResourceBundle
  95. */
  96. public void setBasename(String basename) {
  97. setBasenames(new String[]{basename});
  98. }
  99. /**
  100. * Set an array of basenames, each following the above-mentioned special
  101. * convention. The associated resource bundles will be checked sequentially
  102. * when resolving a message code.
  103. * <p>Note that message definitions in a <i>previous</i> resource bundle
  104. * will override ones in a later bundle, due to the sequential lookup.
  105. * @param basenames an array of basenames
  106. * @see #setBasename
  107. * @see java.util.ResourceBundle
  108. */
  109. public void setBasenames(String[] basenames) {
  110. this.basenames = basenames;
  111. }
  112. /**
  113. * Set the default charset to use for parsing properties files.
  114. * Used if no file-specific charset is specified for a file.
  115. * <p>Default is none, using java.util.Properties' default charset.
  116. * @see #setFileEncodings
  117. * @see org.springframework.util.PropertiesPersister#load
  118. */
  119. public void setDefaultEncoding(String defaultEncoding) {
  120. this.defaultEncoding = defaultEncoding;
  121. }
  122. /**
  123. * Set per-file charsets to use for parsing properties files.
  124. * @param fileEncodings Properties with filenames as keys and charset
  125. * names as values. Filenames have to match the basename syntax,
  126. * with optional locale-specific appendices: e.g. "WEB-INF/messages"
  127. * or "WEB-INF/messages_en".
  128. * @see #setBasenames
  129. * @see org.springframework.util.PropertiesPersister#load
  130. */
  131. public void setFileEncodings(Properties fileEncodings) {
  132. this.fileEncodings = fileEncodings;
  133. }
  134. /**
  135. * Set whether to fall back to the system Locale if no files for a specific
  136. * Locale have been found. Default is true; if this is turned off, the only
  137. * fallback will be the default file (e.g. "messages.properties" for
  138. * basename "messages").
  139. * <p>Falling back to the system Locale is the default behavior of
  140. * java.util.ResourceBundle. However, this is often not desirable in an
  141. * application server environment, where the system Locale is not relevant
  142. * to the application at all: Set this flag to "false" in such a scenario.
  143. */
  144. public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
  145. this.fallbackToSystemLocale = fallbackToSystemLocale;
  146. }
  147. /**
  148. * Set the number of seconds to cache loaded properties files.
  149. * <ul>
  150. * <li>Default is "-1", indicating to cache forever (just like
  151. * java.util.ResourceBundle).
  152. * <li>A positive number will cache loaded properties files for the given
  153. * number of seconds. This is essentially the interval between refresh attempts.
  154. * Note that a refresh attempt will first check the last-modified timestamp
  155. * of the file before actually reloading it; so if files don't change, this
  156. * interval can be set rather low, as refresh attempts will not actually reload.
  157. * <li>A value of "0" will check the last-modified timestamp of the file on
  158. * every message access. <b>Do not use this in a production environment!</b>
  159. * </ul>
  160. */
  161. public void setCacheSeconds(int cacheSeconds) {
  162. this.cacheMillis = cacheSeconds * 1000;
  163. }
  164. /**
  165. * Set the PropertiesPersister to use for parsing properties files.
  166. * The default is DefaultPropertiesPersister.
  167. * @see org.springframework.util.DefaultPropertiesPersister
  168. */
  169. public void setPropertiesPersister(PropertiesPersister propertiesPersister) {
  170. this.propertiesPersister = propertiesPersister;
  171. }
  172. /**
  173. * Set the ResourceLoader to use for loading bundle properties files.
  174. * The default is DefaultResourceLoader. Will get overridden by the
  175. * ApplicationContext if running in a context.
  176. * @see org.springframework.core.io.DefaultResourceLoader
  177. */
  178. public void setResourceLoader(ResourceLoader resourceLoader) {
  179. this.resourceLoader = resourceLoader;
  180. }
  181. protected MessageFormat resolveCode(String code, Locale locale) {
  182. for (int i = 0; i < this.basenames.length; i++) {
  183. List filenames = calculateAllFilenames(this.basenames[i], locale);
  184. for (int j = 0; j < filenames.size(); j++) {
  185. String filename = (String) filenames.get(j);
  186. PropertiesHolder propHolder = getProperties(filename);
  187. if (propHolder.getProperties() != null) {
  188. MessageFormat result = propHolder.getMessageFormat(code, locale);
  189. if (result != null) {
  190. return result;
  191. }
  192. }
  193. }
  194. }
  195. return null;
  196. }
  197. /**
  198. * Calculate all filenames for the given bundle basename and Locale.
  199. * Will calculate filenames for the given Locale, the system Locale
  200. * (if applicable), and the default file.
  201. * @param basename the basename of the bundle
  202. * @param locale the locale
  203. * @return the List of filenames to check
  204. * @see #setFallbackToSystemLocale
  205. * @see #calculateFilenamesForLocale
  206. */
  207. protected List calculateAllFilenames(String basename, Locale locale) {
  208. synchronized (this.cachedFilenames) {
  209. Map localeMap = (Map) this.cachedFilenames.get(basename);
  210. if (localeMap != null) {
  211. List filenames = (List) localeMap.get(locale);
  212. if (filenames != null) {
  213. return filenames;
  214. }
  215. }
  216. List filenames = new ArrayList(7);
  217. filenames.addAll(calculateFilenamesForLocale(basename, locale));
  218. if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
  219. filenames.addAll(calculateFilenamesForLocale(basename, Locale.getDefault()));
  220. }
  221. filenames.add(basename);
  222. if (localeMap != null) {
  223. localeMap.put(locale, filenames);
  224. }
  225. else {
  226. localeMap = new HashMap();
  227. localeMap.put(locale, filenames);
  228. this.cachedFilenames.put(basename, localeMap);
  229. }
  230. return filenames;
  231. }
  232. }
  233. /**
  234. * Calculate the filenames for the given bundle basename and Locale,
  235. * appending language code, country code, and variant code.
  236. * E.g.: basename "messages", Locale "de_AT_oo" -> "messages_de_AT_OO",
  237. * "messages_de_AT", "messages_de".
  238. * @param basename the basename of the bundle
  239. * @param locale the locale
  240. * @return the List of filenames to check
  241. */
  242. protected List calculateFilenamesForLocale(String basename, Locale locale) {
  243. List result = new ArrayList(3);
  244. String language = locale.getLanguage();
  245. String country = locale.getCountry();
  246. String variant = locale.getVariant();
  247. StringBuffer temp = new StringBuffer(basename);
  248. if (language.length() > 0) {
  249. temp.append('_').append(language);
  250. result.add(0, temp.toString());
  251. }
  252. if (country.length() > 0) {
  253. temp.append('_').append(country);
  254. result.add(0, temp.toString());
  255. }
  256. if (variant.length() > 0) {
  257. temp.append('_').append(variant);
  258. result.add(0, temp.toString());
  259. }
  260. return result;
  261. }
  262. /**
  263. * Get PropertiesHolder for the given filename, either from the cache
  264. * or freshly loaded.
  265. */
  266. protected PropertiesHolder getProperties(String filename) {
  267. synchronized (this.cachedProperties) {
  268. PropertiesHolder propHolder = (PropertiesHolder) this.cachedProperties.get(filename);
  269. if (propHolder != null &&
  270. (propHolder.getRefreshTimestamp() < 0 ||
  271. propHolder.getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) {
  272. return propHolder;
  273. }
  274. else {
  275. return refreshProperties(filename, propHolder);
  276. }
  277. }
  278. }
  279. /**
  280. * Refresh the PropertiesHolder for the given bundle filename.
  281. * The holder can be null if not cached before, or a timed-out cache entry
  282. * (potentially getting re-validated against the current last-modified timestamp).
  283. */
  284. protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
  285. long refreshTimestamp = (this.cacheMillis < 0) ? -1 : System.currentTimeMillis();
  286. Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
  287. try {
  288. long fileTimestamp = -1;
  289. if (this.cacheMillis >= 0) {
  290. // last-modified timestamp of file will just be read if caching with timeout
  291. // (allowing to use classpath resources if caching forever)
  292. fileTimestamp = resource.getFile().lastModified();
  293. if (fileTimestamp == 0) {
  294. throw new IOException("File [" + resource.getFile().getAbsolutePath() + "] does not exist");
  295. }
  296. if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
  297. if (logger.isDebugEnabled()) {
  298. logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
  299. }
  300. propHolder.setRefreshTimestamp(refreshTimestamp);
  301. return propHolder;
  302. }
  303. }
  304. InputStream is = resource.getInputStream();
  305. Properties props = new Properties();
  306. try {
  307. String charset = null;
  308. if (this.fileEncodings != null) {
  309. charset = this.fileEncodings.getProperty(filename);
  310. }
  311. if (charset == null) {
  312. charset = this.defaultEncoding;
  313. }
  314. if (charset != null) {
  315. if (logger.isDebugEnabled()) {
  316. logger.debug("Loading properties for filename [" + filename + "] with charset '" + charset + "'");
  317. }
  318. this.propertiesPersister.load(props, new InputStreamReader(is, charset));
  319. }
  320. else {
  321. if (logger.isDebugEnabled()) {
  322. logger.debug("Loading properties for filename [" + filename + "]");
  323. }
  324. this.propertiesPersister.load(props, is);
  325. }
  326. propHolder = new PropertiesHolder(props, fileTimestamp);
  327. }
  328. finally {
  329. is.close();
  330. }
  331. }
  332. catch (IOException ex) {
  333. if (logger.isDebugEnabled()) {
  334. logger.debug("Properties file [" + filename + "] not found for MessageSource: " + ex.getMessage());
  335. }
  336. // empty holder representing "not found"
  337. propHolder = new PropertiesHolder();
  338. }
  339. propHolder.setRefreshTimestamp(refreshTimestamp);
  340. this.cachedProperties.put(filename, propHolder);
  341. return propHolder;
  342. }
  343. /**
  344. * Clear the resource bundle cache.
  345. * Following resolve calls will lead to reloading of the properties files.
  346. */
  347. public void clearCache() {
  348. logger.info("Clearing resource bundle cache");
  349. synchronized (this.cachedProperties) {
  350. this.cachedProperties.clear();
  351. }
  352. }
  353. /**
  354. * Clear the resource bundle caches of this MessageSource and all its ancestors.
  355. * @see #clearCache
  356. */
  357. public void clearCacheIncludingAncestors() {
  358. clearCache();
  359. if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) {
  360. ((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors();
  361. }
  362. }
  363. public String toString() {
  364. return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";
  365. }
  366. /**
  367. * PropertiesHolder for caching.
  368. * Stores the last-modified timestamp of the source file for efficient
  369. * change detection, and the timestamp of the last refresh attempt
  370. * (updated every time the cache entry gets re-validated).
  371. */
  372. protected class PropertiesHolder {
  373. private Properties properties;
  374. private long fileTimestamp = -1;
  375. private long refreshTimestamp = -1;
  376. /** Cache to hold already generated MessageFormats per message code */
  377. private final Map cachedMessageFormats = new HashMap();
  378. protected PropertiesHolder(Properties properties, long fileTimestamp) {
  379. this.properties = properties;
  380. this.fileTimestamp = fileTimestamp;
  381. }
  382. protected PropertiesHolder() {
  383. }
  384. protected Properties getProperties() {
  385. return properties;
  386. }
  387. protected long getFileTimestamp() {
  388. return fileTimestamp;
  389. }
  390. protected void setRefreshTimestamp(long refreshTimestamp) {
  391. this.refreshTimestamp = refreshTimestamp;
  392. }
  393. protected long getRefreshTimestamp() {
  394. return refreshTimestamp;
  395. }
  396. protected MessageFormat getMessageFormat(String code, Locale locale) {
  397. synchronized (this.cachedMessageFormats) {
  398. Map localeMap = (Map) this.cachedMessageFormats.get(code);
  399. if (localeMap != null) {
  400. MessageFormat result = (MessageFormat) localeMap.get(locale);
  401. if (result != null) {
  402. return result;
  403. }
  404. }
  405. String msg = this.properties.getProperty(code);
  406. if (msg != null) {
  407. if (localeMap == null) {
  408. localeMap = new HashMap();
  409. this.cachedMessageFormats.put(code, localeMap);
  410. }
  411. MessageFormat result = createMessageFormat(msg, locale);
  412. localeMap.put(locale, result);
  413. return result;
  414. }
  415. return null;
  416. }
  417. }
  418. }
  419. }