/*
* 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();
}
}
}
|