DO NOT REPLY TO THIS EMAIL, BUT PLEASE POST YOUR BUG RELATED COMMENTS THROUGH THE WEB INTERFACE AVAILABLE AT <http://nagoya.apache.org/bugzilla/show_bug.cgi?id=18289>. ANY REPLY MADE TO THIS MESSAGE WILL NOT BE COLLECTED AND INSERTED IN THE BUG DATABASE.
http://nagoya.apache.org/bugzilla/show_bug.cgi?id=18289 Dynamic Message Resources Summary: Dynamic Message Resources Product: Struts Version: 1.1 RC1 Platform: All OS/Version: Other Status: NEW Severity: Enhancement Priority: Other Component: Utilities AssignedTo: [EMAIL PROTECTED] ReportedBy: [EMAIL PROTECTED] Attached code provides a subclass of MessageResources which dynamically reloads as changes are made. ============================================================================== Consists of 5 files which follow: DynamicResourceBundle.java - A resource bundle which refreshes itself as the underlying file(s) change ResourceLoader.java - A subclass of ClassLoader which loads property bundles ResourceLocation.java - A wrapper which encapsulates the location of a property bundle either in a property file or a jar file BundleMessageResources.java - Subclass of MessageResources which uses an underlying DynamicResourceBundle to handle resource requests. BundleMessageResourcesFactory.java ============================================================================= package org.apache.struts.util; import java.util.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * <p>Title: DynamicResourceBundle</p> * <p>Description: Takes care of monitoring underlying bundle locations for * changes and reloads (by creating a new ResourceLoader) if necessary</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 * @todo we need to move this to a more general package */ public abstract class DynamicResourceBundle { /** * The <code>Log</code> instance for this class. */ protected static final Log log = LogFactory.getLog (DynamicResourceBundle.class); /** * Maximum length of one branch of the resource search path tree. * Used in getBundle. */ protected static final int MAX_BUNDLES_SEARCHED = 3; /** * Cache of locales */ protected static HashMap locales = new HashMap(); /** * How often do we check the underlying resource for changes ? */ protected static long checkInterval = 60000; /** * * @param interval ms between checks */ public static synchronized void setCheckInterval(long interval) { checkInterval = interval; } /** * * @return ms between checks */ public static long getCheckInterval() { return checkInterval; } /** * * @param baseName fully qualified base name of the resource * @param locale locale for messages * @return key */ protected static String getKey(String baseName, Locale locale) { return baseName + ", " + locale.toString(); } public static ResourceBundle getBundle(String baseName, Locale locale) { return getBundle(baseName, locale, null); } /** * * @param baseName * @param locale * @return */ public static ResourceBundle getBundle(String baseName, Locale locale, ResourceLoader loader) { BundleMetaData meta = null; String key = getKey(baseName, locale); List locationList = null; synchronized (locale) { meta = (BundleMetaData) locales.get(key); if (meta == null) { if (loader == null) { loader = new ResourceLoader(); } locationList = calculateBundleLocations(baseName, locale, loader); meta = new BundleMetaData(locationList); locales.put(key, meta); log.info("Caching locations for bundle -> " + key + " | Cache size = " + locales.size()); } if (bundleFilesHaveChanged(meta)) { log.warn("Underlying resources for bundle " + baseName + " , " + locale + " have changed, reloading"); meta.refreshLoader(); } } // Resource bundle just returns a bundle from cache if already // loaded with this loader return ResourceBundle.getBundle(baseName, locale, meta.loader); } /** * Calculate the bundles along the search path from the base bundle to the * bundle specified by baseName and locale. Heavily based on the implementation * in ResourceBundle * @param baseName the base bundle name * @param locale the locale * the search path. * @return List of ResourceLocations for the bundle * */ protected static List calculateBundleLocations(String baseName, Locale locale, ResourceLoader loader) { final ArrayList result = new ArrayList(MAX_BUNDLES_SEARCHED); final String language = locale.getLanguage(); final int languageLength = language.length(); final String country = locale.getCountry(); final int countryLength = country.length(); final String variant = locale.getVariant(); final int variantLength = variant.length(); ResourceLocation location = null; log.info("Calculating bundle resource locations for " + getKey (baseName, locale)); if ((languageLength + countryLength + variantLength) == 0) { //The locale is "", "", "". return result; } final StringBuffer temp = new StringBuffer(baseName.replace('.', '/')); location = loader.locateResource(temp.toString() + ".properties"); if (location != null) { log.info(" --> Adding location " + location); result.add(location); } temp.append('_'); temp.append(language); location = loader.locateResource(temp.toString() + ".properties"); if (location != null) { log.info(" --> Adding location " + location); result.add(location); } if ((countryLength + variantLength) == 0) { return result; } temp.append('_'); temp.append(country); location = loader.locateResource(temp.toString() + ".properties"); if (location != null) { log.info(" --> Adding location " + location); result.add(location); } if (variantLength == 0) { return result; } temp.append('_'); temp.append(variant); location = loader.locateResource(temp.toString() + ".properties"); if (location != null) { log.info(" --> Adding location " + location); result.add(location); } return result; } /** * * @param meta Instance of BundleMetaData * @return true of any resources in the bundle have changed */ protected static boolean bundleFilesHaveChanged(BundleMetaData meta) { // is it time to check even ? log.info("Checking locations for changes, time since last check = " + (System.currentTimeMillis() - meta.lastCheck)); // if it is not time to check then just assume nothing has changed if (meta.isTimeToCheck() == false) { return false; } // check all the locations for changes Iterator locations = meta.locationList.iterator(); while (locations.hasNext()) { ResourceLocation location = (ResourceLocation) locations.next(); log.info("Checking location -> " + location); if (location.hasChanged()) { return true; } } // if we fall through then nothing has changed return false; } /** * * <p>Title: BundleMetaData</p> * <p>Description: Just a way to keep related info about a resource bundle * encapsulated in one place</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 */ protected static class BundleMetaData { /** * when was this bundle last checked for changes ? */ long lastCheck = 0; /** * the loader used to load resource */ ClassLoader loader = new ResourceLoader(); /** * the list of locations that comprise this bundle */ List locationList = null; /** * * @param locationList the list of resources that comprise this bundle */ BundleMetaData(List locationList) { this.locationList = locationList; } /** * create a new loader for the resources so we can dynamically * load changes */ void refreshLoader() { this.loader = new ResourceLoader(); } /** * * @return flag indicating whether it is time to check the resource for * changes */ boolean isTimeToCheck() { long currentTime = System.currentTimeMillis(); long interval = currentTime - lastCheck; if (interval > checkInterval) { this.lastCheck = currentTime; return true; } return false; } } } ================================================================================ package org.apache.struts.util; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * <p>Title: ResourceLoader </p> * <p>Description: Checks resources to see if timestamp has been modified, * if so then reloads the resource. Only works for property resources NOT * class based resources</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 */ public class ResourceLoader extends ClassLoader { /** * The <code>Log</code> instance for this class. */ protected static final Log log = LogFactory.getLog(ResourceLoader.class); /** * No args constructor */ public ResourceLoader() { } /** * * @param name Fully qualified name of the resource to locate * @return InputStream to the resource */ public InputStream getResourceAsStream(String name) { ResourceLocation resource = this.locateResource(name); if (resource != null) { log.info("Located resource returning stream -> " + name); // return the input stream if resource located return resource.getInputStream(); } else { //log.info("Can't locate resource -> " + name); return null; } } /** * subclasses may want to override to provide different strategies * for locating resource. Here we use the ClassLoader and we always * check for new resources so we can add resources on the fly * @param name the name of the resource * @return URL of resource */ public static ResourceLocation locateResource(String name) { ResourceLocation resource = null; ClassLoader classLoader = Thread.currentThread().getContextClassLoader (); if (classLoader == null) { classLoader = ResourceLoader.class.getClassLoader(); } URL url = classLoader.getResource(name); if (log.isTraceEnabled()) { log.debug("URL of resource: " + url); } // attempt to access the resource through the URL if (url != null) { try { resource = new ResourceLocation(url); } catch (MalformedURLException e) { log.error("Bad URL for resource -> " + resource); } } return resource; } } =============================================================================== package org.apache.struts.util; import java.io.*; import java.net.*; import java.util.*; import java.util.jar.*; import org.apache.commons.lang.StringUtils; /** * <p>Title: ResourceLocation</p> * <p>Description: Wrapper around resources located either in jars * or as files in the CLASSPATH. * Access to it needs to externally synchronized</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 * @todo we need to move this to a more general utility class */ public class ResourceLocation { /** * Url of the resource */ protected URL url = null; /** * Fully qualified filename of the underlying resource */ protected String filename = null; /** * If this is a jar resource, this is the fully qualified name of the * resource in the jar */ protected String entryname = null; /** * The last time this resource was modified */ protected long lastmod = 0; /** * * @param url The URL which points to the resource * @throws MalformedURLException */ public ResourceLocation(URL url) throws MalformedURLException { if (url == null) { throw new MalformedURLException(); } // fill in state this.url = url; if (this.isJarResource(url)) { // extract the filename, entry name URL suburl = new URL(url.getPath()); filename = suburl.getPath(); // strip filename off entryname = StringUtils.prechomp(filename, "!"); // chomp entry off filename = StringUtils.chomp(filename, "!"); // strip off initial / if present on entry name // else JarFile can't locate the entry if (entryname.charAt(0) == '/') { entryname = StringUtils.substring(entryname, 1); } } else { filename = url.getPath(); } } /** * Static convenience method for determining if a resource is a jar file * @param url The URL of the resource * @return true if URL points to a jar file */ public static boolean isJarResource(URL url) { String proto = url.getProtocol(); if (proto.equalsIgnoreCase("jar")) { return true; } else { return false; } } /** * Access the entry name * @return If resource is in a jar, this is the entry of the resource * within the jar. Else null. */ public String getEntryName() { return this.entryname; } /** * Access the filename * @return The underlying filename of the resource */ public String getFilename() { return this.filename; } /** * Access the URL * @return The URL of the resource */ public URL getURL() { return this.url; } /** * Does this resource exist ? * Note that we check this each time so that resources can be dynamically * added, deleted * @return true if resource exists on file system */ public boolean exists() { // construct a file object with path info File file = new File(filename); return file.exists(); } /** * Has this resource changed since the last time this method was called ? * Synchronized cause we change the underlying object state * @return true if changed */ public synchronized boolean hasChanged() { if (this.exists() == false) { return false; } long mod = this.lastModified(); if (mod > this.lastmod) { this.lastmod = mod; return true; } else { return false; } } /** * When was this resource last modified ? * @return last modification time of underlying file * or 0 if the resource does not exist */ public long lastModified() { // construct a file object with path info File file = new File(filename); if (file.exists()) { return file.lastModified(); } else { return 0; } } /** * * @return String repr of object */ public String toString() { StringBuffer repr = new StringBuffer(); repr.append("Resource URL : " + this.url.toString() + "\n"); repr.append("Filename : " + this.filename + "\n"); if (this.entryname != null) { repr.append("Entryname : " + this.entryname + "\n"); } repr.append("Exists ? : " + this.exists()); return repr.toString(); } /** * * @return Stream to the resource */ public InputStream getInputStream() { InputStream is = null; // bail out if the resource is unavailable if (this.exists() == false) { return is; } try { // open stream to an entry in a jar file if (this.isJarResource(this.url)) { JarFile jarFile = new JarFile(filename); JarEntry entry = jarFile.getJarEntry(entryname); if (entry != null) { is = jarFile.getInputStream(entry); } // open stream to a file } else { is = new FileInputStream(filename); } } catch (IOException e) { ; } finally { if (is == null) { // we always want to return something rather than a NP // in this case just return an empty stream byte[] buf = new byte[] { }; is = new ByteArrayInputStream(buf); } } return is; } } ================================================================================ package org.apache.struts.util; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * <p>Title: BundleMessageResources</p> * <p>Description: Subclass of Struts MessageResources which uses a dynamically * reloadable resource bundle to locate messages</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 */ public class BundleMessageResources extends MessageResources { /** * * @param factory The factory which created this resource * @param config String which holds the fully qualified base resource name */ public BundleMessageResources(MessageResourcesFactory factory, String config) { super(factory, config); } /** * * @param factory The factory which created this resource * @param config String which holds the fully qualified base resource name * @param returnNull Flag to return null if can't locate message */ public BundleMessageResources(MessageResourcesFactory factory, String config, boolean returnNull) { super(factory, config, returnNull); } /** * * @param locale Message locale * @param key Message key * @return The locale specific message */ public String getMessage(Locale locale, String key) { ResourceBundle bundle = DynamicResourceBundle.getBundle(config, locale); if (bundle != null) { return bundle.getString(key); } else { log.warn("Can't locate bundle for locale,key -> " + locale + "," + key); if (returnNull) { return null; } else { return "??? " + key + " ???"; } } } } =============================================================================== package org.apache.struts.util; /** * <p>Title: BundleMessageResourcesFactory</p> * <p>Description: Called by the Struts framework to create BundleMessageResources</p> * <p>Copyright: None</p> * <p>Company: CoLinx</p> * @author Richard Lawson * @version 1.0 */ public class BundleMessageResourcesFactory extends MessageResourcesFactory { /** * * @param config Fully qualified base name for the resource * @return BundleMessageResources */ public MessageResources createResources(String config) { return new BundleMessageResources(this, config, this.returnNull); } } --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]