001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.options;
018
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.ArrayList;
023import java.util.Iterator;
024import java.util.Properties;
025
026import org.conqat.lib.commons.enums.EnumUtils;
027
028/**
029 * This class offers a safe and flexible interface to Java properties files.
030 * 
031 * Property files must follow the format for Java properties files. See Javadoc
032 * of {@link java.util.Properties} for details.
033 * 
034 * @author Florian Deissenboeck
035 * @author Axel Gerster
036 * 
037 * @see java.util.Properties
038 */
039public class Options {
040
041        /**
042         * Returned by <code>countValues</code> when trying to count values of a
043         * non-present option.
044         */
045        public static final int OPTION_NOT_PRESENT = -1;
046
047        /**
048         * This implementation is back by a <code>Properties</code> object.
049         */
050        private Properties properties;
051
052        /**
053         * Construct a new <code>Option</code> object holding now options. Use methods
054         * {@link #init(String)}or {@link #setOption(String, String)}to store options.
055         */
056        public Options() {
057                init();
058        }
059
060        /**
061         * This initalizes the <code>Options</code> object by reading a properties file.
062         * 
063         * @param filename
064         *            full-qualified name of the properties file
065         * @throws IOException
066         *             Thrown if an I/O problem is encountered while reading properties
067         *             file.
068         */
069        public void init(String filename) throws IOException {
070                properties = new Properties();
071                InputStream inputStream = new FileInputStream(filename);
072                properties.load(inputStream);
073                inputStream.close();
074        }
075
076        /**
077         * Init empty <code>Options</code> object. Existing options are cleared.
078         */
079        public void init() {
080                properties = new Properties();
081        }
082
083        /**
084         * Sets and option. Setting an already existing option overwrites current value.
085         * 
086         * @param option
087         *            name of the option
088         * @param value
089         *            option's value, must have same format as defined in the properties
090         *            file
091         * @return <code>true</code> if option was alreay present, <code>false</code>
092         *         otherwise
093         */
094        public boolean setOption(String option, String value) {
095                boolean overriden = hasOption(option);
096                properties.setProperty(option, value);
097                return overriden;
098        }
099
100        /**
101         * Gets the value for a specified option.
102         * 
103         * @param option
104         *            the name of the option
105         * @return the option's value, if the option is not present or has a
106         *         <code>null</code> value <code>null</code> is returned. If the option
107         *         has a space separated value list, the whole list is returned. Use
108         *         {@link #getValues(String)}to access single values.
109         */
110        public String getValue(String option) {
111                if (!hasOption(option)) {
112                        return null;
113                }
114                String value = properties.getProperty(option);
115
116                if ("".equals(value)) {
117                        return null;
118                }
119                return value;
120        }
121
122        /**
123         * Return the value for a specified option or a default value if option is not
124         * present.
125         * 
126         * @param option
127         *            name of the option
128         * @param defaultValue
129         *            default value to use, if option is not present
130         * @return the option's value or the default value
131         */
132
133        public String getValue(String option, String defaultValue) {
134                if (hasOption(option)) {
135                        return getValue(option);
136                }
137                return defaultValue;
138        }
139
140        /**
141         * Returns the space separated value of an option as array. A option might have
142         * more the one space separated value. This method returns them as an array. To
143         * allow values containing spaces use double quotes.
144         * <p>
145         * <i>Example: </i> For the following line in a properties file
146         * <p>
147         * <code>option=value1 value2 "value 3" value4</code>
148         * <p>
149         * the method returns this array <br/>
150         * 
151         * <code><br/>
152         * a[0] = &quot;value1&quot;<br/>
153         * a[1] = &quot;value2&quot;<br/>
154         * a[2] = &quot;value 3&quot;<br/>
155         * a[3] = &quot;value4&quot;<br/>
156         * </code>
157         * 
158         * @param option
159         *            name of the option
160         * @return the array as desribed above
161         */
162        public String[] getValues(String option) {
163                if (!hasOption(option)) {
164                        return null;
165                }
166                String values = properties.getProperty(option);
167
168                if ("".equals(values)) {
169                        return null;
170                }
171
172                return parse(values);
173        }
174
175        /**
176         * Checks if the specified option is present and has a boolean value. Boolean
177         * values are <code>true</code>,<code>false</code>, <code>yes</code> and
178         * <code>no</code>
179         * 
180         * @param option
181         *            name of the option
182         * @return if the option is present and has a boolean value <code>true</code> is
183         *         returned, otherwise <code>false</code>
184         */
185        public boolean hasBooleanValue(String option) {
186                if (!hasValue(option)) {
187                        return false;
188                }
189
190                String value = getValue(option);
191
192                return checkTrue(value) || checkFalse(value);
193        }
194
195        /**
196         * Get the value for an option as <code>boolean</code>.
197         * 
198         * @param option
199         *            name of the option
200         * @return the value of this option
201         * @throws ValueConversionException
202         *             if the option doesn't have a boolean value. Use
203         *             {@link #hasBooleanValue(String)}method or default value enabled
204         *             version {@link #getBooleanValue(String, boolean)}of this method
205         *             to avoid conversion problems.
206         */
207        public boolean getBooleanValue(String option) throws ValueConversionException {
208                if (!hasBooleanValue(option)) {
209                        throw new ValueConversionException(option);
210                }
211
212                String value = getValue(option);
213
214                if (checkTrue(value)) {
215                        return true;
216                }
217
218                return false;
219        }
220
221        /**
222         * Get the value for an option as instance of an enumeration. Enumeration names
223         * are matched in non case-sensitive way. Dashes in values are replaced by
224         * underscores.
225         * <p>
226         * Typical usage is:
227         * 
228         * <pre>
229         * Colors color = options.getEnumValue(&quot;enum1&quot;, Colors.class);
230         * </pre>
231         * 
232         * where <code>Colors</code> is an enumeration.
233         * 
234         * @param <T>
235         *            the enumeration
236         * @param option
237         *            the name of the option
238         * @param enumType
239         *            the enumeration type
240         * @return the enumeration entry
241         * @throws ValueConversionException
242         *             if the option doesn't have a value of the specified enumeration.
243         *             Use {@link #hasEnumValue(String, Class)}method or default value
244         *             enabled version {@link #getEnumValue(String, Enum, Class)}of this
245         *             method to avoid conversion problems.
246         */
247        public <T extends Enum<T>> T getEnumValue(String option, Class<T> enumType) throws ValueConversionException {
248                if (!hasEnumValue(option, enumType)) {
249                        throw new ValueConversionException(option);
250                }
251
252                String value = getValue(option);
253
254                return EnumUtils.valueOfIgnoreCase(enumType, normalizeEnumConstantName(value));
255        }
256
257        /**
258         * Same as {@link #getEnumValue(String, Class)} but allows to specify default
259         * value.
260         * 
261         * @param <T>
262         *            the enumeration
263         * @param option
264         *            the name of the option
265         * @param enumType
266         *            the enumeration type
267         * @return the enumeration entry
268         * 
269         */
270        public <T extends Enum<T>> T getEnumValue(String option, T defaultValue, Class<T> enumType) {
271
272                try {
273                        return getEnumValue(option, enumType);
274                } catch (ValueConversionException e) {
275                        return defaultValue;
276                }
277
278        }
279
280        /**
281         * Checks if the specified option is present and has a legal value.
282         * 
283         * @param option
284         *            name of the option
285         * @return if the option is present and has a legal value <code>true</code> is
286         *         returned, otherwise <code>false</code>
287         */
288        public <T extends Enum<T>> boolean hasEnumValue(String option, Class<T> enumType) {
289                if (!hasValue(option)) {
290                        return false;
291                }
292
293                String value = getValue(option);
294
295                return checkEnum(value, enumType);
296        }
297
298        /**
299         * Check if value describe an an element of the enumeration (case-insenstive
300         * match).
301         * 
302         */
303        private static <T extends Enum<T>> boolean checkEnum(String value, Class<T> enumType) {
304                T t = EnumUtils.valueOfIgnoreCase(enumType, normalizeEnumConstantName(value));
305                if (t == null) {
306                        return false;
307                }
308                return true;
309        }
310
311        /**
312         * Get the value for an option as <code>int</code>.
313         * 
314         * @param option
315         *            name of the option
316         * @return the value of this option
317         * @throws ValueConversionException
318         *             if the option doesn't have a <code>int</code> value. Use
319         *             {@link #hasIntValue(String)}method or default value enabled
320         *             version {@link #getIntValue(String, int)}of this method to avoid
321         *             conversion problems.
322         */
323        public int getIntValue(String option) throws ValueConversionException {
324                if (!hasIntValue(option)) {
325                        throw new ValueConversionException(option);
326                }
327
328                String value = getValue(option);
329
330                return Integer.parseInt(value);
331        }
332
333        /**
334         * Checks if the specified option is present and has a <code>int</code> value.
335         * 
336         * @param option
337         *            name of the option
338         * @return if the option is present and has a <code>int</code> value
339         *         <code>true</code> is returned, otherwise <code>false</code>
340         */
341        public boolean hasIntValue(String option) {
342                if (!hasValue(option)) {
343                        return false;
344                }
345
346                String value = getValue(option);
347
348                return checkInt(value);
349        }
350
351        /**
352         * Same as {@link #getBooleanValue(String)}but allows to specify a default
353         * value.
354         * 
355         * @param option
356         *            name of the option
357         * @param defaultValue
358         *            default value
359         * @return return the value of the option if option is present and has a boolean
360         *         value, otherwise the default value is returned
361         */
362        public boolean getBooleanValue(String option, boolean defaultValue) {
363
364                try {
365                        return getBooleanValue(option);
366                } catch (ValueConversionException e) {
367                        return defaultValue;
368                }
369
370        }
371
372        /**
373         * Same as {@link #getIntValue(String)}but allows to specify a default value.
374         * 
375         * @param option
376         *            name of the option
377         * @param defaultValue
378         *            default value
379         * @return return the value of the option if option is present and has an
380         *         integer value, otherwise the default value is returned
381         */
382        public int getIntValue(String option, int defaultValue) {
383                try {
384                        return getIntValue(option);
385                } catch (ValueConversionException e) {
386                        return defaultValue;
387                }
388        }
389
390        /**
391         * Checks if a given string represent an integer.
392         * 
393         * @param value
394         *            - the string to check
395         * @return <code>true</code> if the string represents an integer,
396         *         <code>false</code> otherwise
397         */
398        private static boolean checkInt(String value) {
399                try {
400                        Integer.parseInt(value);
401                } catch (NumberFormatException ex) {
402                        return false;
403                }
404                return true;
405        }
406
407        /**
408         * Checks if the string is a boolean literal with value <code>false</code>.
409         * Literals <code>false</code> and <code>no</code> are allowed.
410         * 
411         * @param value
412         *            the string to check
413         * @return <code>true</code> if the string represents a booleean literal with
414         *         value <code>false</code>,<code>false</code> otherwise
415         */
416        private static boolean checkFalse(String value) {
417                value = value.trim();
418
419                if (value.toLowerCase().equals("false")) {
420                        return true;
421                }
422                if (value.toLowerCase().equals("no")) {
423                        return true;
424                }
425
426                return false;
427        }
428
429        /**
430         * Checks if the string is a boolean literal with value <code>true</code>.
431         * Literals <code>true</code> and <code>yes</code> are allowed.
432         * 
433         * @param value
434         *            the string to check
435         * @return <code>true</code> if the string represents a booleean literal with
436         *         value <code>true</code>,<code>false</code> otherwise
437         */
438        private static boolean checkTrue(String value) {
439                value = value.trim();
440
441                if (value.toLowerCase().equals("true")) {
442                        return true;
443                }
444                if (value.toLowerCase().equals("yes")) {
445                        return true;
446                }
447
448                return false;
449        }
450
451        /**
452         * Parses a space separated value list. To use values with spaces, use double
453         * quotes.
454         * 
455         * @param string
456         *            the value list to parse
457         * @return an array containing the values, quotes are omitted
458         */
459        private static String[] parse(String string) {
460                string = string.trim();
461                int length = string.length();
462                char[] content = new char[length];
463                string.getChars(0, length, content, 0);
464
465                ArrayList<String> list = new ArrayList<String>();
466
467                int i = 0;
468
469                int lastPos = 0;
470                boolean inQM = false;
471                boolean inToken = false;
472
473                while (i < length) {
474                        switch (content[i]) {
475                        case ' ':
476                        case '\t':
477                                if (inToken && !inQM) {
478                                        // parameter found
479                                        String parameter = string.substring(lastPos, i).trim();
480                                        parameter = parameter.replaceAll("\"", "");
481                                        list.add(parameter);
482                                        lastPos = i;
483                                }
484
485                                inToken = false;
486                                // lastPos++;
487                                break;
488                        case '\"':
489                                inQM = !inQM;
490                                break;
491                        default:
492                                inToken = true;
493                        }
494                        i++;
495                }
496
497                String parameter = string.substring(lastPos, i).trim();
498                parameter = parameter.replaceAll("\"", "");
499                list.add(parameter);
500
501                String[] result = new String[list.size()];
502                list.toArray(result);
503
504                return result;
505        }
506
507        /**
508         * Checks if a specified option is present.
509         * 
510         * @param option
511         *            name of the option
512         * @return <code>true</code> if option is present, <code>false</code> otherwise
513         */
514        public boolean hasOption(String option) {
515                return !(properties.getProperty(option) == null);
516        }
517
518        /**
519         * Checks if specified option has a value.
520         * 
521         * @param option
522         *            name of the option
523         * @return <code>true</code> if option is present and has a value,
524         *         <code>false</code> otherwise (even if option is present but doesn't
525         *         have a value)
526         */
527        public boolean hasValue(String option) {
528                return countValues(option) > 0;
529        }
530
531        /**
532         * Count the space separated values of an option. Double quotes are taken into
533         * account.
534         * 
535         * @param option
536         *            name of the option
537         * @return value count
538         */
539        public int countValues(String option) {
540                if (!hasOption(option)) {
541                        return OPTION_NOT_PRESENT;
542                }
543
544                String[] values = getValues(option);
545
546                if (values == null) {
547                        return 0;
548                }
549
550                return values.length;
551        }
552
553        /**
554         * Returns a list with key-value-pairs as string.
555         * 
556         * @return key-value-pairs as string
557         */
558        @Override
559        public String toString() {
560                StringBuffer buffer = new StringBuffer();
561                Iterator<Object> it = properties.keySet().iterator();
562                while (it.hasNext()) {
563                        String key = (String) it.next();
564                        String value = properties.getProperty(key);
565                        buffer.append(key + " = " + value);
566                        if (it.hasNext()) {
567                                buffer.append(System.getProperty("line.separator"));
568                        }
569                }
570                return buffer.toString();
571        }
572
573        /**
574         * Exception objects of this class are possibly returned by
575         * {@link Options#getBooleanValue(String)}and
576         * {@link Options#getIntValue(String)}, if corresponding options don't have a
577         * boolean respectively integer value.
578         * 
579         */
580        @SuppressWarnings("serial")
581        public static class ValueConversionException extends Exception {
582
583                /**
584                 * Construct new conversion exception.
585                 * 
586                 * @param option
587                 *            name of the option causing the exception
588                 */
589                public ValueConversionException(String option) {
590                        super("Option: " + option);
591                }
592        }
593
594        /**
595         * Get the value for an option as <code>float</code>.
596         * 
597         * @param option
598         *            name of the option
599         * @return the value of this option
600         * @throws ValueConversionException
601         *             if the option doesn't have a float value.
602         */
603        public float getFloatValue(String option) throws ValueConversionException {
604                if (!hasFloatValue(option)) {
605                        throw new ValueConversionException(option);
606                }
607
608                String value = getValue(option);
609
610                return Float.parseFloat(value);
611        }
612
613        /**
614         * Checks if the specified option is present and has a float value.
615         * 
616         * 
617         * @param option
618         *            name of the option
619         * @return if the option is present and has a float value <code>true</code> is
620         *         returned, otherwise <code>false</code>
621         */
622        public boolean hasFloatValue(String option) {
623                if (!hasValue(option)) {
624                        return false;
625                }
626
627                String value = getValue(option);
628
629                return checkFloat(value);
630        }
631
632        /**
633         * Checks if a string contains a float.
634         */
635        private static boolean checkFloat(String value) {
636                try {
637                        Float.parseFloat(value);
638                } catch (NumberFormatException ex) {
639                        return false;
640                }
641                return true;
642        }
643
644        /**
645         * Normalize enum constant name. This replaces all dashes with underscores.
646         */
647        private static String normalizeEnumConstantName(String constantName) {
648                return constantName.replaceAll("-", "_");
649        }
650}