Package com.slytechs.utils.properties

Source Code of com.slytechs.utils.properties.SmartProperties

/**
* Copyright (C) 2006 Mark Bednarczyk, Sly Technologies, Inc.
*
* 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
*
*/
package com.slytechs.utils.properties;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* <P>A little more advanced properties where any type of object can be stored
* as long as it can convert to/from a string for storage. Also listeners can be
* registered on properties when property values change.</P>
*
* <P>Default properties can and should be defined although not strictly required.</P>
* <P> You start out with a blank properties object, if you want to load properties
* just set the SmartProperties.FILENAME property to the filename you wish to read
* and SmartProperites will do the rest.</P>
*
* @author Mark Bednarczyk
* @author Sly Technologies, Inc.
*/
public class SmartProperties {
  public static final String D_FILENAME = "properties.cfg";

  /**
   * File property for this object. The file property specifies the
   * filename of where the properties are stored. If null properties are
   * not written to disk. By changing this property a new file is instantly
   * created and all existing properties stored immediately.
   */
  public static final String FILENAME = "properties.filename";

  public static final Log logger = LogFactory.getLog(SmartProperties.class);

  private static SmartProperties globaltProperties;
 
  private Exception exception = null;

  /**
   * Creates a new instance of the provided object.
   *
   * @param c The class to invoke "valueOf(String)" method on.
   * @return An instance of the object as generated by valueOf(String) invocation.
   * @throws InstantiationException
   */
  @SuppressWarnings("unchecked")
  private static <T> T invokeValueOf(Class<T> c, String propertyValue) throws Exception {
    Object res = null;
    java.lang.reflect.Method valueOf;
    try {
      valueOf = c.getDeclaredMethod("valueOf", String.class);
    } catch (NoSuchMethodException e) {
      /*
       * If Class.valueOf(String) doesn't exist, try a direct constructor
       * Class<T>(String)
       */
     
      Constructor constructor = c.getDeclaredConstructor(String.class);
      return (T)constructor.newInstance(propertyValue);
    }
    res = valueOf.invoke(/* This is a static method obj=null */null, propertyValue);
   
    return (T) res;
  }
 
  private SmartProperties defaults;

  private Map<String, String> descriptions;

  private Map<String,Integer> lineNumber;

  private PropertyChangeSupport listeners;

  /**
   * File from and to which the properties are read and written to.
   */
  private File propertiesFile;

  private Map<String, Boolean> storeFlags;

  private Map<String, List<String>> values;

  private VetoableChangeSupport vetoables;

  private Map<String, Object> cache;

  private Boolean DEFAULT_STORE_FLAG = false;

  public String PROPERTY_MODIFIED = "modified";
 
  public String PROPERTY_EXCEPTION = "exception";
 
  public String PROPERTY_NEW = "new property";
 
  public String PROPERTY_REMOVE = "removed property";

  private boolean modified = false;

  /**
   * Initialize properties without defaults.
   */
  public SmartProperties() {

    values = new HashMap<String, List<String>>();
    descriptions = new HashMap<String, String>();
    listeners = new PropertyChangeSupport(this);
    vetoables = new VetoableChangeSupport(this);
    storeFlags = new HashMap<String, Boolean>();
    lineNumber = new HashMap<String,Integer>();
    cache = new HashMap<String, Object>();
   
    initProperties();

    setModified(false);
  }


  /**
   * <P>
   * Initialize properties with defaults.
   * </P><P>
   * If a new property is set then the default if overriden and removed.
   * </P>
   *
   * @param defaults the defaults for these properties.
   */
  public SmartProperties(SmartProperties defaults) {
    this();

    this.defaults = defaults;

  }
 
  /**
   * Add a listener for a all property changes.
   *
   * @param propertyName name of the property to listen on for changes.
   * @param listener the listener that will be notified when property
   * changes.
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {

    if (listeners == null) {
      listeners = new PropertyChangeSupport(this);
    }

    listeners.addPropertyChangeListener(listener);
  }


  /**
   * Add a listener for a named property change.
   *
   * @param propertyName name of the property to listen on for changes.
   * @param listener the listener that will be notified when property
   * changes.
   */
  public void addPropertyChangeListener(String propertyName,
      PropertyChangeListener listener) {

    if (listeners == null) {
      listeners = new PropertyChangeSupport(this);
    }

    listeners.addPropertyChangeListener(propertyName, listener);
  }

