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
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
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
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
195
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
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 <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 = "files.description";
391 * String value = getString(key, "MessageFormat string");
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 ... <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 <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 }