Java tutorial
/* * RefreshingProperties.java * * Created on November 11, 2005, 10:15 PM * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.Properties; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; /** * This is a java.util.Properties class that will check a file or URL for changes * periodically. It has a threaded and non-threaded mode, and will reload a URL * every recheck time, or inspect the last modified date on a file on check. * @author <a href="mailto:cooper@screaming-penguin.com">Robert "kebernet" Cooper</a> * @version $Revision: 1.4 $ */ public class RefreshingProperties extends Properties { /** * DOCUMENT ME! */ private static final Logger LOG = Logger.getLogger(RefreshingProperties.class.getCanonicalName()); /** * DOCUMENT ME! */ private ArrayList<RefreshingProperties.RefreshListener> listeners = new ArrayList<RefreshingProperties.RefreshListener>(); /** * DOCUMENT ME! */ private Thread updater; /** * DOCUMENT ME! */ private URL url; /** * DOCUMENT ME! */ private long lastCheck; /** * DOCUMENT ME! */ private long recheckTime = 5 * 60 * 1000; private boolean loading = false; private ArrayList<RefreshingProperties> augmentProps = new ArrayList<RefreshingProperties>(); private ArrayList<RefreshingProperties> overrideProps = new ArrayList<RefreshingProperties>(); private boolean noImportMode = false; /** * Creates a new RefreshingProperties object. * This constructor will use the default settings of threaded mode and recheck at 5 minutes. * @param url URL to read from. * @throws IOException Thrown on read errors. */ public RefreshingProperties(URL url) throws IOException { init(url, recheckTime, true); } /** * Creates a new RefreshingProperties object. * This will use the default recheck at 5 minutes. * @param url URL to read from * @param useThread Indicates whether the check should run in threaded or non-threaded road. * @throws IOException Thrown on read errors. */ public RefreshingProperties(URL url, boolean useThread) throws IOException { init(url, recheckTime, useThread); } /** * Creates a new RefreshingProperties object. * Uses the default threaded mode. * @param recheckTime number of milliseconds between rechecks * @param url URL to load from * @throws IOException Thrown on read errors. */ public RefreshingProperties(URL url, long recheckTime) throws IOException { init(url, recheckTime, true); } /** * Creates a new RefreshingProperties object. * @param url URL to read from * @param recheckTime recheck time in milliseconds * @param useThread Whether the recheck should be threaded or unthreaded. * @throws IOException Thrown on read errors. */ public RefreshingProperties(URL url, long recheckTime, boolean useThread) throws IOException { init(url, recheckTime, useThread); } /** * Calls the <tt>Hashtable</tt> method <code>put</code>. Provided for * parallelism with the <tt>getProperty</tt> method. Enforces use of * strings for property keys and values. The value returned is the * result of the <tt>Hashtable</tt> call to <code>put</code>. * * @param key the key to be placed into this property list. * @param value the value corresponding to <tt>key</tt>. * @return the previous value of the specified key in this property * list, or <code>null</code> if it did not have one. * @see #getProperty * @since 1.2 */ public Object setProperty(String key, String value) { Object retValue; threadCheck(); retValue = super.setProperty(key, value); return retValue; } /** * Searches for the property with the specified key in this property list. * If the key is not found in this property list, the default property list, * and its defaults, recursively, are then checked. The method returns * <code>null</code> if the property is not found. * * @param key the property key. * @return the value in this property list with the specified key value. * @see #setProperty * @see #defaults */ public String getProperty(String key) { threadCheck(); String retValue; retValue = super.getProperty(key); return retValue; } /** * Searches for the property with the specified key in this property list. * If the key is not found in this property list, the default property list, * and its defaults, recursively, are then checked. The method returns the * default value argument if the property is not found. * * @param key the hashtable key. * @param defaultValue a default value. * * @return the value in this property list with the specified key value. * @see #setProperty * @see #defaults */ public String getProperty(String key, String defaultValue) { String retValue; threadCheck(); retValue = super.getProperty(key, defaultValue); return retValue; } /** * DOCUMENT ME! * * @param listener DOCUMENT ME! */ public void addRefreshListener(RefreshingProperties.RefreshListener listener) { this.listeners.add(listener); } /** * Creates a shallow copy of this hashtable. All the structure of the * hashtable itself is copied, but the keys and values are not cloned. * This is a relatively expensive operation. * * @return a clone of the hashtable. */ public Object clone() { Object retValue; threadCheck(); retValue = super.clone(); return retValue; } /** * Tests if some key maps into the specified value in this hashtable. * This operation is more expensive than the <code>containsKey</code> * method.<p> * * Note that this method is identical in functionality to containsValue, * (which is part of the Map interface in the collections framework). * * @return <code>true</code> if and only if some key maps to the * <code>value</code> argument in this hashtable as * determined by the <tt>equals</tt> method; * <code>false</code> otherwise. * @see #containsKey(Object) * @see #containsValue(Object) * @see Map * @param value a value to search for. */ public boolean contains(Object value) { threadCheck(); boolean retValue; retValue = super.contains(value); return retValue; } /** * Tests if the specified object is a key in this hashtable. * * @return <code>true</code> if and only if the specified object * is a key in this hashtable, as determined by the * <tt>equals</tt> method; <code>false</code> otherwise. * @see #contains(Object) * @param key possible key. */ public boolean containsKey(Object key) { boolean retValue; threadCheck(); retValue = super.containsKey(key); return retValue; } /** * Returns true if this Hashtable maps one or more keys to this value.<p> * * Note that this method is identical in functionality to contains * (which predates the Map interface). * * @return <tt>true</tt> if this map maps one or more keys to the * specified value. * @see Map * @since 1.2 * @param value value whose presence in this Hashtable is to be tested. */ public boolean containsValue(Object value) { boolean retValue; threadCheck(); retValue = super.containsValue(value); return retValue; } /** * Returns an enumeration of the values in this hashtable. * Use the Enumeration methods on the returned object to fetch the elements * sequentially. * * @return an enumeration of the values in this hashtable. * @see java.util.Enumeration * @see #keys() * @see #values() * @see Map */ public java.util.Enumeration<Object> elements() { java.util.Enumeration retValue; threadCheck(); retValue = super.elements(); return retValue; } /** * Returns a Set view of the entries contained in this Hashtable. * Each element in this collection is a Map.Entry. The Set is * backed by the Hashtable, so changes to the Hashtable are reflected in * the Set, and vice-versa. The Set supports element removal * (which removes the corresponding entry from the Hashtable), * but not element addition. * * @return a set view of the mappings contained in this map. * @see Map.Entry * @since 1.2 */ public java.util.Set<java.util.Map.Entry<Object, Object>> entrySet() { java.util.Set retValue; threadCheck(); retValue = super.entrySet(); return retValue; } /** * Returns the value to which the specified key is mapped in this hashtable. * * @return the value to which the key is mapped in this hashtable; * <code>null</code> if the key is not mapped to any value in * this hashtable. * @see #put(Object, Object) * @param key a key in the hashtable. */ public Object get(Object key) { threadCheck(); Object retValue; for (RefreshingProperties over : this.overrideProps) { Object overValue = over.get(key); if (overValue != null) { return overValue; } } retValue = super.get(key); if (retValue == null) { for (RefreshingProperties aug : this.augmentProps) { Object augValue = aug.get(key); if (augValue != null) { retValue = augValue; break; } } } return retValue; } /** * Returns a Set view of the keys contained in this Hashtable. The Set * is backed by the Hashtable, so changes to the Hashtable are reflected * in the Set, and vice-versa. The Set supports element removal * (which removes the corresponding entry from the Hashtable), but not * element addition. * * @return a set view of the keys contained in this map. * @since 1.2 */ public java.util.Set<Object> keySet() { java.util.Set retValue; threadCheck(); retValue = super.keySet(); for (RefreshingProperties props : this.augmentProps) { retValue.addAll(props.keySet()); } for (RefreshingProperties props : this.overrideProps) { retValue.addAll(props.keySet()); } return retValue; } /** * Returns an enumeration of the keys in this hashtable. * * @return an enumeration of the keys in this hashtable. * @see Enumeration * @see #elements() * @see #keySet() * @see Map */ public java.util.Enumeration<Object> keys() { java.util.Enumeration retValue; threadCheck(); retValue = (new Vector(this.keySet())).elements(); return retValue; } /** * Returns an enumeration of all the keys in this property list, * including distinct keys in the default property list if a key * of the same name has not already been found from the main * properties list. * * @return an enumeration of all the keys in this property list, including * the keys in the default property list. * @see java.util.Enumeration * @see java.util.Properties#defaults */ public java.util.Enumeration<Object> propertyNames() { java.util.Enumeration retValue; threadCheck(); retValue = super.propertyNames(); return retValue; } /** * Maps the specified <code>key</code> to the specified * <code>value</code> in this hashtable. Neither the key nor the * value can be <code>null</code>. <p> * * The value can be retrieved by calling the <code>get</code> method * with a key that is equal to the original key. * * @return the previous value of the specified key in this hashtable, * or <code>null</code> if it did not have one. * @see Object#equals(Object) * @see #get(Object) * @param key the hashtable key. * @param value the value. */ public Object put(Object key, Object value) { threadCheck(); if (!this.noImportMode && key instanceof String && ((String) key).startsWith("@import.")) { String keyString = ((String) key); String importType = keyString.substring(8, keyString.lastIndexOf(".")); ImportRefreshListener irl = null; if (importType.equals("override")) { irl = new ImportRefreshListener(this, true); } else if (importType.equals("augment")) { irl = new ImportRefreshListener(this, false); } else { throw new RuntimeException("Import type: " + importType + " unknown."); } try { boolean useThread = (this.updater != null); RefreshingProperties importedProp = new RefreshingProperties(new URL(this.url, (String) value), this.recheckTime, useThread); if (irl.clobber) { this.overrideProps.add(importedProp); } else { this.augmentProps.add(importedProp); } importedProp.addRefreshListener(irl); this.importLoad(importedProp, irl.clobber); } catch (Exception e) { throw new RuntimeException("Exception creaing child properties", e); } } return super.put(key, value); } /** * Copies all of the mappings from the specified Map to this Hashtable * These mappings will replace any mappings that this Hashtable had for any * of the keys currently in the specified Map. * * @since 1.2 * @param t Mappings to be stored in this map. */ public void putAll(java.util.Map t) { threadCheck(); super.putAll(t); } /** * Removes the key (and its corresponding value) from this * hashtable. This method does nothing if the key is not in the hashtable. * * @return the value to which the key had been mapped in this hashtable, * or <code>null</code> if the key did not have a mapping. * @param key the key that needs to be removed. */ public Object remove(Object key) { threadCheck(); Object retValue; retValue = super.remove(key); return retValue; } /** * DOCUMENT ME! * * @param listener DOCUMENT ME! */ public void removeRefreshListener(RefreshingProperties.RefreshListener listener) { this.listeners.remove(listener); } /** * Returns a Collection view of the values contained in this Hashtable. * The Collection is backed by the Hashtable, so changes to the Hashtable * are reflected in the Collection, and vice-versa. The Collection * supports element removal (which removes the corresponding entry from * the Hashtable), but not element addition. * * @return a collection view of the values contained in this map. * @since 1.2 */ public java.util.Collection<Object> values() { java.util.Collection retValue; threadCheck(); ArrayList values = new ArrayList(); for (Object key : this.keySet()) { values.add(this.get(key)); } return values; } /** * DOCUMENT ME! */ private void check() { try { if (this.url.getProtocol().equals("file")) { File f = new File(this.url.getFile()); if (f.lastModified() > this.lastCheck) { this.load(); } } else if (!this.url.getProtocol().equals("file") && System.currentTimeMillis() - this.lastCheck > this.recheckTime) { this.load(); } this.lastCheck = System.currentTimeMillis(); } catch (IOException e) { RefreshingProperties.LOG.log(Level.WARNING, "Exception reloading properies.", e); } } private void importLoad(RefreshingProperties source, boolean clobber) { Enumeration keys = source.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); if (clobber || this.getProperty(key) == null) this.put(key, source.getProperty(key)); } this.fireEvents(); } /** * DOCUMENT ME! * * @param url DOCUMENT ME! * @param recheckTime DOCUMENT ME! * @param useThread DOCUMENT ME! * * @throws IOException DOCUMENT ME! */ private void init(URL url, long recheckTime, boolean useThread) throws IOException { this.url = url; this.recheckTime = recheckTime; if (useThread) { this.updater = new UpdateThread(this); this.updater.start(); } this.check(); } /** * DOCUMENT ME! * * @throws IOException DOCUMENT ME! */ private void load() throws IOException { this.loading = true; InputStream is = null; super.clear(); is = this.url.openStream(); super.load(is); is.close(); RefreshingProperties.LOG.log(Level.FINEST, "Loading of " + this.url + " at " + new Date()); this.fireEvents(); this.loading = false; } private void fireEvents() { RefreshingProperties.ReloadEvent event = new ReloadEvent(this, this.url, System.currentTimeMillis()); for (RefreshingProperties.RefreshListener listener : this.listeners) { listener.propertiesRefreshNotify(event); } } /** * DOCUMENT ME! */ private void threadCheck() { if (!this.loading && this.updater == null) { check(); } } /** * DOCUMENT ME! * * @author $author$ * @version $Revision: 1.4 $ */ public static interface RefreshListener { /** * DOCUMENT ME! * * @param event DOCUMENT ME! */ public void propertiesRefreshNotify(RefreshingProperties.ReloadEvent event); } /** * DOCUMENT ME! * * @author $author$ * @version $Revision: 1.4 $ */ public static class ReloadEvent { /** * DOCUMENT ME! */ private URL url; /** * DOCUMENT ME! */ private long time; private RefreshingProperties source; /** * Creates a new ReloadEvent object. * * @param url DOCUMENT ME! * @param time DOCUMENT ME! */ public ReloadEvent(RefreshingProperties source, URL url, long time) { this.setSource(source); this.url = url; this.time = time; } /** * DOCUMENT ME! * * @param time DOCUMENT ME! */ public void setTime(long time) { this.time = time; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public long getTime() { return time; } /** * DOCUMENT ME! * * @param url DOCUMENT ME! */ public void setUrl(URL url) { this.url = url; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public URL getUrl() { return url; } public RefreshingProperties getSource() { return source; } public void setSource(RefreshingProperties source) { this.source = source; } } /** * DOCUMENT ME! * * @author $author$ * @version $Revision: 1.4 $ */ private class UpdateThread extends Thread { /** * DOCUMENT ME! */ private RefreshingProperties props; /** * Creates a new UpdateThread object. * * @param props DOCUMENT ME! */ UpdateThread(RefreshingProperties props) { this.setDaemon(true); this.props = props; } /** * DOCUMENT ME! */ public void run() { boolean running = true; while (running) { props.LOG.log(Level.FINEST, "RefeshingProperties thread check of " + props.url + " at " + new Date()); try { Thread.sleep(props.recheckTime); } catch (InterruptedException e) { RefreshingProperties.LOG.log(Level.WARNING, "Interrupted.", e); } props.check(); } } } private class ImportRefreshListener implements RefreshListener { private RefreshingProperties target; private boolean clobber; ImportRefreshListener(RefreshingProperties target, boolean clobber) { this.target = target; this.clobber = clobber; } public void propertiesRefreshNotify(ReloadEvent event) { target.fireEvents(); } } }