  /**
   * Add a vetoable listener for a named property change. The listener can
   * throw a veto exception to interrupt and disallow the property to be
   * set to this value.
   *
   * @param propertyName name of the property to listen on for changes.
   * @param listener the listener that will be notified when property
   * changes.
   */
  public void addVetoableChangeListener(String propertyName,
      VetoableChangeListener listener) {

    if (listeners == null) {
      listeners = new PropertyChangeSupport(this);
    }

    vetoables.addVetoableChangeListener(propertyName, listener);
  }

  /**
   * Return a boolean value from the object.
   *
   * @param name name of the property to lookup.
   * @return "boolean" value.
   * @param def default value to return when the lookup fails.
   * @exception Exception a runtime exception is thrown if object can
   * not be converted to indicated primitive.
   */
  public boolean booleanValue(String name, boolean def) {
    return get(Boolean.class, name, def);
  }

  /**
   * Return a float value from the object.
   *
   * @param name name of the property to lookup.
   * @return "float" value.
   * @param def default value to return when the lookup fails.
   * @exception Exception a runtime exception is thrown if object can
   * not be converted to indicated primitive.
   */
  public float floatValue(String name, float def) {
    return get(Float.class, name, def);
  }

  public synchronized <T>T get(Class<T> c, String key, T defaultValue) {
    List<String> value = values.get(key);
   
    if (value != null && value.isEmpty() == false) {
      try {
        return invokeValueOf(c, value.get(0));
      } catch (Exception e) {
        setException(e);
        return defaultValue;
      }

    } else if (defaults != null) {
      return defaults.get(c, key, defaultValue);

    } else {
      return defaultValue;
    }
   
  }
 
  @SuppressWarnings("unchecked")
  public synchronized <T extends Object> List<T> getList(Class<T> c, String key, List<T> defaultValue) {
    List<String> value = values.get(key);
   
    if (value != null && value.isEmpty() == false) {
      // TODO: redo for better generic type checking
      List<T> cachedValue = (List<T>) cache.get(key);
      if (cachedValue == null) {
        cachedValue = new ArrayList<T>(value.size());
       
        for (String v: value) {
          try {
            cachedValue.add(invokeValueOf(c, v));
          } catch (Exception e) {
            setException(e);
          }
        }
      }
     
      return cachedValue;

    } else if (defaults != null) {
      return defaults.getList(c, key, defaultValue);

    } else {
      return defaultValue;
    }
   
  }


  /**
   * Sets the internal property exception and fires off notification
   *
   * @param e
   *   Exception being thrown
   */
  private void setException(Exception e) {
    Exception old = exception;
    exception = e;
   
    listeners.firePropertyChange(PROPERTY_EXCEPTION, old, e);
   
  }


  /**
   * Return property value as object. If a defaults property manager was not
   * specified in the constructor the user given default value is returned
   * when all lookups fail.
   *
   * @param name
   *            property name to lookup
   * @param defaultObject
   *            default value to return when the lookup fails.
   * @return String value of the property
   * @exception Exception
   *                a runtime exception is thrown if object can not be
   *                converted to indicated primitive.
   */
  public String get(String name, String defaultObject) {
    return get(String.class, name, defaultObject);
  }

  /**
   * Returns the description of the named property.
   *
   * @param name name of the property to retrieve description for.
   *
   * @return property description. If property description has not been set
   * before, an empty string is returned "".
   */
  public synchronized String getDescription(String name) {

    String d = (String) descriptions.get(name);

    if (d != null) {
      return d;

    } else if (defaults != null) {
      return defaults.getDescription(name);

    } else {
      return "";
    }
  }

 
  /**
   * Returns the currently set filename where these properites are stored.
   *
   * @return filename of the properties file.
   */
  public String getFilename() {
    return stringValue(FILENAME, D_FILENAME);
  }

