001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021package org.biojava.utils;
022
023import java.io.BufferedInputStream;
024import java.io.FileInputStream;
025import java.io.FileNotFoundException;
026import java.io.IOException;
027import java.io.InputStream;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.Properties;
031import java.util.StringTokenizer;
032
033/**
034 * a sub-class of java.util.Properties that provides the same constructors, adds two convenient load methods to load
035 * the properties from files and, most importantly, adds getPropertyAsXXX() methods to get a property as an object of
036 * type XXX.
037 *
038 * @author <A href="mailto:Gerald.Loeffler@vienna.at">Gerald Loeffler</A> for the 
039 *         <A href="http://www.imp.univie.ac.at">IMP</A>
040 */
041public class TypedProperties extends Properties {
042  /**
043   * the default string of delimiter characters used by getAsStringList()
044   */
045  public static final String DEFAULT_DELIMITERS = ",;\t";
046  
047  /**
048   * Creates an empty property list with no default values.
049   */
050  public TypedProperties() {
051    super();
052  }
053  
054  /**
055   * Creates an empty property list with the specified defaults.
056   * @param defaults the defaults.
057   */
058  public TypedProperties(Properties defaults) {
059    super(defaults);
060  }
061
062  /**
063   * Reads a property list (key and element pairs) from the file with the given file name.
064   * @param fileName the file name. Not null.
065   * @exception FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
066   *                                  other reason cannot be opened for reading.
067   * @exception IOException if an error occurred when reading from the input stream created from the file with the given
068   *                        name.
069   */
070  public void load(String fileName) throws FileNotFoundException, IOException {
071    if (fileName == null) {
072        throw new IllegalArgumentException("fileName not null");
073    }
074
075    load(new BufferedInputStream(new FileInputStream(fileName)));
076  }
077
078  /**
079   * Reads a property list (key and element pairs) from the given
080   * file which is interpreted as a resource of the given class. The difference
081   * between a normal file and a resource file is the way in which the file is
082   * located: with a normal file, the filename is taken literally to load the
083   * file from the file system, whereas with a resource file, the given name is
084   * used to ask the class loader of the given class to load the file
085   * (see java.lang.Class.getResourceAsStream() and
086   * java.lang.ClassLoader.getSystemResourceAsStream()).
087   *
088   * @see java.lang.Class
089   * @see java.lang.ClassLoader
090   *
091   * @param clazz the class with which the resource identified by resourceName
092   *               is taken to be associated with
093   *               (java.lang.Class.getResourceAsStream() on this Class
094   *               object is used to load the resource). If clazz is null, the
095   *               resource is considered to be a system resource, and
096   *               java.lang.ClassLoader.getSystemResourceAsStream() is used to
097   *               load the resource.
098   * @param resourceName the name of the resource from which to load the properties. It is a precondition that the
099   *                     resource with this name exists (regardless whether it is interpreted as a system resource or a
100   *                     class resource), otherwise an IllegalArgumentException is thrown.
101   * @exception IOException if an error occurred when reading from the input stream created from the given resource.
102   */
103  public void load(Class clazz, String resourceName) throws IOException {
104    InputStream is = null;
105    if (clazz == null) {
106      // load resource as system resource
107      is = ClassLoader.getSystemResourceAsStream(resourceName);
108      if (is == null) {
109          throw new IllegalArgumentException("system reource " + resourceName + " must exist");
110      }
111    } else {
112      // load resource as class resource
113      is = clazz.getResourceAsStream(resourceName);
114      if (is == null) {
115          throw new IllegalArgumentException("resource " + resourceName + " associated with class " + clazz + " must exist");
116      }
117    }
118    load(is);
119  }
120
121  /**
122   * Searches for the property with the specified key in this property list. If the key is not found in this property
123   * list, the default property list, and its defaults, recursively, are then checked. The method returns null if the
124   * property is not found. If the property is found its value is parsed as an integer and returned. If parsing the 
125   * value fails, a NumberFormatException is thrown.
126   *
127   * @param key the property key.
128   * @return the integer value of the property with the given key or null if the given key is not associated with a
129   *         property.
130   * @exception NumberFormatException if the property associated with the given key does not have an integer value.
131   */
132  public Integer getPropertyAsInteger(String key) throws NumberFormatException {
133    String v = getProperty(key);
134    if (v == null) return null;
135    return new Integer(v);
136  }
137
138  /**
139   * Searches for the property with the specified key in this property list. If the key is not found in this property
140   * list, the default property list, and its defaults, recursively, are then checked. The method returns null if the
141   * property is not found. If the property is found its value is parsed as a long and returned. If parsing the 
142   * value fails, a NumberFormatException is thrown.
143   *
144   * @param key the property key.
145   * @return the long value of the property with the given key or null if the given key is not associated with a
146   *         property.
147   * @exception NumberFormatException if the property associated with the given key does not have an integer value.
148   */
149  public Long getPropertyAsLong(String key) throws NumberFormatException {
150    String v = getProperty(key);
151    if (v == null) return null;
152    return new Long(v);
153  }
154
155  /**
156   * Searches for the property with the specified key in this property list. If the key is not found in this property
157   * list, the default property list, and its defaults, recursively, are then checked. The method returns null if the
158   * property is not found. If the property is found its value is parsed as a double and returned. If parsing the 
159   * value fails, a NumberFormatException is thrown.
160   *
161   * @param key the property key.
162   * @return the double value of the property with the given key or null if the given key is not associated with a
163   *         property.
164   * @exception NumberFormatException if the property associated with the given key does not have an integer value.
165   */
166  public Double getPropertyAsDouble(String key) throws NumberFormatException {
167    String v = getProperty(key);
168    if (v == null) return null;
169    return new Double(v);
170  }
171
172  /**
173   * Searches for the property with the specified key in this property list. If the key is not found in this property
174   * list, the default property list, and its defaults, recursively, are then checked. The method returns null if the
175   * property is not found. If the property is found its value is parsed as an boolean and returned. If parsing the 
176   * value fails, a RuntimeException is thrown.
177   * <p>
178   * If the property value is equal, ignoring case, to the string "true" or "yes" then the boolean value
179   * returned from this method is true. If the property value is equal, ignoring case, to the string
180   * "false" or "no"  then the boolean value returned from this method is false.
181   *
182   * @param key the property key.
183   * @return the boolean value of the property with the given key or null if the given key is not associated with a
184   *         property.
185   * @exception RuntimeException if the property associated with the given key does not have an integer value.
186   */
187  public Boolean getPropertyAsBoolean(String key) throws RuntimeException {
188    String v = getProperty(key);
189    if (v == null) return null;
190    if      (v.equalsIgnoreCase("true")  || v.equalsIgnoreCase("yes")) return Boolean.TRUE;
191    else if (v.equalsIgnoreCase("false") || v.equalsIgnoreCase("no"))  return Boolean.FALSE;
192    else throw new RuntimeException("property value " + v + " is not parseable as a boolean");
193  }
194
195  /**
196   * Searches for the property with the specified key in this property list. If the key is not found in this property
197   * list, the default property list, and its defaults, recursively, are then checked. The method returns null if the
198   * property is not found. If the property is found its value is parsed as a list of strings and returned as a List
199   * object that contains only String objects. Parsing the property value as a list of strings can not fail and so this
200   * method does not throw an exception.
201   * <p>
202   * The property value is interpreted as String objects (tokens) separated by one or more (consecutive) separator
203   * characters taken from the delims string. Any of these characters separates the tokens and can hence not be part of
204   * any token! The tokens identified in this way are put into a List in the order in which they appear in the property 
205   * value. White space at the beginning and end of each token are removed before storing the token as an element of the
206   * list (this includes white space at the beginning and end of the complete property value)! Empty strings are also
207   * never added to the list, i.e. if after removal of white space from a token a token is the empty string, it is not
208   * stored in the list! All this results in a very natural conversion of the property value into a list of strings:
209   * only "real" (non-white-space, non-white-space-bounded, non-delimiter-containing) sub-strings from the
210   * property value are put as string elements into the list.
211   *
212   * @param key the property key.
213   * @param delims the string of allowed delimiter characters (not null and not empty).
214   * @return the List of strings for the property with the given key or null if the given key is not associated with a
215   *         property. An empty list is returned if a property with the given key exists but its value is empty or
216   *         consists only of white space.
217   */
218  public List getPropertyAsStringList(String key, String delims) {
219    if (delims == null || delims.length() == 0) {
220        throw new IllegalArgumentException("delims != null && delims.length() > 0");
221    }
222
223    String v = getProperty(key);
224    if (v == null) return null;
225
226    List l = new ArrayList();
227
228    v = v.trim();
229    if (v.length() > 0) {
230      StringTokenizer st = new StringTokenizer(v, delims, false);
231      while (st.hasMoreTokens()) {
232        String token = (st.nextToken()).trim();
233        if (token != null && token.length() > 0) l.add(token); // store only non-empty tokens
234      }
235    }
236
237    return l;
238  }
239  
240  /**
241   * just like getPropertyAsStringList(String key, String delims) but uses ',' (comma), ';' (semicolon) and '\t' (tab)
242   * as the possible delimiters.
243   */
244  public List getPropertyAsStringList(String key) {
245    return getPropertyAsStringList(key, DEFAULT_DELIMITERS);
246  }
247
248  public String toString() {
249    return "TypedProperties";
250  }
251  
252  public boolean equals(Object o) {
253    if (o == this) return true;
254    
255    // this class extends another class than Object:
256    if (!super.equals(o)) return false;
257    
258    // this and that are identical if we made it 'til here
259    return true;
260  }
261  
262  public int hashCode() {
263    // this class extends another class than Object:
264    int hc = super.hashCode();
265
266    return hc;
267  }
268  
269  public Object clone() {
270    TypedProperties o = (TypedProperties) super.clone();
271
272    return o;
273  }
274}