View Javadoc

1   /***
2    * Copyright (c) 2003, 2004, Chess iT
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    * 
8    * - Redistributions of source code must retain the above copyright notice, this
9    *   list of conditions and the following disclaimer.
10   *
11   * - Redistributions in binary form must reproduce the above copyright notice,
12   *   this list of conditions and the following disclaimer in the documentation
13   *   and/or other materials provided with the distribution.
14   *
15   * - Neither the name of Chess iT, nor the names of its contributors may be used 
16   *   to endorse or promote products derived from this software without specific
17   *   prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
20   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
21   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
22   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
23   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
24   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
25   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
26   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
27   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
29   * POSSIBILITY OF SUCH DAMAGE.
30   * 
31   */
32  package nl.chess.it.util.config;
33  
34  import java.io.File;
35  import java.io.InputStream;
36  import java.lang.reflect.Method;
37  import java.lang.reflect.Modifier;
38  import java.net.MalformedURLException;
39  import java.net.URL;
40  import java.text.DateFormat;
41  import java.text.MessageFormat;
42  import java.text.NumberFormat;
43  import java.text.ParsePosition;
44  import java.util.ArrayList;
45  import java.util.Date;
46  import java.util.HashSet;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.Properties;
50  import java.util.Set;
51  import java.util.StringTokenizer;
52  
53  /***
54   * Configuration class. It's most important feature is an extensive self-validation call (
55   * {@link #validateConfiguration()}) that not only makes sure that required properties exists, but
56   * also that they are of the correct type and don't cause exceptions.
57   * <p>
58   * Partly based on the old Config class by Rolf Heller.
59   * </p>
60   * 
61   * @author Guus Bosman (Chess-iT)
62   */
63  public abstract class Config {
64  
65      private final InstrumentedProperties properties;
66  
67      /***
68       * Name of the file that has been used to fill the configuration data. Used for displaying
69       * messages when something goes wrong.
70       */
71      private final String sourceDescription;
72  
73      /***
74       * Constructs a Config based on an existing Properties. This allows for a custom location of the
75       * properties. Also, it makes it possible to change the values of the properties used in this
76       * class at runtime. The Properties given is used directly (no defensive copy is made). Do
77       * whatever you like, but changing values at run-time this way has <b>not </b> been tested, and
78       * may lead to interesting problems.
79       * 
80       * @param properties Cannot be <code>null</code>.
81       * @throws NullPointerException If properties is <code>null</code>.
82       */
83      protected Config(Properties properties) {
84          this.sourceDescription = "<Properties outside this class>";
85  
86          if (properties == null) {
87              throw new NullPointerException("properties cannot be null here.");
88          }
89  
90          this.properties = new InstrumentedProperties(properties);
91      }
92  
93      /***
94       * Constructs a Config based on a resource. This resource will be lookup using this class'
95       * classloader, and then be read using an URL.
96       * 
97       * @param resourceName Name of the resource to look for, as used in {@link
98       *            java.lang.ClassLoader#getResource(java.lang.String)}. Cannot be <code>null</code>.
99       * @throws NullPointerException If resourceName is <code>null</code>.
100      */
101     protected Config(String resourceName) throws ConfigurationException {
102         if (resourceName == null) {
103             throw new NullPointerException("resourceName cannot be null here.");
104         }
105 
106         URL location = this.getClass().getClassLoader().getResource(resourceName);
107 
108         if (location == null) {
109             throw new ConfigurationException("Cannot find resource '" + resourceName
110                     + "' to load configuration properties from.");
111         }
112 
113         /*
114          * Store for displaying later.
115          */
116         this.sourceDescription = location.toString();
117 
118         InputStream inputStream;
119 
120         try {
121             inputStream = location.openStream();
122 
123             Properties tmpproperties = new Properties();
124             tmpproperties.load(inputStream);
125             properties = new InstrumentedProperties(tmpproperties);
126         } catch (Exception e) {
127             throw new ConfigurationException("Could not read resource '" + resourceName
128                     + "' to load configuration properties from.", e);
129         }
130     }
131 
132     /***
133      * Validates the configuration properties. That means that all getters of this class are tested
134      * once; any exceptions thrown when calling a getter are reported.
135      * 
136      * @return ConfigValidationResult the result of the validation. Never <code>null</code>.
137      */
138     public final ConfigValidationResult validateConfiguration() {
139         Class toCheck = this.getClass();
140         Method[] methods = toCheck.getMethods();
141 
142         Object object = this;
143         ConfigValidationResult result = new ConfigValidationResult();
144         result.setSourceDescription(sourceDescription);
145 
146         /*
147          * Keep track of which properties are used.
148          */
149         properties.startInstrumenting();
150 
151         for (int i = 0; i < methods.length; i++) {
152             Method method = methods[i];
153 
154             if (isPublicGetter(method)) {
155                 tryMethodAndRememberResult(object, method, result);
156             }
157         }
158 
159         Set usedKeys = properties.getUsedKeys();
160         Set allKeys = new HashSet(properties.getProperties().keySet());
161         allKeys.removeAll(usedKeys);
162         result.setUnusedProperties(allKeys);
163         properties.stopInstrumenting();
164 
165         return result;
166     }
167 
168     private void tryMethodAndRememberResult(Object object, Method method, ConfigValidationResult result) {
169         try {
170             properties.clearResult();
171 
172             Object resultValue = method.invoke(object, null);
173             String lastUsedKey = properties.getLastUsedKey();
174             String lastUsedValue = properties.getLastUsedValue();
175             PropertyDisplayItem displayItem = new PropertyDisplayItem(method.getName(), lastUsedKey, lastUsedValue,
176                     (resultValue == null ? null : resultValue.toString()));
177             result.getUsedProperties().add(displayItem);
178         } catch (Exception e) {
179             // the 'result' will construct an error message from the exception.
180             result.addError(method.getName(), e);
181         }
182     }
183 
184     /***
185      * Whether or not a method is a public getter. That means a method whose name starts with "get"
186      * or with "is". Additionally a check could be made on whether or not the right return type is
187      * used ("is" must match boolean), but that's optional.
188      * 
189      * @param method Method to look at.
190      * @return <code>true</code> is the method is a public getter, <code>false</code> otherwise.
191      */
192 
193     /*
194      * @todo Make sure that Exceptions are also taken into account when looking at the signature of
195      * a method.
196      */
197     private boolean isPublicGetter(Method method) {
198         if (method.getName().equals("getClass")) {
199             return false;
200         }
201 
202         boolean isOkay = Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())
203                 && ((method.getName().startsWith("get")) || method.getName().startsWith("is"))
204                 && (!method.getReturnType().equals(Void.TYPE));
205 
206         return isOkay;
207     }
208 
209     /***
210      * Gets the String value for the given key. <b>Note: </b> this method is not the one that gives
211      * access to the underlying Properties, see {@link #getValue(String)}for that.
212      * 
213      * @param key Key to search on.
214      * @return String value. Never <code>null</code>, always <code>trimmed()</code>.
215      * @throws MissingPropertyException if the key does not result in a value.
216      */
217     protected final String getString(String key) {
218         String value = getValue(key);
219 
220         if (value == null) {
221             throw new MissingPropertyException(key);
222         }
223 
224         return value.trim();
225     }
226 
227     /***
228      * Gets the value String for the given key. Throws an exception that describes the type of the
229      * expected value, which is nice to let the user know what kind of value should be filled in.
230      * 
231      * @param key Key to search on.
232      * @param expectedType Description of the expected type of the value of the key we're looking
233      *            for. Used in error messages.
234      * @return String value. Never <code>null</code>, always <code>trimmed()</code>.
235      * @throws MissingPropertyException if the key does not result in a value.
236      */
237     protected final String getString(String key, String expectedType) {
238         String value = getValue(key);
239 
240         if (value == null) {
241             throw new MissingPropertyException(key, expectedType);
242         }
243 
244         return value.trim();
245     }
246 
247     /***
248      * This method gives access to the underlying Properties this class is wrapped around. You could
249      * overwrite this to do special tricks for every value that is read from the property-file
250      * (Properties object, really).
251      * 
252      * @param key. Cannot be <code>null</code>.
253      * @return Value for the given key, or <code>null</code> if such a value cannot be found.
254      */
255     protected String getValue(String key) {
256         return properties.getProperty(key);
257     }
258 
259     protected final int getInt(String key) {
260         String stringValue = getString(key, "integer");
261 
262         try {
263             return Integer.parseInt(stringValue);
264         } catch (Exception e) {
265             throw new InvalidPropertyException(key, stringValue, "integer");
266         }
267     }
268 
269     protected final long getSizeInBytes(String key) {
270         String stringValue = getString(key, "size in bytes");
271         Number result;
272         ParsePosition pp = new ParsePosition(0);
273         NumberFormat format = NumberFormat.getNumberInstance(Locale.US);
274 
275         try {
276             result = format.parse(stringValue, pp);
277         } catch (Exception e) {
278             throw new InvalidPropertyException(key, stringValue, "file size");
279         }
280 
281         long multiplier = 1;
282 
283         /***
284          * Determine unit.
285          */
286         if (pp.getIndex() < stringValue.length()) {
287             String leftOver = stringValue.substring(pp.getIndex()).trim();
288             boolean understood = false;
289 
290             if (leftOver.equals("KB")) {
291                 multiplier = 1024;
292                 understood = true;
293             }
294 
295             if (leftOver.equals("kB")) {
296                 multiplier = 1000;
297                 understood = true;
298             }
299 
300             if (leftOver.equals("MB")) {
301                 multiplier = 1024 * 1024;
302                 understood = true;
303             }
304 
305             if (leftOver.equals("mB")) {
306                 multiplier = 1000 * 1000;
307                 understood = true;
308             }
309 
310             if (leftOver.equals("GB")) {
311                 multiplier = 1024 * 1024 * 1024;
312                 understood = true;
313             }
314 
315             if (leftOver.equals("gB")) {
316                 multiplier = 1000 * 1000 * 1000;
317                 understood = true;
318             }
319 
320             if (leftOver.equalsIgnoreCase("bytes")) {
321                 multiplier = 1;
322                 understood = true;
323             }
324 
325             if (leftOver.equalsIgnoreCase("byte")) {
326                 multiplier = 1;
327                 understood = true;
328             }
329 
330             if (understood == false) {
331                 throw new InvalidPropertyException(key, stringValue, "file size (unit not understood)");
332             }
333         }
334 
335         long l = new Double(result.doubleValue() * multiplier).longValue();
336 
337         if (l != (result.doubleValue() * multiplier)) {
338             throw new InvalidPropertyException(key, stringValue, "file size (would have rounded value)");
339         }
340 
341         return l;
342 
343         //return Integer.parseInt(stringValue);
344     }
345 
346     /***
347      * Returns a duration.
348      * 
349      * @param key Key to search on.
350      * @return A duration (long) in milliseconds. Never a negative number.
351      */
352     protected final long getDurationInMs(String key) {
353         String stringValue = getString(key, "duration");
354 
355         return new DurationParser().getDurationInMs(key, stringValue);
356     }
357 
358     /***
359      * Checks if the value stored under the given key is a pattern that can be used to construct a
360      * MessageFormat. See {@link #testMessageFormat(String, String, Object[])}for more information.
361      * 
362      * @param key String Key that will be used to retrieve the messageformat pattern. Cannot be
363      *            <code>null</code>.
364      * @param testArguments Arguments to test the pattern with. Cannot be <code>null</code>.
365      * @return String MessageFormat pattern
366      */
367     protected final String getMessageFormatPattern(String key, Object[] testArguments) {
368         String value = getString(key, "MessageFormat string");
369 
370         testMessageFormat(key, value, testArguments);
371 
372         return value;
373     }
374 
375     /***
376      * <p>
377      * Verifies that the given pattern can be used to construct a MessageFormat. The testArguments
378      * array should contains values that represent reasonable values that will be used for this
379      * messageFormat.
380      * </p>
381      * <p>
382      * <b>Example: </b>In your properties file you have the following messageformat pattern:
383      * <code>files.description=There {0,choice,0#are no files|1#is one file|1 &lt;are {0,number,integer} files}</code>
384      * You could use the following calls in your getXxx() method to testMessageFormat to verify the
385      * messageformat deals with a variety of numbers correctly:
386      * 
387      * <pre><code>
388      * 
389      * public String getFilesDescription() {
390      *     String key = &quot;files.description&quot;;
391      *     String value = getString(key, &quot;MessageFormat string&quot;);
392      * 
393      *     testMessageFormat(key, value, new Object[] {new Integer(0)}); // tests the {0, choice, 0#} part
394      *     testMessageFormat(key, value, new Object[] {new Integer(1)}); // tests the {0, choice ... |1#} part
395      *     testMessageFormat(key, value, new Object[] {new Integer(10)});// tests the {0, choice ... &lt;1} part
396      *     return value;
397      * }
398      * </code>
399      * </pre>
400      * 
401      * The above code would for example make sure the following entry in a property-file is not
402      * accepted:
403      * <code>files.description=There {0,choice,0#are no files|1#is one file|1 &lt;are {0,invaliddatatype} files}</code>
404      * </p>
405      * 
406      * @param key Key that was used to retrieve the messageformat pattern.
407      * @param pattern The messageformat pattern to test.
408      * @param testArguments Arguments to test the pattern with.
409      */
410     protected final void testMessageFormat(String key, String pattern, Object[] testArguments) {
411         if (key == null) {
412             throw new NullPointerException("key cannot be null here.");
413         }
414         if (pattern == null) {
415             throw new NullPointerException("pattern cannot be null here.");
416         }
417         if (testArguments == null) {
418             throw new NullPointerException("testarguments cannot be null here.");
419         }
420         try {
421             MessageFormat.format(pattern, testArguments);
422         } catch (IllegalArgumentException e) {
423             StringBuffer testArgumentsDescription = new StringBuffer();
424             for (int i = 0; i < testArguments.length; i++) {
425                 Object object = testArguments[i];
426                 testArgumentsDescription.append("{" + i + "} = (");
427                 if (object != null) {
428                     testArgumentsDescription.append(object.toString() + ", " + object.getClass().getName() + ")");
429                 } else {
430                     testArgumentsDescription.append("<null>)");
431                 }
432                 if ((i + 1) < testArguments.length) {
433                     testArgumentsDescription.append(", ");
434                 }
435 
436             }
437             throw new InvalidPropertyException(key, pattern, "valid MessageFormat string for "
438                     + testArgumentsDescription);
439         }
440     }
441 
442     protected final Date getDate(String key) {
443         String stringValue = getString(key, "date");
444 
445         ParsePosition pp = new ParsePosition(0);
446         DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.US);
447         formatter.setLenient(false);
448 
449         Date result = null;
450 
451         /***
452          * Have an exception ready, just in case. Contains an example of how a date should be
453          * written.
454          */
455         InvalidPropertyException informativeException = new InvalidPropertyException(key, stringValue,
456                 ("date (such as " + formatter.format(new Date())) + ")");
457 
458         try {
459             result = formatter.parse(stringValue, pp);
460 
461             if (pp.getIndex() != stringValue.length()) {
462                 throw informativeException;
463             }
464         } catch (Exception e) {
465             throw informativeException;
466         }
467 
468         return result;
469     }
470 
471     /***
472      * Gets the value for the given key. The value is expected to be an String which subvalues are
473      * separated using comma's. Empty Strings will <b>not </b> be returned.
474      * 
475      * @param key Key to search on.
476      * @param itemsDescription Description of individual values, should be written in plural.
477      *            Example: "email adresses". Optional, can be <code>null</code>.
478      * @return A String array. Never <code>null</code>, might be empty (zero elements).
479      */
480     protected final String[] getStringArrayFromCommaString(String key, String itemsDescription) {
481         String valueDescription = "comma separated list";
482         if ((itemsDescription != null) && (!itemsDescription.equals(""))) {
483             valueDescription += " of " + itemsDescription;
484         }
485         String stringValue = getString(key, valueDescription);
486 
487         StringTokenizer st = new StringTokenizer(stringValue, ",");
488         List tokens = new ArrayList();
489 
490         while (st.hasMoreTokens()) {
491             String currentToken = st.nextToken();
492             currentToken = currentToken.trim();
493             if (!currentToken.equals("")) {
494                 tokens.add(currentToken);
495             }
496         }
497         return (String[]) tokens.toArray(new String[] {});
498     }
499 
500     /***
501      * Gets the value for the given key. The value is expected to be an String which subvalues are
502      * separated using comma's. Empty Strings will <b>not </b> be returned.
503      * 
504      * @param key Key to search on.
505      * @return A String array. Never <code>null</code>, might be empty (zero elements).
506      */
507     protected final String[] getStringArrayFromCommaString(String key) {
508         return getStringArrayFromCommaString(key, null);
509     }
510 
511     /***
512      * Gets the value for the given key. The value is expected to be an URL. A String is an URL if
513      * it can be read by the {@linkURL#URL(java.lang.String) constructor of the URL class}.
514      * 
515      * @param key Key to search on.
516      * @return An URL. Never <code>null</code>.
517      */
518     protected final URL getURL(String key) {
519         String stringValue = getString(key, "url");
520         URL url;
521 
522         try {
523             url = new URL(stringValue);
524         } catch (MalformedURLException e) {
525             throw new InvalidPropertyException(key, stringValue, "url");
526         }
527 
528         return url;
529     }
530 
531     /***
532      * Gets the value for the given key. The value is boolean, so <code>true</code> or
533      * <code>false</code>. The following texts are recognized: on, true, yes and 1, and off,
534      * false, no and 0. Any other value causes an exception.
535      * 
536      * @param key Key to search on.
537      * @return boolean
538      * @throws InvalidPropertyException If the value cannot be parsed as a boolean.
539      */
540     protected final boolean getBoolean(String key) {
541         String stringValue = getString(key, "boolean");
542         stringValue = stringValue.toLowerCase();
543 
544         if (stringValue.equals("on") || stringValue.equals("true") || stringValue.equals("yes")
545                 || stringValue.equals("1")) {
546             return true;
547         }
548 
549         if (stringValue.equals("off") || stringValue.equals("false") || stringValue.equals("no")
550                 || stringValue.equals("0")) {
551             return false;
552         }
553 
554         throw new InvalidPropertyException(key, stringValue, "boolean");
555     }
556 
557     protected final File getExistingReadableFile(String key) {
558         String value = getString(key, "filename");
559         return new FileParser().getExistingReadableFile(key, value);
560     }
561 
562     protected final File getWritableFile(String key) {
563         String value = getString(key, "filename");
564         return new FileParser().getWriteableFile(key, value);
565     }
566 
567     /***
568      * Indicates whether or not a value is given for this key.
569      * 
570      * @param key Cannot be <code>null</code>.
571      * @return <code>true</code> if a value has been given; <code>false</code> otherwise.
572      */
573     protected final boolean hasValue(String key) {
574         return (getValue(key) != null);
575     }
576 }
577 
578 /***
579  * Simple adapter around Properties, used to keep track of which keys in the properties are used
580  * during a given interval. The measuring starts when {@link #startInstrumenting()}is called, and
581  * stopped with {@link #stopInstrumenting()}.
582  * 
583  * @author Guus Bosman (Chess iT)
584  * @version $Revision: 1.1.1.1 $
585  */
586 
587 class InstrumentedProperties {
588 
589     private Properties properties;
590     private Set usedKeys = new HashSet();
591     private String lastUsedKey;
592     private String lastUsedValue;
593     private boolean instrumentingEnabled = false;
594 
595     InstrumentedProperties(Properties properties) {
596         this.properties = properties;
597     }
598 
599     public String getProperty(String key) {
600         String value = properties.getProperty(key);
601 
602         if (instrumentingEnabled) {
603             usedKeys.add(key);
604             lastUsedKey = key;
605             lastUsedValue = value;
606         }
607 
608         return value;
609     }
610 
611     /***
612      * Start a session to instrument the property file.
613      */
614     public void startInstrumenting() {
615         instrumentingEnabled = true;
616         usedKeys = new HashSet();
617     }
618 
619     /***
620      * Stops a session instrumenting the property file.
621      */
622     public void stopInstrumenting() {
623         instrumentingEnabled = false;
624     }
625 
626     public Set getUsedKeys() {
627         return usedKeys;
628     }
629 
630     /***
631      * Allows direct access to underlying properties. If you use this you are able to divert the
632      * instrumenting of course.
633      * 
634      * @return
635      */
636     public Properties getProperties() {
637         return properties;
638     }
639 
640     /***
641      * Returns the last used key (key in property file).
642      * 
643      * @return String. Might be <code>null</code> if no key has been used since the last call to
644      *         {@link #clearResult}.
645      */
646     public String getLastUsedKey() {
647         return lastUsedKey;
648     }
649 
650     /***
651      * Returns the last used value.
652      * 
653      * @return String. Might be <code>null</code> if a value was really <code>null</code> or if
654      *         no key has been used since the last call to {@link #clearResult}.
655      */
656     public String getLastUsedValue() {
657         return lastUsedValue;
658     }
659 
660     public void clearResult() {
661         lastUsedKey = null;
662         lastUsedValue = null;
663     }
664 }