  public int getLineno(String propertyName) {
    return lineNumber.get(propertyName);
  }


  /**
   * Initialize properties
   */
  private void initProperties() {

    /*
     * define the  FILENAME property which will cause the properties to
     * be read from the filename
     */
    setDescription(FILENAME, "Filename of this properties file");
    set(FILENAME, D_FILENAME);
    setStore(FILENAME, false); // We dont store this property, for runtime only
   
    /*
     * Handle the property change for FILENAME property
     */
    addVetoableChangeListener(FILENAME, new VetoableChangeListener() {
      /**
       * Handle property change event. SmartProperties object monitors certain
       * properties which have direct effect on this object. Main property
       * being monitored is FILENAME.
       */
      @SuppressWarnings("unchecked")
      public void vetoableChange(PropertyChangeEvent event)
          throws PropertyVetoException {

        /* Open file for reading and read properties */
        if (event.getPropertyName().equals(FILENAME) == true) {
          // TODO: redo for better generic type checking
          List<String> fn = (List<String>) event.getNewValue();
          propertiesFile = new File(fn.get(0));

          try {
            if (propertiesFile.canRead() == true) {
              FileReader in = new FileReader(propertiesFile);
              logger.trace("Reading properties from "+ propertiesFile);
              readAllProperties(in);
              in.close();

            } else {
              propertiesFile.createNewFile();
            }

          } catch (IOException ioe) {
            PropertyVetoException e = new PropertyVetoException(
                "Error property file=" + fn + ", " + ioe.toString(),
                event);

            e.initCause(ioe);

            throw e;

          }
        }
      }

    });

  }

  /**
   * Return an int value from the object.
   *
   * @param name name of the property to lookup.
   * @param def default value to return when the lookup fails.
   * @return "int" value.
   * @exception Exception a runtime exception is thrown if object can
   * not be converted to indicated primitive.
   */
  public int intValue(String name, int def) {
    return get(Integer.class, name, def);
  }

  /**
   * Load all properties from a file. The filename is defined by property
   * FILENAME.
   */
  public void load() throws IOException {
    readAllProperties(stringValue(FILENAME, D_FILENAME));
  }

  public void logFatal(Log log, String property, String msg) {
    log.error(msg);

    log.error("Please correct the configuration file '"
        + getFilename() + "'");
       
   
    /* Write out the comment ahead of the key */
    String c[] = getDescription(property).trim().split("\n");
   
    for (int i = 0; i < c.length; i++) {
      log.error("# " + c[i]);
    }
   
    log.error(property + "=" + get(property, "") + " (on line number "
        + getLineno(property) + " in '" + getFilename() + "')");

  }

  /**
   * Return a long value from the object.
   *
   * @param name name of the property to lookup.
   * @return "long" value.
   * @param default default value to return when the lookup fails.
   * @exception Exception a runtime exception is thrown if object can
   * not be converted to indicated primitive.
   */
  public long longValue(String name, long def) {
    return get(Long.class, name, def);
  }

  /**
   * Reads all properties from a reader stream.
   *
   * @param reader Reader used to reader from its inputstream.
   */
  private void readAllProperties(Reader reader) throws IOException {

    BufferedReader in = new BufferedReader(reader);

    String l;
    String d = "";

    int lineno = 0;
    while ((l = in.readLine()) != null) {
      lineno ++;

      /* trim off white space and skip empty lines */
      if ((l = l.trim()).equals("") == true) {
        continue;
      }

      if (l.startsWith("#") == true) {
        if (l.length() >= 2) {
          d += l.substring(2); // Skip # and space

        } else {
          d += l.substring(1); // Skip # and space
        }
        d += "\n";

      } else {
        /* Must be a property=value pair */
        String[] r = l.split("=", 2); // match n - 1 times

        if (r.length != 2) {
          continue; // Invalid format
        }

        /*
         * 1st = property name
         * 2nd = value
         */
        String name = r[0].trim();
        String value = r[1].trim();

        lineNumber.put(name, lineno);
        setDescription(name, d);
        setStore(name, true); // All read properties are storable by default
        add(name, value);

        d = "";
      }
     

    }
  }

