All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.eBus.util.Properties Maven / Gradle / Ivy

There is a newer version: 7.4.0
Show newest version
//
// 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
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright (C) 2001 - 2010, 2012. Charles W. Rapp.
// All Rights Reserved.
//

package net.sf.eBus.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@code Properties} extends
 * {@code java.util.Properties} to include:
 * 
    *
  • * Loading properties from a named file and returning * a {@code net.sf.eBus.util.Properties} object. *
  • *
  • * Storing a {@link net.sf.eBus.util.Properties} object * into a named file. *
  • *
  • * Returning property values as a boolean, int, double and * String array. *
  • *
  • * Setting property values as a boolean, int, double and * String array. *
  • *
  • * Informs registered {@link PropertiesListener}s when the * underlying properties file has changed and been * automatically reloaded. *

    * Note: automatic properties file reloading is done only if * there is a registered properties listener. If there are * no registered listeners, then Properties does not watch * the underlying file for changes and so does not * automatically reload the properties file if it should * change. *

  • *
* * @author Charles Rapp */ public final class Properties extends java.util.Properties { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * This is eBus version 2.1.0. */ private static final long serialVersionUID = 0x050200L; /** * Append this suffix to the file name. */ private static final String THREAD_SUFFIX = "WatchThread"; //----------------------------------------------------------- // Statics. // /** * Once a property file is loaded, store the Properties * object in this map under its file name. */ private static Map sPropertiesMap = new HashMap<>(); /** * Properties map synchronizer. */ private static Lock sPropertiesMutex = new ReentrantLock(); /** * Logging subsystem interface. */ private static final Logger sLogger = Logger.getLogger(Properties.class.getName()); //----------------------------------------------------------- // Locals. // /** * This properties file is associated with this file. */ private final String mFileName; /** * Inform these listeners of property file changes. */ private transient final List mListeners; /** * This thread polls the watch key for file updates. */ private transient WatchThread mWatchThread; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates an empty property list with no default values. * Because there is no associated file, property listening * is not allowed. */ public Properties() { super (); mFileName = null; mListeners = new LinkedList<>(); mWatchThread = null; } // end of Properties() /** * Creates an empty property list with default values. * Because there is no associated file, property listening * is not allowed. * @param defaults the default property values. */ public Properties(final java.util.Properties defaults) { super (defaults); mFileName = null; mListeners = new LinkedList<>(); mWatchThread = null; } // end of Properties(java.util.Properties) // This properties object cannot be directly instantiated // by an application but only through the loadProperties() // static methods. private Properties(final String fileName) { super (); mFileName = fileName; mListeners = new LinkedList<>(); mWatchThread = null; } // end of Properties(String) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // Object Method Overrides. // @Override public boolean equals(final Object o) { return (super.equals(o)); } // equals(Object) @Override public int hashCode() { return (super.hashCode()); } // end of hashCode() // // end of Object Method Overrides. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // // // Get methods, no default. // /** * Returns the named property value as a * {@code boolean}. If {@code key} is an unknown * property or is neither {@code "true"} nor * {@code "false"} then returns {@code false}. * @param key The property key. * @return the named property value as a * {@code boolean}. If the value is {@code "true"}, * then {@code true} is returned, otherwise returns * {@code false}. * @see #getBooleanProperty(String, boolean) * @see #setBooleanProperty(String, boolean) */ public boolean getBooleanProperty(final String key) { final String value = getProperty(key); boolean retval = false; if (value != null) { final Boolean bool = Boolean.valueOf(value.trim()); retval = bool; } return (retval); } // end of getBooleanProperty(String) /** * Returns the named property value as an {@code int}. * If {@code key} is an unknown property or is not a * valid integer, then returns {@code 0}. * @param key The property key. * @return the named property value as an {@code int}. * @see #getIntProperty(String, int) * @see #setIntProperty(String, int) */ public int getIntProperty(final String key) { final String value = getProperty(key); int retval = 0; if (value != null && value.length() > 0) { try { retval = Integer.parseInt(value.trim()); } catch (NumberFormatException numberex) { // Ignore - return value already set to 0. } } return (retval); } // end of getIntProperty(String) /** * Returns the named property value as a {@code double}. * If {@code key} is an unknown property or is not a * value double, then returns {@code 0.0}. * @param key The property key. * @return the named property value as a {@code double}. * @exception NumberFormatException * if the property value is not a valid double. * @see #getDoubleProperty(String, double) * @see #setDoubleProperty(String, double) */ public double getDoubleProperty(final String key) { final String value = getProperty(key); double retval = 0.0; if (value != null && value.length() > 0) { try { retval = Double.parseDouble(value.trim()); } catch (NumberFormatException numberex) { // Ignore - return value already set to 0.0. } } return (retval); } // end of getDoubleProperty(String) /** * Returns the named property value as a * {@code String[]}. {@code ifs} is used as * the interfield separator character. * If the property value does not exist, then returns * an empty array. * @param key The property key. * @param ifs The interfield separator. * @return the named property value as a * {@code String[]}. If the property value does not * exist, then returns an empty array. * @see #setArrayProperty(String, String[], char) */ public String[] getArrayProperty(final String key, final char ifs) { final String value = getProperty(key); String[] retval; if (value == null || value.length() == 0) { retval = new String[0]; } else { // Place the ifs in a regular expression [] block // just in case the ifs is also a regular expression // character. retval = value.split(String.format("[%c]", ifs)); } return (retval); } // end of getArrayProperty(String, char) // // Get methods with default. // /** * Returns the named property value as a * {@code boolean}. If {@code key} is an unknown * property or an invalid boolean string, then returns the * default value. * @param key The property key. * @param defaultValue The default value. * @return the named property value as a * {@code boolean}. If {@code key} is an unknown * property or an invalid boolean string, then returns * {@code defaultValue}. * @see #getBooleanProperty(String) * @see #setBooleanProperty(String, boolean) */ public boolean getBooleanProperty(final String key, final boolean defaultValue) { final String value = getProperty(key); boolean retval; if (value == null) { retval = defaultValue; } else { final Boolean bool = Boolean.valueOf(value.trim()); retval = bool; } return (retval); } // end of getBooleanProperties(String, boolean) /** * Returns the named property value as an {@code int}. * If either the property does not exist or does exist but * is not an integer, then returns the default value. * @param key The property key. * @param defaultValue The default value. * @return the named property value as an {@code int} * or {@code defaultValue}. * @see #getIntProperty(String) * @see #setIntProperty(String, int) */ public int getIntProperty(final String key, final int defaultValue) { final String value = getProperty(key); int retval = defaultValue; if (value != null && value.length() > 0) { try { retval = Integer.parseInt(value.trim()); } catch (NumberFormatException formex) { retval = defaultValue; } } return (retval); } // end of getIntProperty(String, int) /** * Returns the named property value as a {@code double}. * If the property value does not exist or is not a valid * {@code double}, then returns the default value. * @param key The property key. * @param defaultValue The default value. * @return the named property value as a {@code double}. * If the property value does not exist or is not a valid * {@code double}, then returns * {@code defaultValue}. * @see #getDoubleProperty(String) * @see #setDoubleProperty(String, double) */ public double getDoubleProperty(final String key, final double defaultValue) { final String value = getProperty(key); double retval; if (value == null || value.length() == 0) { retval = defaultValue; } else { try { retval = Double.parseDouble(value); } catch (NumberFormatException formex) { retval = defaultValue; } } return (retval); } // end of getDoubleProperties(String, double) /** * Returns a set of keys in this property list whose key * matches the given regular expression pattern {@code p} * and the corresponding values are strings. * Includes distinct keys in the default property list if a * key of the same name is not in the main properties list. * Properties whose key or value is not of type * {@code String} are omitted. *

* The returned set is not backed by the {@code Properties} * object. Changes to {@code this Properties} are not * reflected in the set or vice versa. *

* @param p match property keys against this pattern. * @return String property keys matching the regular expression * pattern. * @see #stringPropertyNames() * @see #defaults */ public Set stringPropertyNames(final Pattern p) { final Iterator it; Matcher m; final Set retval = this.stringPropertyNames(); for (it = retval.iterator(); it.hasNext() == true;) { m = p.matcher(it.next()); // If the property does *not* match the pattern, // then remove it from the set. if (m.matches() == false) { it.remove(); } } return (retval); } // end of stringPropertyNames(Pattern) // // end of Get Methods. //----------------------------------------------------------- //----------------------------------------------------------- // Set Methods. // /** * Sets the named property value to the specified boolean. * @param key The property key. * @param value The boolean value. * @see #getBooleanProperty(String) * @see #getBooleanProperty(String, boolean) */ public void setBooleanProperty(final String key, final boolean value) { final Boolean obj = value; setProperty(key, obj.toString()); return; } /** * Sets the named property value to the specified integer. * @param key The property key. * @param value The integer value. * @see #getIntProperty(String) * @see #getIntProperty(String, int) */ public void setIntProperty(final String key, final int value) { setProperty(key, Integer.toString(value)); return; } // end of setIntProperty(String, int) /** * Sets the named property value to the specified double. * @param key The property key. * @param value The double value. * @see #getDoubleProperty(String) * @see #getDoubleProperty(String, double) */ public void setDoubleProperty(final String key, final double value) { setProperty(key, Double.toString(value)); return; } // end of setDoubleProperty(String, double) /** * Sets the named property value to the string array. * The array is converted into a single string by * concatenating the strings together separated by * {@code ifs}. * @param key The property key. * @param value The string array. * @param ifs The interfield separator. * @see #getArrayProperty */ public void setArrayProperty(final String key, final String[] value, final char ifs) { StringBuffer buffer; int bufferLen; int i; String newValue = ""; if (value.length > 0) { // Figure out the buffer size first by adding up // the string length. for (i = 0, bufferLen = 0; i < value.length; ++i) { bufferLen += value[i].length(); } // Add in the IFS. bufferLen += value.length - 1; // Allocate the buffer and start filling it in. buffer = new StringBuffer(bufferLen); for (i = 0; i < value.length; ++i) { if (i > 0) { buffer.append(ifs); } buffer.append(value[i]); } newValue = buffer.toString(); } setProperty(key, newValue); return; } // end of setArrayProperty(String, String[], char) // // end of Set Methods. //----------------------------------------------------------- /** * Reloads properties from the properties file. * @exception IOException * if there are errors reading in the properties file. * @see #store(String) * @see #loadProperties(String) * @see #loadProperties(File) */ public void load() throws IOException { if (mFileName == null) { throw ( new IOException("no property file to load")); } try (final FileInputStream fis = new FileInputStream(mFileName)) { load(fis); } catch (FileNotFoundException nofilex) { // Convert this exception to an IOException. throw ( new IOException(mFileName + " does not exist")); } return; } // end of load() /** * Stores properties in properties file using the provided * header comment. The header comment is placed at the * property file's start. * @param header File header comment. May be * {@code null}. * @exception FileNotFoundException * if the properties file could not be created. * @exception IOException * if there is an error storing the properties into the * file. * @see #load * @see #loadProperties(String) * @see #loadProperties(File) */ public void store(final String header) throws FileNotFoundException, IOException { if (mFileName == null) { throw (new IOException("no property file")); } else { try (final FileOutputStream fos = new FileOutputStream(mFileName)) { store(fos, header); } } return; } // end of store(String) /** * Stores properties in the named properties file using the * provided header comment. The header comment is placed at * the property file's start. * @param fileName Property file's name. * @param header File header comment. May be * {@code null}. * @exception IllegalArgumentException * if {@code fileName} is either {@code null} or an empty * string. * @exception FileNotFoundException * if the properties file could not be created. * @exception IOException * if there is an error storing the properties into the * file. * @see #store(String) * @see #store(File, String) * @see #load * @see #loadProperties(String) * @see #loadProperties(File) */ public void store(final String fileName, final String header) throws IllegalArgumentException, FileNotFoundException, IOException { if (fileName == null || fileName.isEmpty() == true) { throw ( new IllegalArgumentException( "null or empty fileName")); } else { store(new File(fileName), header); } return; } // end of store(String, String) /** * Stores properties in the named properties file using the * provided header comment. The header comment is placed at * the property file's start. * @param file Property file. * @param header File header comment. May be * {@code null}. * @exception IllegalArgumentException * if {@code file} is {@code null}. * @exception FileNotFoundException * if the properties file could not be created. * @exception IOException * if there is an error storing the properties into the * file. * @see #store(String) * @see #store(String, String) * @see #load * @see #loadProperties(String) * @see #loadProperties(File) */ public void store(final File file, final String header) throws IllegalArgumentException, FileNotFoundException, IOException { boolean existsFlag; if (file == null) { throw (new IllegalArgumentException("null file")); } // Only non-directory, writeable files may be property // files. else if ((existsFlag = file.exists()) == true && file.isDirectory() == true) { throw (new IOException(file.getName() + " is a directory")); } else if (existsFlag == true && file.canWrite() == false) { throw (new IOException(file.getName() + " is unwriteable")); } else { try (final FileOutputStream fos = new FileOutputStream(file)) { store(fos, header); } } return; } // end of store(File, String) /** * Adds a properties listener. If this is the first listener * and the watchRate > zero, then starts the watch timer. *

* Note: when the underlying properties file changes, it will * be automatically reloaded prior to calling back to the * registered {@link PropertiesListener}s. * @param listener Add this properties listener. * @exception IllegalArgumentException * if {@code listener} is {@code null}. * @exception IllegalStateException * if this is no underlying property file. */ public void addListener(final PropertiesListener listener) throws IllegalArgumentException, IllegalStateException { boolean addFlag = false; int listenerSize = 0; if (mFileName == null) { throw ( new IllegalStateException( "no underlying property file")); } else if (listener == null) { throw ( new IllegalArgumentException("null listener")); } synchronized (mListeners) { if (mListeners.contains(listener) == false) { mListeners.add(listener); addFlag = true; listenerSize = mListeners.size(); } } // If this is the first listener, then // start watching the properties file. if (addFlag == true && listenerSize == 1) { try { final File f = (new File(mFileName)).getAbsoluteFile(); final FileSystem fs = FileSystems.getDefault(); final Path p = fs.getPath(f.getParent()); final Path fp = fs.getPath(f.getName()); final WatchService watcher; final WatchKey key; if (sLogger.isLoggable(Level.FINE) == true) { sLogger.fine( String.format( "Watching %s properties file.", mFileName)); } watcher = fs.newWatchService(); key = p.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); mWatchThread = new WatchThread(fp, key, watcher, String.format("%s%s", f.getName(), THREAD_SUFFIX), this); mWatchThread.start(); } catch (IOException ioex) { sLogger.log(Level.WARNING, "File watch failed.", ioex); } } return; } // end of addListener(PropertiesListener) /** * Removes a properties listener. If there are no more * listeners and the watch timer is running, then the * timer is canceled. *

* Note: when the watch timer is canceled, Properties * will no longer determine if the underlying properties file * has changed and so will not automatically reload said file * if it should change. * @param listener Remove this listener. * @exception IllegalStateException * if this is no underlying property file. */ public void removeListener(final PropertiesListener listener) { if (mFileName == null) { throw ( new IllegalStateException( "no underlying property file")); } if (listener != null) { boolean removeFlag = false; boolean emptyFlag = false; synchronized (mListeners) { if (mListeners.contains(listener) == true) { mListeners.remove(listener); removeFlag = true; emptyFlag = mListeners.isEmpty(); } } // If there are no more listeners, then // stop watching the properties file. if (removeFlag == true && emptyFlag == true) { mWatchThread.cancel(); mWatchThread = null; if (sLogger.isLoggable(Level.FINE) == true) { sLogger.fine( String.format( "Stopped watching %s properties file.", mFileName)); } } } return; } // end of removeListener(PropertiesListener) /** * Returns a properties list loaded with the values found * in the named properties file. If the file does not exist, * an empty properties object is returned. This allows new * properties to be created and stored. * @param fileName the properties file name. * @return A properties list. * @exception IllegalArgumentException * if {@code fileName} is either {@code null} or an empty * string. * @exception IOException * if {@code fileName} is not a valid properties * file. * @see #loadProperties(File) * @see #load * @see #store(String) */ public static Properties loadProperties( final String fileName) throws IllegalArgumentException, IOException { Properties retval = null; if (fileName == null || fileName.isEmpty() == true) { throw ( new IllegalArgumentException( "null or empty fileName")); } else { retval = loadProperties(new File(fileName)); } return (retval); } // end of loadProperties(String) /** * Returns a properties list loaded with the values found * in the properties file. If the file does not exist, * an empty properties object is returned. This allows new * properties to be created and stored. * @param file the properties file object. * @return A properties list. * @exception IllegalArgumentException * if {@code file} is {@code null}. * @exception IOException * if {@code file} is not a valid properties file. * @see #loadProperties(String) * @see #load * @see #store(String) */ public static Properties loadProperties(final File file) throws IllegalArgumentException, IOException { boolean existsFlag; Properties retval = null; if (file == null) { throw (new IllegalArgumentException("null file")); } // Only non-directory, readable files may be property // files. else if ((existsFlag = file.exists()) == true && file.isDirectory() == true) { throw (new IOException(file.getName() + " is a directory")); } else if (existsFlag == true && file.canRead() == false) { throw (new IOException(file.getName() + " is unreadable")); } else { final String fileName = file.getPath(); sPropertiesMutex.lock(); try { retval = sPropertiesMap.get(fileName); // Check if this properties file has already been // loaded. if (retval == null) { if (sLogger.isLoggable(Level.FINE) == true) { sLogger.fine( String.format( "Loading %s properties file.", fileName)); } // No. Create a new properties object and // load it up. retval = createInstance( file, fileName, existsFlag); } } catch (IOException ioex) { throw (ioex); } finally { sPropertiesMutex.unlock(); } } return (retval); } // end of loadProperties(File) /** * Does the actual work of creating and filling a properties * instance. * @param file load the properties from this file. * @param fileName the properties file name. * @param existsFlag if {@code true} then read in the * properties file; otherwise do not. * @return the properties instance. * @throws IOException * if there is an error loading the properties file. */ private static Properties createInstance( final File file, final String fileName, final boolean existsFlag) throws IOException { final Properties retval = new Properties(fileName); sPropertiesMap.put(fileName, retval); if (existsFlag == true) { try (final FileInputStream fis = new FileInputStream(file)) { retval.load(fis); } catch (IOException ioex) { // Remove the properties object from the // map and re-throw the exception. sPropertiesMap.remove(fileName); throw (ioex); } } return (retval); } // end of createInstance(File, String, boolean) private void handleFileEvent(final WatchEvent event) { if (sLogger.isLoggable(Level.FINE) == true) { sLogger.fine( String.format("Properties event: %s.", (event.kind()).name())); } // Whether the properties file was created, modified or // deleted, they are all "updates". // Reload the properties and inform the listeners. try { final PropertiesEvent propEvent = new PropertiesEvent(this); List listeners = new LinkedList<>(); load(); // Copy the listeners list and use that. That way // _listeners can be modified without affecting // the loop below. synchronized (mListeners) { listeners.addAll(mListeners); } // Tell the listeners about the changes but only // if the modified properties are successfully // loaded. listeners.stream(). forEach((listener) -> { try { listener.propertiesUpdate(propEvent); } catch (Exception jex) { sLogger.log(Level.WARNING, "Properties listener exception.", jex); } }); } catch (IOException ioex) { // Ignore. } return; } // end of handleFileEvent(WatchEvent) //--------------------------------------------------------------- // Inner classes. // private static final class WatchThread extends Thread { //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // public WatchThread(final Path propFile, final WatchKey key, final WatchService watcher, final String name, final Properties owner) { super (name); _propertiesFile = propFile; _key = key; _watcher = watcher; _owner = owner; } // end of WatchThread(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Thread Method Overrides. // /** * This thread waits for watch events to occur on the * registered watch key and reports them to the owning * properties instance. This thread terminates when * the watch key is no longer registered with the watch * service. */ @Override public void run() { boolean runFlag = true; // Continue taking watch events until the no longer // registered. while (runFlag == true) { // Wait for the watch key to be signalled. // Why? // Because pollEvents() does not block. try { // Don't bother with the returned key - // it will be the one we already have. _watcher.take(); // Get the latest events and pass them to the // properties instance. _key.pollEvents().stream(). filter((event) -> (event.kind()!= OVERFLOW && (event.context()).equals( _propertiesFile) == true)). forEach((event) -> { _owner.handleFileEvent(event); }); // Ignore overflows. } catch (InterruptedException interrupt) { // Ignore. } // Reset the key before checking for new events. // If reset() returns false, then we are no // longer registered with the watch service. runFlag = _key.reset(); } return; } // end of run() // // end of Thread Method Overrides. //------------------------------------------------------- /** * Cancels the registration with the watch service. * This will cause this thread to stop running. */ public void cancel() { _key.cancel(); this.interrupt(); return; } // end of cancel() //----------------------------------------------------------- // Member data. // /** * Look for changes to this file only. */ private final Path _propertiesFile; /** * Use to key to watch for changes to this directory. */ private final WatchKey _key; /** * The key is registered with this watch service. */ private final WatchService _watcher; /** * When a watch event occurs, inform this properties * instance. */ private final Properties _owner; } // end of class WatchThread } // end of class Properties // // CHANGE LOG // $Log: Properties.java,v $ // Revision 1.6 2008/01/19 14:02:29 charlesr // Added net.sf.eBus.io imports. // // Revision 1.5 2005/07/21 00:06:43 charlesr // Moved to Java 5: // + Using generics in collection declarations. // + Using for-each syntax. // // Revision 1.4 2004/12/26 13:35:51 charlesr // Correct critical section synchronization. // // Revision 1.3 2004/07/25 16:02:05 charlesr // Corrected javadoc comments. // // Revision 1.2 2004/07/19 14:43:35 charlesr // Added constructors matching java.util.Properties. Fixed adding // or removing listener from within properties update callback. // // Revision 1.1 2004/03/02 23:19:54 charlesr // Added PropertiesListener support. // // Revision 1.0 2003/11/20 01:46:48 charlesr // Initial revision //





© 2015 - 2024 Weber Informatics LLC | Privacy Policy