RefreshingProperties.java Source code

Java tutorial

Introduction

Here is the source code for RefreshingProperties.java

Source

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