  /**
   * Reads properties from a named file.
   *
   * @param filename filename to open up and read properties from.
   */
  private void readAllProperties(String filename) throws IOException {
    FileReader reader = new FileReader(filename);

    readAllProperties(reader);
  }

  /**
   * Sets or replaces the current value for the named property.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   */
  public synchronized <T extends Object> void set(String name, T value) {
    add(name, value, true);
  }
  /**
   * Appends or adds another value for the named property.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   */
  public synchronized <T extends Object> void add(String name, T value) {
    add(name, value, false);
  }

  /**
   * Sets an opaque value for the named property.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   */
  @SuppressWarnings("unchecked")
  private synchronized <T extends Object> void add(String name, T value, boolean clear) {
    // TODO: redo for better generic type checking
    List<T> old = (List<T>) getList(value.getClass(), name, null);
   
    if (storeFlags.get(name) == null) {
      storeFlags.put(name, DEFAULT_STORE_FLAG );
    }
   
    cache.remove(name);
    if (clear) {
      values.remove(name);
    }

   
    List<String> v = values.get(name);
    if (v == null) {
      v = new ArrayList<String>();
      values.put(name, v);
     
      listeners.firePropertyChange(PROPERTY_NEW, null, name);
    }

    v.add(value.toString());
   
    if ((old == null) || (old.equals(value) == false)) {
      listeners.firePropertyChange(name, old, getList(value.getClass(), name, null));
    }
   
    setModified(true);
  }


  /**
   * <P>
   * Set a description for a property.
   * Every property can have a description to go along with it. This is
   * usefull if you want to have some kind of comment/description above
   * a property in a configuration or properties file.
   * </P><P>
   * Multi-line description separated by newline are usually put on
   * a separate line but still prefixed with appropriate character to
   * indicate a comment in the config file.
   * </P><P>
   * Listeners for property change events are not notified of the description
   * change. Only if the value of the property changes.
   * </P>
   *
   * @param name name of the property to set the description for.
   * @param description description for the property.
   */
  public synchronized void setDescription(String name, String description) {
    descriptions.put(name, description);
    setModified(true);
  }

  /**
   * Set FILENAME property. This causes the object to first check and read
   * properties from this file and then store/update changes to it.
   *
   * @param filename filename of the properties file.
   */
  public void setFilename(String filename) {
    try {
      setWithVeto(FILENAME, filename);

    } catch (PropertyVetoException pve) {
      logger.warn("Filename on properites vetoed by " + pve.toString());
    }
  }

  /**
   * Allows a proproperty to be saved or not saved. If true the property
   * will be saved when the store() method is called. Otherwise when set
   * to false it will not be.
   *
   *
   * @param store True store the named property otherwise the property will
   * not be stored.
   */
  public synchronized void setStore(String property, boolean store) {
    storeFlags.put(property, new Boolean(store));

  }

  /**
   * Sets an opaque value for the named property with Veto capability.
   * A PropertyVetoException can be thrown by the object if the property
   * is out of the bounded range for this property type. I.e. if a filename
   * property is set and the filename reffers to an invalid filename.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   * @throws PropertyVetoException
   */
  public synchronized <T> void setWithVeto(String name, T value) throws PropertyVetoException {
    addWithVeto(name, value, true);
  }
 
