1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
57
58 class ConfigImpl implements Config {
59
60
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
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
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>();
165
166
167
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
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
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
233
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
246
247 public ConfigImpl clone() {
248 return new ConfigImpl(this);
249 }
250
251
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
285
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) {
330
331
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
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>();
504
505
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
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
561 final Class clss = method.getReturnType();
562
563
564 if (clss == Void.TYPE) return null;
565
566 synchronized (lock) {
567
568 checkProperties();
569 if (ConfigImpl.this.lastModified != this.lastModified) {
570 if (LOGGER.isLoggable(FINER)) LOGGER.log(FINER, "{0} updated properties", this);
571
572 objects.clear();
573
574
575 this.properties = ConfigImpl.this.properties;
576
577
578 this.lastModified = ConfigImpl.this.lastModified;
579 }
580 }
581
582 synchronized (this) {
583 try {
584 final Object value;
585
586 if (objects.containsKey(methodName)) {
587 value = objects.get(methodName);
588 } else {
589
590 final String name = policy.propertyFromMethod(methodName);
591
592
593
594
595
596 final ConfigKey key = new ConfigKey(params.domain, name);
597 String propertyValue = lookupProperty(key);
598 if (propertyValue == null) {
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 {
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
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
617
618
619
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
652 if (clss == String.class) {
653
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
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
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
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 }