/* * Copyright 2008 biaoping.yin * * 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.frameworkset.spi.support; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import org.frameworkset.spi.BaseApplicationContext; import org.frameworkset.spi.ResourceLoaderAware; import org.frameworkset.util.Assert; import org.frameworkset.util.PropertiesPersister; import org.frameworkset.util.io.DefaultResourceLoader; import org.frameworkset.util.io.Resource; import org.frameworkset.util.io.ResourceEditor; import org.frameworkset.util.io.ResourceLoader; import com.frameworkset.util.DaemonThread; import com.frameworkset.util.DefaultPropertiesPersister; import com.frameworkset.util.ResourceInitial; import com.frameworkset.util.SimpleStringUtil; /** * <p> * Title: HotDeployResourceBundleMessageSource.java * </p> * <p> * Description: * </p> * <p> * bboss workgroup * </p> * <p> * Copyright (c) 2007 * </p> * * @Date 2012-5-6 下午8:47:20 * @author biaoping.yin * @version 1.0 */ public class HotDeployResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware { private static final String PROPERTIES_SUFFIX = ".properties"; private static final String XML_SUFFIX = ".xml"; private String[] basenames = new String[0]; private String defaultEncoding; private Properties fileEncodings; private boolean fallbackToSystemLocale = true; private ClassLoader bundleClassLoader; /** * 是否需要对HotDeployResourceBundleMessageSource实例管理的资源文件启用热加载机制 * true 启用 * false 关闭 * 默认启用 */ private boolean changemonitor = true; private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); private ResourceLoader resourceLoader = new HotResourceLoader(); /** 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 basename */ private final Map cachedMergedProperties = new HashMap(); private static final PropertiesHolder NOTEXIST_propHolder = new PropertiesHolder(); private static final PropertiesHolder ERROR_propHolder = new PropertiesHolder(); /** * Set a single basename, following the basic ResourceBundle convention of * not specifying file extension or language codes, but in contrast to * {@link ResourceBundleMessageSource} referring to a Bboss resource * location: e.g. "WEB-INF/messages" for "WEB-INF/messages.properties", * "WEB-INF/messages_en.properties", etc. * <p> * As of Bboss 1.2.2, XML properties files are also supported: e.g. * "WEB-INF/messages" will find and load "WEB-INF/messages.xml", * "WEB-INF/messages_en.xml", etc as well. Note that this will only work on * JDK 1.5+. * * @param basename * the single basename * @see #setBasenames * @see ResourceEditor * @see java.util.ResourceBundle */ public void setBasename(String basename) { String[] basenames = basename.split(","); setBasenames(basenames); } /** * Set an array of basenames, each following the basic ResourceBundle * convention of not specifying file extension or language codes, but in * contrast to {@link ResourceBundleMessageSource} referring to a Bboss * resource location: e.g. "WEB-INF/messages" for * "WEB-INF/messages.properties", "WEB-INF/messages_en.properties", etc. * <p> * As of Bboss 1.2.2, XML properties files are also supported: e.g. * "WEB-INF/messages" will find and load "WEB-INF/messages.xml", * "WEB-INF/messages_en.xml", etc as well. Note that this will only work on * JDK 1.5+. * <p> * The associated resource bundles will be checked sequentially when * resolving a message code. Note that message definitions in a * <i>previous</i> 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. * <p> * Default is none, using the <code>java.util.Properties</code> default * encoding. * <p> * Only applies to classic properties files, not to XML files. * * @param defaultEncoding * the default charset * @see #setFileEncodings */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** * Set per-file charsets to use for parsing properties files. * <p> * 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. "WEB-INF/messages" or * "WEB-INF/messages_en". * @see #setBasenames */ 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. "messages.properties" for * basename "messages"). * <p> * Falling back to the system Locale is the default behavior of * <code>java.util.ResourceBundle</code>. 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 PropertiesPersister to use for parsing properties files. * <p> * The default is a DefaultPropertiesPersister. */ public void setPropertiesPersister(PropertiesPersister propertiesPersister) { this.propertiesPersister = (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); } /** * Set the ResourceLoader to use for loading bundle properties files. * <p> * 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 DefaultResourceLoader * @see ResourceLoaderAware */ public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); } /** * Resolves the given message code as key in the retrieved bundle files, * returning the value found in the bundle as-is (without MessageFormat * parsing). */ protected String resolveCodeWithoutArguments(String code, Locale locale) { if (this.refresh_interval <= 0) { PropertiesHolder propHolder = getMergedProperties(locale); String result = propHolder.getProperty(code); if (result != null) { return result; } } else { for (int i = 0; i < this.basenames.length; i++) { List filenames = calculateAllFilenames(this.basenames[i], locale); for (int j = 0; j < filenames.size(); j++) { String filename = (String) filenames.get(j); PropertiesHolder propHolder = getProperties(filename); String result = propHolder.getProperty(code); if (result != null) { return result; } } } } return null; } /** * Resolves the given message code as key in the retrieved bundle files, * using a cached MessageFormat instance per message code. */ protected MessageFormat resolveCode(String code, Locale locale) { if (refresh_interval < 0) { PropertiesHolder propHolder = getMergedProperties(locale); MessageFormat result = propHolder.getMessageFormat(code, locale); if (result != null) { return result; } } else { for (int i = 0; i < this.basenames.length; i++) { List filenames = calculateAllFilenames(this.basenames[i], locale); for (int j = 0; j < filenames.size(); j++) { String filename = (String) filenames.get(j); PropertiesHolder propHolder = getProperties(filename); MessageFormat result = propHolder.getMessageFormat(code, locale); 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. * <p> * Only used when caching resource bundle contents forever, i.e. with * cacheSeconds < 0. Therefore, merged properties are always cached forever. */ protected PropertiesHolder getMergedProperties(Locale locale) { PropertiesHolder mergedHolder = (PropertiesHolder) this.cachedMergedProperties .get(locale); if (mergedHolder != null) { return mergedHolder; } synchronized (this.cachedMergedProperties) { mergedHolder = (PropertiesHolder) this.cachedMergedProperties .get(locale); if (mergedHolder != null) { return mergedHolder; } Properties mergedProps = new Properties(); mergedHolder = new PropertiesHolder(mergedProps); 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 = (String) 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 */ protected List calculateAllFilenames(String basename, Locale locale) { Map localeMap = (Map) this.cachedFilenames.get(basename); if (localeMap != null) { List filenames = (List) localeMap.get(locale); if (filenames != null) { return filenames; } } synchronized (this.cachedFilenames) { localeMap = (Map) this.cachedFilenames.get(basename); if (localeMap != null) { List filenames = (List) 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 (Iterator it = fallbackFilenames.iterator(); it.hasNext();) { String fallbackFilename = (String) it.next(); 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; } } /** * 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 */ protected PropertiesHolder getProperties(String filename) { PropertiesHolder propHolder = (PropertiesHolder) this.cachedProperties .get(filename); if(propHolder != null) return propHolder; synchronized (this.cachedProperties) { propHolder = (PropertiesHolder) this.cachedProperties .get(filename); if(propHolder != null) return propHolder; // if (propHolder != null // && (propHolder.getRefreshTimestamp() < 0 || propHolder // .getRefreshTimestamp() > System.currentTimeMillis() // - this.cacheMillis)) { // // up to date // return propHolder; // } propHolder = this.firstLoadProperties(filename); if(propHolder.getResource() != null && this.isChangemonitor()) checkResource(this,propHolder.getResource(),propHolder.getBasename(),propHolder.getRelativefile()); cachedProperties.put(filename, propHolder); return propHolder; } } /** * Refresh the PropertiesHolder for the given bundle filename. The holder * can be <code>null</code> 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 */ // protected 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) { // { // 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; // } /** * relativefile = "org/frameworkset/spi/support/messages_en_US.properties"; * basename = "org/frameworkset/spi/support/messages_en_US"; * filepath = "d:/workspace/org/frameworkset/spi/support/messages_en_US.properties"; * @param filename * @param basename * @param relativefile * @return */ protected PropertiesHolder refreshProperties(File filepath,String basename,String relativefile) { // filename = "org/frameworkset/spi/support/messages_en_US.properties"; Resource resource = this.resourceLoader.getResource(relativefile); // if (!resource.exists()) { // resource = this.resourceLoader.getResource(filename + XML_SUFFIX); // } PropertiesHolder propHolder = null; try { Properties props = loadProperties(resource, relativefile); propHolder = new PropertiesHolder(props,filepath,basename,relativefile); } catch (IOException ex) { { logger.warn( "Could not parse properties file [" + filepath + "]", ex); } // Empty holder representing "not valid". propHolder = ERROR_propHolder; } this.cachedProperties.put(basename, propHolder); return propHolder; } protected PropertiesHolder firstLoadProperties(String filename) { String name = filename + PROPERTIES_SUFFIX; Resource resource = this.resourceLoader.getResource(name); boolean reset = false; if (!resource.exists()) { name = filename + XML_SUFFIX; resource = this.resourceLoader.getResource(name); reset = true; } PropertiesHolder propHolder = null; if (reset && !resource.exists()) { propHolder = NOTEXIST_propHolder; } else { try { Properties props = loadProperties(resource, name); File f = null; try { f = resource.getFile(); } catch(Throwable e) { logger.warn( new StringBuffer().append("Get properties file from ").append( resource.getClass().getCanonicalName() ).append( " failed:"+e.getMessage()).toString()); } propHolder = new PropertiesHolder(props,f,filename,name); } catch (IOException ex) { { try { logger.warn( new StringBuffer().append("Could not parse properties file [").append( resource.getFile().getCanonicalPath() ).append( "]").toString(), ex); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // Empty holder representing "not valid". propHolder = ERROR_propHolder; } } // 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 */ protected 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(); } } /** * Clear the resource bundle caches of this MessageSource and all its * ancestors. * * @see #clearCache */ public void clearCacheIncludingAncestors() { clearCache(); if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) { ((ReloadableResourceBundleMessageSource) getParentMessageSource()) .clearCacheIncludingAncestors(); } } public String toString() { return getClass().getName() + ": basenames=[" + SimpleStringUtil.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). */ protected static class PropertiesHolder { private Properties properties; private File resource; private String basename; private String relativefile; // private long fileTimestamp = -1; // // private long refreshTimestamp = -1; /** Cache to hold already generated MessageFormats per message code */ private final Map cachedMessageFormats = new HashMap(); public PropertiesHolder(Properties properties,File resource,String basename,String relativefile) { this.properties = properties; this.resource = resource; this.basename = basename; this.relativefile = relativefile; // this.fileTimestamp = fileTimestamp; } public PropertiesHolder(Properties properties) { this.properties = properties; // this.resource = resource; // 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); } public MessageFormat getMessageFormat(String code, Locale locale) { if (this.properties == null) { return null; } synchronized (this.cachedMessageFormats) { Map localeMap = (Map) this.cachedMessageFormats.get(code); if (localeMap != null) { MessageFormat result = (MessageFormat) localeMap .get(locale); if (result != null) { return result; } } String msg = this.properties.getProperty(code); if (msg != null) { if (localeMap == null) { localeMap = new HashMap(); this.cachedMessageFormats.put(code, localeMap); } MessageFormat result = createMessageFormat(msg, locale); localeMap.put(locale, result); return result; } return null; } } public File getResource() { return resource; } public String getBasename() { return basename; } public String getRelativefile() { return relativefile; } } /** * Calculate the filenames for the given bundle basename and Locale, * appending language code, country code, and variant code. E.g.: basename * "messages", Locale "de_AT_oo" -> "messages_de_AT_OO", "messages_de_AT", * "messages_de". * * @param basename * the basename of the bundle * @param locale * the locale * @return the List of filenames to check */ protected List calculateFilenamesForLocale(String basename, Locale locale) { List result = new ArrayList(3); String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); StringBuffer temp = new StringBuffer(basename); if (language.length() > 0) { temp.append('_').append(language); result.add(0, temp.toString()); } if (country.length() > 0) { temp.append('_').append(country); result.add(0, temp.toString()); } if (variant.length() > 0) { temp.append('_').append(variant); result.add(0, temp.toString()); } return result; } /** * 属性文件变更检测时间间隔,单位为毫秒,默认为5秒间隔 * 在配置属性文件时全局指定 */ private static long refresh_interval = 5000; private static DaemonThread damon = null; private static Object lock = new Object(); private static void checkResource(HotDeployResourceBundleMessageSource messagesource,File file,String basename,String filename){ refresh_interval = BaseApplicationContext.getResourceFileRefreshInterval(); if(refresh_interval > 0) { if(damon == null) { synchronized(lock) { if(damon == null) { damon = new DaemonThread(refresh_interval,"Message files Refresh Worker"); damon.start(); } } } damon.addFile(file, new ResourceFileRefresh(messagesource,file,basename,filename)); } } static class ResourceFileRefresh implements ResourceInitial { private HotDeployResourceBundleMessageSource messagesource ; private String filename; private String basename; private File file; public ResourceFileRefresh(HotDeployResourceBundleMessageSource messagesource,File file,String basename,String filename) { this.messagesource = messagesource; this.filename = filename; this.basename = basename; this.file = file; } public void reinit() { messagesource.refreshProperties(file,basename,filename); } } public static void stopmonitor() { try { if(damon != null) { damon.stopped(); damon = null; } } catch (Throwable e) { // // TODO Auto-generated catch block // e.printStackTrace(); } } public ClassLoader getBundleClassLoader() { return bundleClassLoader; } public void setBundleClassLoader(ClassLoader bundleClassLoader) { this.bundleClassLoader = bundleClassLoader; } public boolean isChangemonitor() { return changemonitor; } public void setChangemonitor(boolean changemonitor) { this.changemonitor = changemonitor; } /** * 注销国际化资源 */ public void destroy() { this.cachedFilenames.clear(); this.cachedMergedProperties.clear(); this.cachedProperties.clear(); super.destroy(); } }