  /**
   * Sets an opaque value for the named property with Veto capability.
   * A PropertyVetoException can be thrown by the object if the property
   * is out of the bounded range for this property type. I.e. if a filename
   * property is set and the filename reffers to an invalid filename.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   * @throws PropertyVetoException
   */
  public synchronized <T> void addWithVeto(String name, T value) throws PropertyVetoException {
    addWithVeto(name, value, false);
  }
 
 
  /**
   * Sets an opaque value for the named property with Veto capability.
   * A PropertyVetoException can be thrown by the object if the property
   * is out of the bounded range for this property type. I.e. if a filename
   * property is set and the filename reffers to an invalid filename.
   *
   * @param name name of the property being modified.
   * @param value new opaque value of the named property.
   */
  @SuppressWarnings("unchecked")
  private synchronized <T> void addWithVeto(String name, T value, boolean clear)
      throws PropertyVetoException {

    // TODO: redo for better generic type checking
    List<T> old = (List<T>) getList(value.getClass(), name, null);
   
    cache.remove(name);
   
    if (clear) {
      values.remove(name);
    }

    boolean newProperty = false;
    List<String> v = values.get(name);
    if (v == null) {
      v = new ArrayList<String>();
      values.put(name, v);
     
      newProperty = true;
    }

    v.add(value.toString());
   
    if ((old == null) || (old.equals(value) == false)) {
      try {
        vetoables.fireVetoableChange(name, old, getList(value.getClass(), name, null));
      } catch (PropertyVetoException e) {
        v.remove(v.size() - 1);
        cache.remove(name);
       
        throw e;
      }
    }
   
    if (storeFlags.get(name) == null) {
      storeFlags.put(name, DEFAULT_STORE_FLAG );
    }
   
    if (newProperty) {
      listeners.firePropertyChange(PROPERTY_NEW, null, name);
    }
    setModified(true);
  }

  /**
   * Store all properties in a file. The filename is defined by property
   * FILENAME.
   */
  public void store() throws IOException {
    logger.trace("propertyFile=" + propertiesFile);
    writeAllProperties();
  }

  /**
   * Return a string value from the object.
   *
   * @param name name of the property to lookup.
   * @return "string" value.
   * @param def default value to return when the lookup fails.
   * @exception Exception a runtime exception is thrown if object can
   * not be converted to indicated primitive.
   */
  public String stringValue(String name, String def) {
    return get(name, def);
  }

  /**
   * Writes all the properties to file
   */
  private void writeAllProperties() throws IOException {

    if (propertiesFile == null) {
      return;
    }

    setModified(false);

    logger.trace("Writting properties");

    FileWriter fout = new FileWriter(propertiesFile);
    PrintWriter out = new PrintWriter(fout);

    Set<String> set = values.keySet();
    List<String> list = new LinkedList<String>(set);
    Collections.sort(list);

    int lineno = 1;
    for (String k: list) {

      boolean s = (Boolean) storeFlags.get(k);
      if (s == false) {
        continue;
      }

      List<String> v = values.get(k);

      /* Write out the comment ahead of the key */
      String c[] = getDescription(k).trim().split("\n");

      if (c.length != 0) {
        out.println(""); // Extra empty line
      }

      for (int i = 0; i < c.length; i++) {
        out.println("# " + c[i]);
        lineno ++;
      }

      /* Write out the property=value pair */

      for (String sv: v) {
        out.println(k + "=" + sv);
        lineno ++;

      }
    }

    fout.close();
  }

  public boolean isModified() {
    return modified;
  }
 
  public boolean remove(String name) {
   
    values.remove(name);
    storeFlags.remove(name);
   
    listeners.firePropertyChange(PROPERTY_REMOVE, name, name);
   
    for (PropertyChangeListener l: listeners.getPropertyChangeListeners(name)) {
      listeners.removePropertyChangeListener(name, l);
    }
   
    for (VetoableChangeListener l: vetoables.getVetoableChangeListeners(name)) {
      vetoables.removeVetoableChangeListener(name, l);
    }
   
    return true;
  }


  public void setModified(boolean modified) {
    if (this.modified == modified) {
      return;
    }
   
    this.modified = modified;
   
    listeners.firePropertyChange(PROPERTY_MODIFIED, !modified, modified);
  }
 
  public static SmartProperties getProperites() {
    if (globaltProperties == null) {
      globaltProperties = new SmartProperties();
    }
   
    return globaltProperties;
  }


  public Exception getException() {
    return exception;
  }
} /* END OF: SmartProperties */
 
TOP

Related Classes of com.slytechs.utils.properties.SmartProperties

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.