View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17   */
18  package com.tomgibara.pronto.config.impl;
19  
20  import static java.util.logging.Level.WARNING;
21  import static java.util.logging.Level.FINER;
22  import static java.util.logging.Level.FINEST;
23  
24  import java.lang.reflect.Array;
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationHandler;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  import java.lang.reflect.Proxy;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.LinkedHashSet;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.logging.Logger;
38  
39  import com.tomgibara.pronto.config.Config;
40  import com.tomgibara.pronto.config.ConfigPolicy;
41  import com.tomgibara.pronto.config.ConfigSource;
42  import com.tomgibara.pronto.config.DefaultConfigPolicy;
43  import com.tomgibara.pronto.config.ProntoConfigException;
44  import com.tomgibara.pronto.util.Arguments;
45  import com.tomgibara.pronto.util.Classes;
46  import com.tomgibara.pronto.util.Objects;
47  import com.tomgibara.pronto.util.Strings;
48  
49  /**
50   * Standard implementation of Config interface.
51   * 
52   * @author Tom Gibara
53   * 
54   */
55  
56  // TODO consider adding support for fields which are interfaces, or concrete
57  // classes (constructors or setters)
58  class ConfigImpl implements Config {
59  
60      // statics
61  
62      /**
63       * The policy to be used if none (or null) is specified.
64       */
65  
66      private static final ConfigPolicy DEFAULT_POLICY = new DefaultConfigPolicy();
67  
68      /**
69       * The log for exceptions and other log worthy events.
70       */
71  
72      private static final Logger LOGGER = Logger.getLogger(ConfigImpl.class.getPackage().getName());
73  
74      // fields
75  
76      /**
77       * A lock synchronizing access to the source and properties fields.
78       */
79      private final Object lock = new Object();
80  
81      /**
82       * A key factory which is used for generating keys during population of the
83       * properties map. Access to this field requires the lock.
84       */
85      private final ConfigKeyFactory factory = new ConfigKeyFactory();
86  
87      /**
88       * Maps parameterizations to the proxies they generate. Access to this field
89       * requires the lock.
90       */
91      private final HashMap<Params, Object> proxies;
92  
93      /**
94       * The class loader to be used for creating proxies and the values returned
95       * from them.
96       */
97      private final ClassLoader classLoader;
98  
99      /**
100      * The object which provides the raw properties.
101      */
102     private final ConfigSource source;
103 
104     /**
105      * The policy for this config.
106      */
107     private ConfigPolicy policy;
108 
109     /**
110      * Maps keys to the properties last obtained from the source. Access to this
111      * field requires the lock.
112      */
113     private HashMap<ConfigKey, String> properties;
114 
115     /**
116      * The time at which the current properties were declared to have been
117      * modified by the source. Access to this field requires the lock.
118      */
119     private long lastModified;
120 
121     /**
122      * The time at which the source was last checked for changes in its
123      * properties. Access to this field requires the lock.
124      */
125     private long lastCheck;
126 
127     // constructors
128 
129     /**
130      * A constructor which clones an existing config.
131      * 
132      * @param that
133      *            the config to be cloned
134      */
135 
136     private ConfigImpl(final ConfigImpl that) {
137         this.policy = that.policy;
138         this.source = that.source;
139         this.classLoader = that.classLoader;
140         synchronized (that.lock) {
141             this.properties = that.properties;
142             this.proxies = new HashMap<Params, Object>(that.proxies);
143             this.lastModified = that.lastModified;
144             this.lastCheck = that.lastCheck;
145         }
146     }
147 
148     /**
149      * A constructor which clones an existing config and specifies a different
150      * class loader to use.
151      * 
152      * @param that
153      *            the config to be cloned
154      * @param classLoader
155      *            the classLoader to be used this object, may be null
156      */
157 
158     private ConfigImpl(final ConfigImpl that, final ClassLoader classLoader) {
159         this.policy = that.policy;
160         this.source = that.source;
161         this.classLoader = classLoader;
162         synchronized (that.lock) {
163             this.properties = that.properties;
164             this.proxies = new HashMap<Params, Object>(); // lose their cache
165             // - possibly
166             // different class
167             // loader made them
168             this.lastModified = that.lastModified;
169             this.lastCheck = that.lastCheck;
170         }
171     }
172 
173     /**
174      * Constructs a new config.
175      * 
176      * @param source
177      *            the source of configuration properties, not null
178      * @param classLoader
179      *            the class loader for creating interface implementations and
180      *            value objects, may be null
181      * 
182      * @throws ProntoConfigException
183      *             if the properties could not be accessed and the policy is to
184      *             throw exceptions
185      */
186 
187     public ConfigImpl(final ConfigSource source, final ClassLoader classLoader) throws ProntoConfigException {
188         Arguments.notNull(source, "source");
189         this.source = source;
190         this.classLoader = classLoader;
191         policy = DEFAULT_POLICY;
192         proxies = new HashMap<Params, Object>();
193         lastModified = -1;
194         lastCheck = -1;
195         readProperties();
196     }
197 
198     // accessors
199 
200     public void setPolicy(ConfigPolicy policy) {
201         if (policy == null) policy = DEFAULT_POLICY;
202         this.policy = policy;
203     }
204 
205     public ConfigPolicy getPolicy() {
206         return policy;
207     }
208 
209     public ClassLoader getClassLoader() {
210         return classLoader;
211     }
212 
213     public ConfigSource getSource() {
214         return source;
215     }
216 
217     // methods
218 
219     public <T> T adaptSettings(String domain, boolean inherit, Class<T> iface) throws IllegalArgumentException {
220         Arguments.notNull(iface, "iface");
221         if (!iface.isInterface()) throw new IllegalArgumentException(String.format("iface not an interface: %s", iface));
222 
223         synchronized (lock) {
224             ConfigKey key = domain == null ? null : factory.newKey(domain);
225             return checkCreateImpl(key, inherit, iface);
226         }
227     }
228 
229     public String getProperty(String name) throws ProntoConfigException {
230         synchronized (lock) {
231             checkProperties();
232             // optimization: factory will not create a new key if the key is
233             // unknown
234             ConfigKey key = factory.newKey(name);
235             String value = key == null ? null : properties.get(key);
236             if (LOGGER.isLoggable(FINEST)) LOGGER.log(FINEST, "{0}={1}", new Object[] { name, value });
237             return value;
238         }
239     }
240 
241     public ConfigImpl forClassLoader(ClassLoader classLoader) {
242         return classLoader == this.classLoader ? this : new ConfigImpl(this, classLoader);
243     }
244 
245     // object methods
246 
247     public ConfigImpl clone() {
248         return new ConfigImpl(this);
249     }
250 
251     // private methods
252 
253     /**
254      * If the minimum read period has elapsed, checks whether the properties
255      * have changed. The lock must be held when calling this method.
256      * 
257      * @throws ProntoConfigException
258      *             if the properties could not be accessed and the policy is to
259      *             throw exceptions
260      */
261 
262     private void checkProperties() throws ProntoConfigException {
263         long now = System.currentTimeMillis();
264         if (LOGGER.isLoggable(FINEST)) LOGGER.log(FINEST, "checking properties");
265         if (lastCheck == -1 || now - lastCheck > policy.getMinReadPeriod()) {
266             lastCheck = now;
267             readProperties();
268         }
269     }
270 
271     /**
272      * Obtains the timestamp and, if desired by the policy, new properties from
273      * the source.
274      * 
275      * The lock must be held when calling this method.
276      * 
277      * @throws ProntoConfigException
278      *             if the properties could not be accessed and the policy is to
279      *             throw exceptions
280      */
281 
282     private void readProperties() throws ProntoConfigException {
283         if (LOGGER.isLoggable(FINER)) LOGGER.log(FINER, "reading properties");
284         // initially set the timestamp to the last modified time, just in case
285         // an exception's raised
286         long timestamp;
287         try {
288             timestamp = source.lastModified();
289             if (timestamp < 0L) throw new IllegalArgumentException(String.format(
290                     "Negative timestamp returned from source: %d", timestamp));
291         } catch (RuntimeException e) {
292             if (policy.isExceptionLogged() && LOGGER.isLoggable(WARNING)) LOGGER.log(WARNING, null, e);
293             if (policy.isExceptionThrown()) throw new ProntoConfigException(e);
294             timestamp = lastModified;
295         }
296         if (lastModified == -1L || policy.isTimestampNewer(timestamp, lastModified)) {
297             Map<String, String> tmp = null;
298             try {
299                 tmp = source.getProperties();
300                 if (tmp == null) throw new NullPointerException("Configuration source returned null properties.");
301             } catch (RuntimeException e) {
302                 if (policy.isExceptionLogged() && LOGGER.isLoggable(WARNING)) LOGGER.log(WARNING, null, e);
303                 if (policy.isExceptionThrown()) throw new ProntoConfigException(e);
304                 tmp = Collections.emptyMap();
305             }
306             createProperties(tmp);
307             lastModified = timestamp;
308         }
309     }
310 
311     /**
312      * Updates the properties and factory fields by remapping the source
313      * properties using config keys. The lock must be held when calling this
314      * method.
315      * 
316      * @param sourceProps
317      *            the properties supplied by the source
318      * @throws ProntoConfigException
319      *             if any property supplied by the source was not a string
320      */
321 
322     private void createProperties(final Map<String, String> sourceProps) throws ProntoConfigException {
323         factory.reset();
324         properties = new HashMap<ConfigKey, String>();
325         for (Map.Entry<String, String> entry : sourceProps.entrySet()) {
326             try {
327                 ConfigKey key = factory.newKey(entry.getKey());
328                 properties.put(key, entry.getValue());
329             } catch (ClassCastException e) { // due to erasure, we may have
330                 // been passed a non-string
331                 // object
332                 throw new ProntoConfigException(e);
333             }
334         }
335         factory.setReadOnly(true);
336     }
337 
338     /**
339      * Returns an implementation which matches the specified parameters, by
340      * first looking in a cache for a matching implementation. The lock must be
341      * held when calling this method.
342      * 
343      * @param <T>
344      *            the interface type being satisfied
345      * @param domain
346      *            the domain of properties, may be null
347      * @param inherit
348      *            true iff the properties should be inherited by the interface
349      *            implementation
350      * @param clss
351      *            the interface class to be satsified
352      * @return a satisfaction of the clss interface
353      * 
354      */
355 
356     @SuppressWarnings("unchecked")
357     private <T> T checkCreateImpl(final ConfigKey domain, final boolean inherit, final Class<T> clss) {
358         Params<T> params = new Params<T>(domain, inherit, clss);
359         T impl = (T) proxies.get(params);
360         if (impl == null) {
361             impl = createImpl(params);
362             proxies.put(params, impl);
363         }
364         return impl;
365     }
366 
367     /**
368      * Creates a dynamic interface proxy for the supplied parameters. The lock
369      * must be held when calling this method.
370      * 
371      * @param <T>
372      *            the interface being satisfied
373      * @param params
374      *            the parameters which define the mapping to configuration
375      *            properties
376      * @return an implementation of the interface specified by the parameters
377      */
378 
379     @SuppressWarnings("unchecked")
380     private <T> T createImpl(final Params<T> params) {
381         return (T) Proxy.newProxyInstance(getActiveClassLoader(), new Class[] {params.iface}, new SettingsHandler(
382                 params));
383     }
384 
385     /**
386      * Return the class loader which will be used to create class instances.
387      * This is the first non-null of: the specified classLoader, the context
388      * class loader and this class's class loader
389      * 
390      * @return the class loader which will be used to create class instances.
391      */
392 
393     private ClassLoader getActiveClassLoader() {
394         if (classLoader != null) return classLoader;
395         ClassLoader tmp = Thread.currentThread().getContextClassLoader();
396         if (tmp != null) return tmp;
397         return getClass().getClassLoader();
398     }
399 
400     // inner classes
401 
402     /**
403      * This class collects together the parameters which control the creation of
404      * a mapping between a java interface and live configuration parameters.
405      * 
406      * @param <T>
407      *            the interface satisfied by a mapping generated with these
408      *            parameters
409      * 
410      * @author Tom Gibara
411      */
412 
413     private static final class Params<T> {
414 
415         /**
416          * The domain from which properties will be drawn to satisfy the
417          * interface.
418          */
419         private final ConfigKey domain;
420 
421         /**
422          * Indicates whether the interface will inherit properties for its
423          * satisfaction.
424          */
425         private final boolean inherit;
426 
427         /**
428          * The interface which is to be implemented using configuration values.
429          */
430         private final Class<T> iface;
431 
432         /**
433          * Constructs a new Params object.
434          * 
435          * @param domain
436          *            the domain of properties
437          * @param inherit
438          *            whether properties are inherited
439          * @param iface
440          *            the interface to implement
441          */
442 
443         private Params(final ConfigKey domain, final boolean inherit, final Class<T> iface) {
444             this.domain = domain;
445             this.inherit = inherit;
446             this.iface = iface;
447         }
448 
449         /**
450          * Two Params instances are equal if all their fields are equal.
451          * 
452          * @param obj
453          *            any object, possibly null
454          * 
455          * @return true if the fields are equal
456          */
457 
458         public boolean equals(final Object obj) {
459             if (obj == this) return true;
460             if (!(obj instanceof Params)) return false;
461             Params that = (Params) obj;
462             if (this.iface != that.iface) return false;
463             if (this.inherit != that.inherit) return false;
464             if (Objects.notEqual(this.domain, that.domain)) return false;
465             return true;
466         }
467 
468         /**
469          * @return a hash code consistent with equality
470          */
471 
472         public int hashCode() {
473             return Objects.hashCode(domain) ^ iface.hashCode() ^ Boolean.valueOf(inherit).hashCode();
474         }
475 
476         /**
477          * A string representation of the parameters, useful for identifying
478          * proxies.
479          * 
480          * @return a human readable string representation of this object
481          */
482 
483         public String toString() {
484             return String.format("[Params domain=%s, inherit=%b, iface=%s]", domain, inherit, iface);
485         }
486     }
487 
488     /**
489      * This class is delegated to via Java's reflective proxy class to satisfy
490      * calls to accessor methods on the interface defined by its
491      * parameterization.
492      * 
493      * @author Tom Gibara
494      * 
495      */
496 
497     private final class SettingsHandler implements InvocationHandler {
498 
499         /**
500          * Maps method names to the object values to be returned for calls to
501          * those method.
502          */
503         private final HashMap<String, Object> objects = new HashMap<String, Object>(); // caching
504         // config
505         // objects
506 
507         /**
508          * The parameters which define the mapping imposed by this handler.
509          */
510         private final Params params;
511 
512         /**
513          * A reference to the config's properties so that values can be searched
514          * for without causing undue contention on the config's lock.
515          */
516         private HashMap<ConfigKey, String> properties;
517 
518         /**
519          * The time at which this handler's copy of the properties map was last
520          * modifed. This value is used to 'enliven' this handler's map as
521          * necessary.
522          */
523         private long lastModified = -1L;
524 
525         /**
526          * Constructs a new parameterized handler.
527          * 
528          * @param params
529          *            the handler's operational parameters, not null
530          */
531 
532         private SettingsHandler(final Params params) {
533             Arguments.notNull(params, "params");
534             this.params = params;
535         }
536 
537         /**
538          * @param proxy
539          *            used only to implement equals
540          * @param method
541          *            the method being invoked
542          * @param args
543          *            the arguments to the method - should be empty
544          * 
545          * @throws Throwable
546          *             if any exception is raised during the execution of the
547          *             method
548          * 
549          * @return the value for an accessor, or the appropriate value for an
550          *         object method
551          */
552 
553         public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
554             final String methodName = method.getName();
555             // deal with object methods first
556             if (methodName.equals("equals")) return Boolean.valueOf(args[0] == proxy);
557             if (methodName.equals("hashCode")) return new Integer(hashCode());
558             if (methodName.equals("toString")) return String.format("[%s dynamically implemented for: %s]", method
559                     .getClass(), params);
560             // identify the method return type
561             final Class clss = method.getReturnType();
562             // optimization - return immediately if return type is void, there's
563             // nothing to do
564             if (clss == Void.TYPE) return null;
565 
566             synchronized (lock) {
567                 // check for reload
568                 checkProperties();
569                 if (ConfigImpl.this.lastModified != this.lastModified) {
570                     if (LOGGER.isLoggable(FINER)) LOGGER.log(FINER, "{0} updated properties", this);
571                     // properties have changed so our object cache is invalid
572                     objects.clear();
573                     // get our own reference to the config's properties so we
574                     // don't need to lock
575                     this.properties = ConfigImpl.this.properties;
576                     // record the last modified date so we know if it happens
577                     // again
578                     this.lastModified = ConfigImpl.this.lastModified;
579                 }
580             }
581 
582             synchronized (this) {
583                 try {
584                     final Object value;
585                     // check for already created value
586                     if (objects.containsKey(methodName)) {
587                         value = objects.get(methodName);
588                     } else {
589                         // deduce the property name
590                         final String name = policy.propertyFromMethod(methodName);
591                         // create proper key value
592                         // can't use factory since key may not be pre-existing
593                         // but still valid due to inheritence
594                         // using factory only in case of non-inheritence adds
595                         // complexity for very little apparent gain?
596                         final ConfigKey key = new ConfigKey(params.domain, name);
597                         String propertyValue = lookupProperty(key);
598                         if (propertyValue == null) { // default value case
599                             value = policy.defaultForClass(clss);
600                             if (clss.isPrimitive() && value == null) throw new NullPointerException(String.format(
601                                     "Null value supplied for primitive: %s", clss));
602                             if (policy.isDefaultsCached()) objects.put(methodName, value);
603                         } else { // convert string to object
604                             Class tmp = clss.isPrimitive() ? Classes.classForPrimitive(clss) : clss;
605                             value = convertValue(propertyValue, tmp, methodName);
606                             if (classLoader != null || policy.isCachingEager()) objects.put(methodName, value);
607                         }
608                     }
609                     if (LOGGER.isLoggable(FINEST)) LOGGER.log(FINEST, "{0}()={1}", new Object[] {methodName, value});
610                     return value;
611                 } catch (RuntimeException e) {
612                     // deal with exception
613                     if (policy.isExceptionLogged() && LOGGER.isLoggable(WARNING)) LOGGER.log(WARNING, null, e);
614                     if (policy.isExceptionThrown()) throw new ProntoConfigException(e);
615                     Object value = policy.defaultForClass(clss);
616                     // at this point we know that the policy is not to pass
617                     // exceptions back to the client code
618                     // so we automatically limit ourselves to logging any
619                     // mistake by the policy
620                     if (clss.isPrimitive() && value == null && LOGGER.isLoggable(WARNING)) LOGGER.log(WARNING, null,
621                             new ProntoConfigException(String.format(
622                                     "Null value supplied for primitive %s in call to method %s", clss, method)));
623                     return value;
624                 }
625             }
626         }
627 
628         /**
629          * Coerces a property value in the form of a string, into the a value
630          * with the correct type to be returned by a specific interface method.
631          * 
632          * @param propertyValue
633          *            the value of the property as a string, not null
634          * @param clss
635          *            the class, not null and not primitive
636          * @param methodName
637          *            the name of the method being invoked on the proxy (used
638          *            for error reporting purposes)
639          * @return the converted value
640          * 
641          * @throws ProntoConfigException
642          *             if an exception occurs converting the property value into
643          *             an object
644          */
645 
646         @SuppressWarnings("unchecked")
647         private Object convertValue(final String propertyValue, final Class clss, final String methodName)
648                 throws ProntoConfigException {
649             Exception ex;
650             try {
651                 // we have a cachable value
652                 if (clss == String.class) {
653                     // they want a string - no processing to do
654                     return propertyValue;
655                 }
656 
657                 if (Enum.class.isAssignableFrom(clss)) {
658                     return Enum.valueOf(clss, propertyValue);
659                 }
660 
661                 if (clss == Set.class || clss == Collection.class) {
662                     return Collections.unmodifiableSet(new LinkedHashSet<String>(Strings.splitCommas(propertyValue)));
663                 }
664 
665                 if (clss == List.class) {
666                     return Collections.unmodifiableList(Strings.splitCommas(propertyValue));
667                 }
668 
669                 if (clss == Map.class) {
670                     return Collections.unmodifiableMap(Strings.parseProperties(propertyValue));
671                 }
672 
673                 if (clss == Class.class) {
674                     return getActiveClassLoader().loadClass(propertyValue);
675                 }
676 
677                 if (clss.isArray()) {
678                     Class c = clss.getComponentType();
679                     List<String> strings = Strings.splitCommas(propertyValue);
680                     Object[] a = (Object[]) Array.newInstance(c, strings.size());
681                     int i = 0;
682                     for (String string : strings) {
683                         a[i++] = convertValue(string, c, methodName);
684                     }
685                     return a;
686                 }
687 
688                 // attempt to create value from string contstructor
689                 Constructor cons = clss.getConstructor(new Class[] {String.class});
690                 return cons.newInstance(new Object[] {propertyValue});
691 
692             } catch (SecurityException e) {
693                 ex = e;
694             } catch (ClassNotFoundException e) {
695                 ex = e;
696             } catch (NoSuchMethodException e) {
697                 ex = e;
698             } catch (IllegalArgumentException e) {
699                 ex = e;
700             } catch (InstantiationException e) {
701                 ex = e;
702             } catch (IllegalAccessException e) {
703                 ex = e;
704             } catch (InvocationTargetException e) {
705                 ex = e;
706             }
707             throw new ProntoConfigException(String.format("Error converting value for method %s: %s", methodName, ex
708                     .getMessage()), ex);
709         }
710 
711         // object methods
712 
713         /**
714          * @return a human readable representation of this object
715          */
716         @Override
717         public String toString() {
718             return String.format("[SettingsHandler for %s]", params);
719         }
720 
721         // private methods
722 
723         /**
724          * Looks for a property which matches the supplied key, recursing up the
725          * inherited keys (if necessary) until a value is found or there are no
726          * more inherited keys.
727          * 
728          * @param key
729          *            the key to the value sought
730          * @return the property for that key, or null
731          */
732         private String lookupProperty(final ConfigKey key) {
733             if (properties.containsKey(key)) return properties.get(key);
734             else if (!params.inherit) return null;
735             else {
736                 ConfigKey next = getInheritedKey(key);
737                 return next == null ? null : lookupProperty(next);
738             }
739         }
740 
741         /**
742          * Returns the key from which a specified key inherits its value, or
743          * null if the key has no parent.
744          * 
745          * @param key
746          *            the key for which the inherited key is sought
747          * @return the key from which the specified key inherits its value, or
748          *         null
749          */
750 
751         private ConfigKey getInheritedKey(final ConfigKey key) {
752             ConfigKey parent = key.getParent();
753             if (parent == null) return null;
754             return new ConfigKey(parent.getParent(), key.getName());
755         }
756 
757     }
758 }