001package com.thetransactioncompany.util;
002
003
004import java.net.MalformedURLException;
005import java.net.URI;
006import java.net.URISyntaxException;
007import java.net.URL;
008import java.util.*;
009
010
011/**
012 * Provides typed retrieval of {@link java.util.Properties} as {@code boolean}, 
013 * {@code int}, {@code long}, {@code float}, {@code double}, 
014 * {@link java.lang.String}, {@link java.net.URL}, {@link java.net.URI} or
015 * {@code enum} values.
016 *
017 * @author Vladimir Dzhuvinov
018 */
019public class PropertyRetriever {
020
021        
022        /** 
023         * The properties to parse.
024         */
025        private final Properties props;
026
027
028        /**
029         * {@code true} if system property override has been enabled,
030         * {@code false} if it's disabled.
031         */
032        private final boolean enableSysPropOverride;
033
034
035        /**
036         * Creates a new retriever for the specified properties. System
037         * property override is disabled.
038         *
039         * @param props The properties. Must not be {@code null}.
040         */
041        public PropertyRetriever(final Properties props) {
042        
043                this(props, false);
044        }
045
046
047        /**
048         * Creates a new retriever for the specified properties.
049         *
050         * @param props                 The properties. Must not be
051         *                              {@code null}.
052         * @param enableSysPropOverride If {@code true} if system properties
053         *                              with the same names are present they
054         *                              will override the specified properties,
055         *                              {@code false} to disable this
056         *                              behaviour.
057         */
058        public PropertyRetriever(final Properties props, final boolean enableSysPropOverride) {
059
060                this.props = new Properties();
061                this.props.putAll(props);
062
063                this.enableSysPropOverride = enableSysPropOverride;
064                if (enableSysPropOverride) {
065                        // Overwrite
066                        this.props.putAll(System.getProperties());
067                }
068        }
069
070
071        /**
072         * Gets the system property override setting.
073         *
074         * @return {@code true} if system property override has been enabled,
075         *         {@code false} if it's disabled.
076         */
077        public boolean systemPropertyOverrideIsEnabled() {
078
079                return enableSysPropOverride;
080        }
081        
082        
083        /**
084         * Retrieves a boolean value.
085         *
086         * @param key The property name.
087         *
088         * @return The property as a boolean value.
089         *
090         * @throws PropertyParseException On a missing or invalid property.
091         */
092        public boolean getBoolean(final String key)
093                throws PropertyParseException {
094        
095                String value = props.getProperty(key);
096                
097                if (value == null)
098                        throw new PropertyParseException("Missing property", key);
099                
100                if (value.equalsIgnoreCase("true"))
101                        return true;
102                
103                else if (value.equalsIgnoreCase("false"))
104                        return false;
105                
106                else
107                        throw new PropertyParseException("Invalid boolean property", key, value);
108        }
109        
110        
111        /**
112         * Retrieves an optional boolean value.
113         *
114         * @param key The property name.
115         * @param def The default value if the property value is undefined or
116         *            empty.
117         *
118         * @return The property as a boolean.
119         *
120         * @throws PropertyParseException On an invalid property.
121         */
122        public boolean getOptBoolean(final String key, final boolean def)
123                throws PropertyParseException {
124        
125                String value = props.getProperty(key);
126                
127                if (value == null || value.trim().isEmpty())
128                        return def;
129                
130                if (value.equalsIgnoreCase("true"))
131                        return true;
132                
133                if (value.equalsIgnoreCase("false"))
134                        return false;
135                
136                throw new PropertyParseException("Invalid boolean property", key, value);
137        }
138        
139        
140        /**
141         * Retrieves an integer value.
142         *
143         * @param key The property name.
144         *
145         * @return The property as an integer.
146         *
147         * @throws PropertyParseException On a missing or invalid property.
148         */
149        public int getInt(final String key)
150                throws PropertyParseException {
151        
152                String value = props.getProperty(key);
153                
154                if (value == null)
155                        throw new PropertyParseException("Missing property", key);
156                
157                try {
158                        return Integer.parseInt(value);
159                
160                } catch (NumberFormatException e) {
161
162                        throw new PropertyParseException("Invalid int property", key, value);
163                }
164        }
165        
166        
167        /**
168         * Retrieves an optional integer value.
169         *
170         * @param key The property name.
171         * @param def The default value if the property value is undefined or
172         *            empty.
173         *
174         * @return The property as an integer.
175         *
176         * @throws PropertyParseException On an invalid property.
177         */
178        public int getOptInt(final String key, final int def)
179                throws PropertyParseException {
180        
181                String value = props.getProperty(key);
182                
183                if (value == null || value.trim().isEmpty())
184                        return def;
185                
186                try {
187                        return Integer.parseInt(value);
188                
189                } catch (NumberFormatException e) {
190
191                        throw new PropertyParseException("Invalid int property", key);
192                }
193        }
194        
195        
196        /**
197         * Retrieves a long value.
198         *
199         * @param key The property name.
200         *
201         * @return The property as a long.
202         *
203         * @throws PropertyParseException On a missing or invalid property.
204         */
205        public long getLong(final String key)
206                throws PropertyParseException {
207        
208                String value = props.getProperty(key);
209                
210                if (value == null)
211                        throw new PropertyParseException("Missing property", key);
212                
213                try {
214                        return Long.parseLong(value);
215                
216                } catch (NumberFormatException e) {
217
218                        throw new PropertyParseException("Invalid long property", key, value);
219                }
220        }
221        
222        
223        /**
224         * Retrieves an optional long value.
225         *
226         * @param key The property name.
227         * @param def The default value if the property value is undefined or
228         *            empty.
229         *
230         * @return The property as a long.
231         *
232         * @throws PropertyParseException On an invalid property.
233         */
234        public long getOptLong(final String key, final long def)
235                throws PropertyParseException {
236        
237                String value = props.getProperty(key);
238                
239                if (value == null || value.trim().isEmpty())
240                        return def;
241                
242                try {
243                        return Long.parseLong(value);
244                
245                } catch (NumberFormatException e) {
246
247                        throw new PropertyParseException("Invalid long property", key, value);
248                }
249        }
250        
251        
252        /**
253         * Retrieves a float value.
254         *
255         * @param key The property name.
256         *
257         * @return The property as a float.
258         *
259         * @throws PropertyParseException On a missing or invalid property.
260         */
261        public float getFloat(final String key)
262                throws PropertyParseException {
263        
264                String value = props.getProperty(key);
265                
266                if (value == null)
267                        throw new PropertyParseException("Missing property", key);
268                
269                try {
270                        return Float.parseFloat(value);
271                
272                } catch (NumberFormatException e) {
273
274                        throw new PropertyParseException("Invalid float property", key, value);
275                }
276        }
277        
278        
279        /**
280         * Retrieves an optional float value.
281         *
282         * @param key The property name.
283         * @param def The default value if the property value is undefined or
284         *            empty.
285         *
286         * @return The property as a float.
287         *
288         * @throws PropertyParseException On an invalid property.
289         */
290        public float getOptFloat(final String key, final float def)
291                throws PropertyParseException {
292        
293                String value = props.getProperty(key);
294                
295                if (value == null || value.trim().isEmpty())
296                        return def;
297                
298                try {
299                        return Float.parseFloat(value);
300                
301                } catch (NumberFormatException e) {
302
303                        throw new PropertyParseException("Invalid float property", key, value);
304                }
305        }
306        
307        
308        /**
309         * Retrieves a double value.
310         *
311         * @param key The property name.
312         *
313         * @return The property as a double.
314         *
315         * @throws PropertyParseException On a missing or invalid property.
316         */
317        public double getDouble(final String key)
318                throws PropertyParseException {
319        
320                String value = props.getProperty(key);
321                
322                if (value == null)
323                        throw new PropertyParseException("Missing property", key);
324                
325                try {
326                        return Double.parseDouble(value);
327                
328                } catch (NumberFormatException e) {
329
330                        throw new PropertyParseException("Invalid double property", key, value);
331                }
332        }
333        
334        
335        /**
336         * Retrieves an optional double value.
337         *
338         * @param key The property name.
339         * @param def The default value if the property value is undefined or
340         *            empty.
341         *
342         * @return The property as a double.
343         *
344         * @throws PropertyParseException On an invalid property.
345         */
346        public double getOptDouble(final String key, final double def)
347                throws PropertyParseException {
348        
349                String value = props.getProperty(key);
350                
351                if (value == null || value.trim().isEmpty())
352                        return def;
353                
354                try {
355                        return Double.parseDouble(value);
356                
357                } catch (NumberFormatException e) {
358
359                        throw new PropertyParseException("Invalid double property", key, value);
360                }
361        }
362        
363        
364        /**
365         * Retrieves a string value.
366         *
367         * @param key The property name.
368         *
369         * @return The property as a string.
370         *
371         * @throws PropertyParseException On a missing or invalid property.
372         */
373        public String getString(final String key)
374                throws PropertyParseException {
375        
376                String value = props.getProperty(key);
377                
378                if (value == null)
379                        throw new PropertyParseException("Missing property", key);
380                
381                return value;
382        }
383        
384        
385        /**
386         * Retrieves an optional string value.
387         *
388         * @param key The property name.
389         * @param def The default value if the property value is undefined or
390         *            empty.
391         *
392         * @return The property as a string.
393         *
394         * @throws PropertyParseException On an invalid property.
395         */
396        public String getOptString(final String key, final String def)
397                throws PropertyParseException {
398        
399                String value = props.getProperty(key);
400                
401                if (value == null || value.trim().isEmpty())
402                        return def;
403                
404                return value;
405        }
406        
407        
408        /**
409         * Retrieves an enumerated string value. String case is ignored during
410         * comparison.
411         *
412         * @param key   The property name.
413         * @param enums A string array defining the acceptable values.
414         *
415         * @return The property as a string.
416         *
417         * @throws PropertyParseException On a missing or invalid property.
418         */
419        public String getEnumString(final String key, final String[] enums)
420                throws PropertyParseException {
421        
422                String value = props.getProperty(key);
423                
424                if (value == null)
425                        throw new PropertyParseException("Missing property", key);
426                
427                for (String en: enums) {
428                        
429                        if (en.equalsIgnoreCase(value))
430                                return value;
431                }
432                        
433                throw new PropertyParseException("Invalid enum string property", key, value);
434        }
435        
436        
437        /**
438         * Retrieves an enumerated string value. String case is ignored during
439         * comparison.
440         *
441         * @param key   The property name.
442         * @param enums A string array defining the acceptable values.
443         * @param def   The default value if the property value is undefined or
444         *              empty.
445         *
446         * @return The property as a string.
447         *
448         * @throws PropertyParseException On an invalid property.
449         */
450        public String getOptEnumString(final String key, final String[] enums, final String def)
451                throws PropertyParseException {
452        
453                String value = props.getProperty(key);
454                
455                if (value == null || value.trim().isEmpty())
456                        return def;
457                
458                for (String en: enums) {
459                        
460                        if (en.equalsIgnoreCase(value))
461                                return value;
462                }
463                        
464                throw new PropertyParseException("Invalid enum string property", key, value);
465        }
466        
467        
468        /**
469         * Retrieves an enumerated constant. String case is ignored during
470         * comparison.
471         *
472         * @param key       The property name.
473         * @param enumClass The enumeration class specifying the acceptable
474         *                  values.
475         *
476         * @return The matching enumerated constant.
477         *
478         * @throws PropertyParseException On a missing or invalid property.
479         */
480        public <T extends Enum<T>> T getEnum(final String key, final Class<T> enumClass)
481                throws PropertyParseException {
482                
483                String value = props.getProperty(key);
484                
485                if (value == null)
486                        throw new PropertyParseException("Missing property", key);
487                        
488                for (T en: enumClass.getEnumConstants()) {
489                
490                        if (en.toString().equalsIgnoreCase(value))
491                                return en;
492                }
493                
494                // No match? -> raise exception
495                throw new PropertyParseException("Invalid enum property", key, value);
496        }
497        
498        
499        /**
500         * Retrieves an optional enumerated constant. String case is ignored
501         * during comparison.
502         *
503         * @param key       The property name.
504         * @param enumClass The enumeration class specifying the acceptable
505         *                  values.
506         * @param def       The default value if the property value is 
507         *                  undefined or empty.
508         *
509         * @return The matching enumerated constant.
510         *
511         * @throws PropertyParseException On a missing or invalid property.
512         */
513        public <T extends Enum<T>> T getOptEnum(final String key, final Class<T> enumClass, final T def)
514                throws PropertyParseException {
515                
516                String value = props.getProperty(key);
517                
518                if (value == null || value.trim().isEmpty())
519                        return def;
520                        
521                for (T en: enumClass.getEnumConstants()) {
522                
523                        if (en.toString().equalsIgnoreCase(value))
524                                return en;
525                }
526                
527                // No match? -> raise exception
528                throw new PropertyParseException("Invalid enum property", key, value);
529        }
530        
531        
532        /**
533         * Retrieves a string list consisting of one or more comma / space
534         * separated items.
535         *
536         * @param key The property name.
537         *
538         * @return The string list with at least one item.
539         *
540         * @throws PropertyParseException On a missing or empty property.
541         */
542        public List<String> getStringList(final String key)
543                throws PropertyParseException {
544                
545                String s = getString(key);
546                
547                StringTokenizer st = new StringTokenizer(s, ", ");
548                List<String> out = new LinkedList<>();
549                while (st.hasMoreElements()) {
550                        out.add(st.nextToken());
551                }
552                
553                if (out.isEmpty()) {
554                        throw new PropertyParseException("Empty string list", key, s);
555                }
556                
557                return out;
558        }
559        
560        
561        /**
562         * Retrieves an optional string list consisting of zero or more comma /
563         * space separated items.
564         *
565         * @param key The property name.
566         * @param def The default value if the property value is undefined or
567         *            empty.
568         *
569         * @return The string list with zero or more items.
570         *
571         * @throws PropertyParseException On a invalid property.
572         */
573        public List<String> getOptStringList(final String key, final List<String> def)
574                throws PropertyParseException {
575                
576                String s = getOptString(key, null);
577                
578                if (s == null) {
579                        return def;
580                }
581                
582                StringTokenizer st = new StringTokenizer(s, ", ");
583                List<String> out = new LinkedList<>();
584                while (st.hasMoreElements()) {
585                        out.add(st.nextToken());
586                }
587                
588                if (out.isEmpty()) {
589                        return def;
590                }
591                
592                return out;
593        }
594        
595        
596        /**
597         * Retrieves a string list consisting of one or more comma / space
598         * separated items from a enumeration.
599         *
600         * @param key   The property name.
601         * @param enums A string array defining the acceptable values.
602         *
603         * @return The string list with at least one item.
604         *
605         * @throws PropertyParseException On a missing or empty property.
606         */
607        public List<String> getEnumStringList(final String key, final String[] enums)
608                throws PropertyParseException {
609                
610                return getEnumStringList(key, Arrays.asList(enums));
611        }
612        
613        
614        /**
615         * Retrieves an optional string list consisting of zero or more comma /
616         * space separated items from a enumeration.
617         *
618         * @param key   The property name.
619         * @param enums A string array defining the acceptable values.
620         * @param def   The default value if the property value is undefined or
621         *              empty.
622         *
623         * @return The string list with zero or more items.
624         *
625         * @throws PropertyParseException On a invalid property.
626         */
627        public List<String> getOptEnumStringList(final String key, final String[] enums, final List<String> def)
628                throws PropertyParseException {
629                
630                return getOptEnumStringList(key, Arrays.asList(enums), def);
631        }
632        
633        
634        /**
635         * Retrieves a string list consisting of one or more comma / space
636         * separated items from a enumeration.
637         *
638         * @param key   The property name.
639         * @param enums A string list defining the acceptable values.
640         *
641         * @return The string list with at least one item.
642         *
643         * @throws PropertyParseException On a missing or empty property.
644         */
645        public List<String> getEnumStringList(final String key, final List<String> enums)
646                throws PropertyParseException {
647                
648                List<String> out = getStringList(key);
649                
650                for (String v: out) {
651                        if (! enums.contains(v)) {
652                                throw new PropertyParseException("Invalid enum string list property: " + v, key, props.getProperty(key));
653                        }
654                }
655                
656                return out;
657        }
658        
659        
660        /**
661         * Retrieves an optional string list consisting of zero or more comma /
662         * space separated items from a enumeration.
663         *
664         * @param key   The property name.
665         * @param enums A string list defining the acceptable values.
666         * @param def   The default value if the property value is undefined or
667         *              empty.
668         *
669         * @return The string list with zero or more items.
670         *
671         * @throws PropertyParseException On a invalid property.
672         */
673        public List<String> getOptEnumStringList(final String key, final List<String> enums, final List<String> def)
674                throws PropertyParseException {
675                
676                List<String> out = getOptStringList(key, def);
677                
678                for (String v: out) {
679                        if (! enums.contains(v)) {
680                                throw new PropertyParseException("Invalid enum string list property: " + v, key, props.getProperty(key));
681                        }
682                }
683                
684                return out;
685        }
686        
687        
688        /**
689         * Retrieves a string list from multiple properties sharing a common
690         * prefix.
691         *
692         * @param commonPrefix The common property name prefix.
693         *
694         * @return The string list with at least one item.
695         *
696         * @throws PropertyParseException On a missing or empty property.
697         */
698        public List<String> getStringListMulti(final String commonPrefix)
699                throws PropertyParseException {
700                
701                List<String> stringList = getOptStringListMulti(commonPrefix, null);
702                
703                if (stringList == null) {
704                        throw new PropertyParseException("Missing properties", commonPrefix + "*");
705                }
706                
707                return stringList;
708        }
709        
710        
711        /**
712         * Retrieves an optional string list from multiple properties sharing a
713         * common prefix.
714         *
715         * @param commonPrefix The common property name prefix.
716         * @param def          The default value if no non-blank properties
717         *                     are found.
718         *
719         * @return The string list with at least one item or the default value.
720         */
721        public List<String> getOptStringListMulti(final String commonPrefix, final List<String> def) {
722                
723                Properties filteredProps = PropertyFilter.filterWithPrefix(commonPrefix, props);
724                List<String> stringList = new LinkedList<>();
725                for (String name: filteredProps.stringPropertyNames()) {
726                        String value = filteredProps.getProperty(name);
727                        if (value == null || value.trim().isEmpty()) {
728                                continue;
729                        }
730                        stringList.add(value);
731                }
732                
733                if (stringList.isEmpty()) {
734                        return def;
735                } else {
736                        return Collections.unmodifiableList(stringList);
737                }
738        }
739        
740        
741        /**
742         * Retrieves a URI value.
743         *
744         * @param key The property name.
745         *
746         * @return The property as a URI.
747         *
748         * @throws PropertyParseException On a missing or invalid property.
749         */
750        public URI getURI(final String key)
751                throws PropertyParseException {
752                
753                String value = props.getProperty(key);
754                
755                if (value == null)
756                        throw new PropertyParseException("Missing property", key);
757                
758                try {
759                        return new URI(value);
760                        
761                } catch (URISyntaxException e) {
762                        
763                        throw new PropertyParseException("Invalid URI property: " + e.getMessage(), key, value);
764                }
765        }
766        
767        
768        /**
769         * Retrieves an optional URI value.
770         *
771         * @param key The property name.
772         * @param def The default value if the property value is undefined or
773         *            empty.
774         *
775         * @return The property as a URL.
776         *
777         * @throws PropertyParseException On an invalid property.
778         */
779        public URI getOptURI(final String key, final URI def)
780                throws PropertyParseException {
781                
782                String value = props.getProperty(key);
783                
784                if (value == null || value.trim().isEmpty())
785                        return def;
786                
787                try {
788                        return new URI(value);
789                        
790                } catch (URISyntaxException e) {
791                        
792                        throw new PropertyParseException("Invalid URI property: " + e.getMessage(), key, value);
793                }
794        }
795
796
797        /**
798         * Retrieves a URL value.
799         *
800         * @param key The property name.
801         *
802         * @return The property as a URL.
803         *
804         * @throws PropertyParseException On a missing or invalid property.
805         */
806        public URL getURL(final String key)
807                throws PropertyParseException {
808        
809                String value = props.getProperty(key);
810                
811                if (value == null)
812                        throw new PropertyParseException("Missing property", key);
813                
814                try {
815                        return new URL(value);
816
817                } catch (MalformedURLException e) {
818
819                        throw new PropertyParseException("Invalid URL property: " + e.getMessage(), key, value);
820                }
821        }
822        
823        
824        /**
825         * Retrieves an optional URL value.
826         *
827         * @param key The property name.
828         * @param def The default value if the property value is undefined or
829         *            empty.
830         *
831         * @return The property as a URL.
832         *
833         * @throws PropertyParseException On an invalid property.
834         */
835        public URL getOptURL(final String key, final URL def)
836                throws PropertyParseException {
837        
838                String value = props.getProperty(key);
839                
840                if (value == null || value.trim().isEmpty())
841                        return def;
842                
843                try {
844                        return new URL(value);
845
846                } catch (MalformedURLException e) {
847
848                        throw new PropertyParseException("Invalid URL property: " + e.getMessage(), key, value);
849                }
850        }
851}