/* * The MIT License * * Copyright 2013 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.giulius; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Stage; import com.google.inject.TypeLiteral; import com.google.inject.matcher.Matchers; import com.google.inject.name.Named; import com.google.inject.name.Names; import com.google.inject.spi.ProvisionListener; import com.google.inject.spi.ProvisionListener.ProvisionInvocation; import com.google.inject.util.Providers; import com.mastfrog.settings.Settings; import com.mastfrog.settings.SettingsBuilder; import com.mastfrog.guicy.annotations.Defaults; import com.mastfrog.guicy.annotations.Namespace; import com.mastfrog.guicy.annotations.Value; import com.mastfrog.giulius.annotations.processors.NamespaceAnnotationProcessor; import com.mastfrog.util.ConfigurationError; import com.mastfrog.settings.MutableSettings; import com.mastfrog.util.Checks; import com.mastfrog.util.Exceptions; import com.mastfrog.util.Streams; import com.mastfrog.util.thread.ProtectedThreadLocal; import com.mastfrog.util.thread.QuietAutoCloseable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.annotation.Annotation; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; /** * A wrapper around Guice's injector which enforces a few things such as how * configuration information is loaded, and binds @Named injections to * values from system properties, environment variables, any default values * specified using the @Defaults annotation, any * com/mastfrom/defaults.properties files on the classpath, * <p/> * Typically you create a Dependencies once on startup, and then get whatever * bootsrap objects you need to start the application. It is possible to * completely isolate things by using multiple Dependencies, but this is usually * an indication of doing something wrong. * * @author Tim Boudreau */ public final class Dependencies { /** * System property which determines Guice stage & result of * isProductionMode(). System property overrides same value in the default * namespace settings (string value is "productionMode"). */ public static final String SYSTEM_PROP_PRODUCTION_MODE = "productionMode"; private final Map<String, Settings> settings = new HashMap<>(); private final Set<SettingsBindings> settingsBindings; private final List<Module> modules = new LinkedList<>(); private volatile Injector injector; public Dependencies(Module... modules) throws IOException { this(SettingsBuilder.createDefault().build(), modules); } public String toString() { StringBuilder sb = new StringBuilder(super.toString()).append(" {\n"); for (Iterator<Module> it = modules.iterator(); it.hasNext();) { sb.append(it.next()); if (it.hasNext()) { sb.append(", "); } } sb.append("\n"); for (Map.Entry<String, Settings> e : settings.entrySet()) { sb.append(" ").append(e.getKey()).append("=").append(e.getValue()).append('\n'); } return sb.append("\n}").toString(); } /** * Create a Dependencies with the classpath contents used to look up * properties files and named properties, and the default settings. * * @param modules * @return A dependencies * @throws Error if an IOException occurred */ public static Dependencies create(Module... modules) { try { return new Dependencies(modules); } catch (IOException ioe) { //we're early enough in startup to bail throw new ConfigurationError(ioe); } } /** * Create a dependency using the passed Configuration, bypassing any loading * from the classpath or elsewhere. * * @param configuration The configuration which is the source for Named * injections * @param modules A set of modules */ public Dependencies(Settings configuration, Module... modules) { this(Collections.singletonMap(Namespace.DEFAULT, configuration), EnumSet.allOf(SettingsBindings.class), modules); } Dependencies(Map<String, Settings> settings, Set<SettingsBindings> settingsBindings, Module... modules) { this.settings.putAll(settings); if (!this.settings.containsKey(Namespace.DEFAULT)) { try { //need at least an empty one for defaults this.settings.put(Namespace.DEFAULT, new SettingsBuilder().build()); } catch (IOException ex) { throw new ConfigurationError(ex); } } this.modules.add(createBindings()); this.modules.addAll(Arrays.asList(modules)); this.settingsBindings = settingsBindings; } public static DependenciesBuilder builder() { return new DependenciesBuilder(); } /** * Get the injector, creating it if necessary. This is the typical * entry-point for starting an application, e.g.: * <pre> * Dependencies deps = new Dependencies(new Module1(), new Module2()); * Server server = deps.getInjector().getInstance(Server.class); * server.start(); * </pre> * * * @return */ public Injector getInjector() { if (injector == null) { if (getStage() == Stage.PRODUCTION) { if (injector == null) { injector = Guice.createInjector(getStage(), modules); } } else { synchronized (this) { try (ThreadLocalCounter c = ctr.enter()) { if (c.get() > 1) { throw new IllegalStateException("Reentrant call to getInjector() on one thread." + "Something is asking for an instance of " + "Dependencies or the Injector while the injector is being initialized - probably " + "an eager singleton. The injector may not be created twice."); } if (injector == null) { injector = Guice.createInjector(getStage(), modules); } } } } } return injector; } private final ThreadLocalCounter ctr = new ThreadLocalCounter(); private static final class ThreadLocalCounter extends QuietAutoCloseable { private final ThreadLocal<Integer> local = new ThreadLocal<>(); public int get() { Integer result = local.get(); return result == null ? 0 : result; } ThreadLocalCounter enter() { Integer val = local.get(); if (val == null) { val = 0; } val++; local.set(val); return this; } @Override public void close() { Integer val = local.get(); assert val != null; val--; if (val == 0) { local.remove(); } else { local.set(val); } } } /** * Get an instance of the passed type; throws an exception if nothing is * bound. * * @param <T> A type * @param type The type * @return An instance of that class */ public <T> T getInstance(Class<T> type) { return getInjector().getInstance(type); } /** * Get an instance of the passed key; throws an exception if nothing is * bound. * * @param <T> A type * @param type The type * @return An instance of that class */ public <T> T getInstance(Key<T> key) { return getInjector().getInstance(key); } /** * Triggers running shutdown hooks; for use with unit tests, servlet * unloading, etc. */ public void shutdown() { try { reg.runShutdownHooks(); } finally { for (Dependencies d : others) { d.shutdown(); } } } /** * For use with frameworks that insist on creating the injector * * @param config * @return */ public static Module createBindings(Settings config) throws IOException { return new Dependencies(config).createBindings(); } /** * Create a module which binds @Named properties appropriately. * <p/> * This method is only necessary when using frameworks such as * GuiceRestEasy, which insist on creating the injector. * * @return */ protected Module createBindings() { return new DependenciesModule(); } /** * whether the productionMode system property (-DproductionMode=true) or * settings (productionMode=true) is set. If it is, will be configured for * production mode, meaning that singletons will be initialized eagerly, and * that modules might configure themselves to configure e.g. for using a * real mail server rather than a mock as you would probably do in * development mode. See {@link Stage#PRODUCTION}. If not provided, this * class will be bootstrapped in {@link Stage#DEVELOPMENT} mode. * * @return true if the system should start up in production mode, false * otherwise */ public boolean isProductionMode() { return isProductionMode(settings.get(Namespace.DEFAULT)); } /** * Whether the productionMode system property (-DproductionMode=true) or * settings (productionMode=true) is set. If it is, will be configured for * production mode, meaning that singletons will be initialized eagerly, and * that modules might configure themselves to configure e.g. for using a * real mail server rather than a mock as you would probably do in * development mode. See {@link Stage#PRODUCTION}. If not provided, this * class will be bootstrapped in {@link Stage#DEVELOPMENT} mode. * * @param settings settings (may contain productionMode) * @return true if the system should start up in production mode, false * otherwise */ public static boolean isProductionMode(Settings settings) { try { return settings.getBoolean(SYSTEM_PROP_PRODUCTION_MODE, false) || Boolean.getBoolean(SYSTEM_PROP_PRODUCTION_MODE); } catch (NoSuchElementException e) { //UGH! return false; } } public Settings getSettings() { Settings result = settings.get(Namespace.DEFAULT); if (result == null) { //May want to just get is production mode out of here. result = settings.get(settings.keySet().iterator().next()); } return result; } public Settings getSettings(String namespace) { Checks.notNull("namespace", namespace); return settings.get(namespace); } /** * Get the Guice stage, as determined by isProductionMode() * * @return */ public Stage getStage() { isIDEMode(); return isProductionMode() ? Stage.PRODUCTION : Stage.DEVELOPMENT; } public static final String IDE_MODE_SYSTEM_PROPERTY = "in.ide"; /** * Used by the tests.guice framework to bypass execution of long running * tests if this system property is set. To use, configure your IDE to pass * -Din.ide=true to Maven * * @return True if the system property is set */ public static boolean isIDEMode() { return Boolean.getBoolean(IDE_MODE_SYSTEM_PROPERTY); } private final ShutdownHookRegistry reg = ShutdownHookRegistry.get(); public void autoShutdownRefresh(SettingsBuilder sb) { reg.add(sb.onShutdownRunnable()); } /** * Same as getInjector().injectMembers(arg) * * @param arg */ public void injectMembers(Object arg) { getInjector().injectMembers(arg); } private static class NamespaceImpl implements Namespace { private final String name; public NamespaceImpl(String name) { this.name = name; } @Override public String value() { return name; } @Override public Class<? extends Annotation> annotationType() { return Namespace.class; } @Override public boolean equals(Object o) { return o instanceof Namespace && name.equals(((Namespace) o).value()); } @Override public int hashCode() { // This is specified in java.lang.Annotation. int result = (127 * "value".hashCode()) ^ name.hashCode(); return result; } } private static final class ValueImpl implements Value { private final Namespace ns; private final String key; ValueImpl(String key, String ns) { this.ns = new NamespaceImpl(ns); this.key = key; } @Override public String toString() { return ns + "::" + key; } @Override public String value() { return key; } @Override public Namespace namespace() { return ns; } @Override public Class<? extends Annotation> annotationType() { return Value.class; } public int hashCode() { // This is specified in java.lang.Annotation. int result = (127 * "value".hashCode()) ^ key.hashCode(); result += (127 * "namespace".hashCode()) ^ ns.hashCode(); return result; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ValueImpl other = (ValueImpl) obj; if (!Objects.equals(this.ns, other.ns)) { return false; } if (!Objects.equals(this.key, other.key)) { return false; } return true; } } private final ProtectedThreadLocal<TypeLiteral<?>> currentType = new ProtectedThreadLocal<>(); private final ProtectedThreadLocal<TypeLiteral<?>> prevType = new ProtectedThreadLocal<>(); private final Set<Dependencies> others = Collections.<Dependencies>synchronizedSet(new HashSet<Dependencies>()); public final Dependencies alsoShutdown(Dependencies other) { if (other == this) { throw new IllegalArgumentException("add to self"); } others.add(other); return this; } private final class DependenciesModule extends AbstractModule { @Override protected void configure() { try { Binder binder = binder(); bind(Dependencies.class).toInstance(Dependencies.this); bind(ShutdownHookRegistry.class).toInstance(reg); Set<String> knownNamespaces = loadNamespaceListsFromClasspath(); log("Loaded namespaces " + knownNamespaces); knownNamespaces.addAll(settings.keySet()); knownNamespaces.add(Namespace.DEFAULT); Stage stage = getStage(); DeploymentMode mode; switch (stage) { case PRODUCTION: mode = DeploymentMode.PRODUCTION; break; default: mode = DeploymentMode.DEVELOPMENT; } bind(DeploymentMode.class).toInstance(mode); Set<String> allKeys = new HashSet<>(); for (String namespace : knownNamespaces) { Settings s = settings.get(namespace); if (s == null) { s = SettingsBuilder.forNamespace(namespace).addGeneratedDefaultsFromClasspath().addDefaultsFromClasspath().build(); settings.put(namespace, s); } allKeys.addAll(s.allKeys()); } Provider<Settings> namespacedSettings = new NamespacedSettingsProvider(Dependencies.this); for (String k : allKeys) { Named n = Names.named(k); PropertyProvider p = new PropertyProvider(k, namespacedSettings); for (SettingsBindings type : settingsBindings) { switch (type) { case INT: binder.bind(Key.get(Integer.class, n)).toProvider(new IntProvider(p)); break; case STRING: binder.bind(Key.get(String.class, n)).toProvider(p); break; case LONG: binder.bind(Key.get(Long.class, n)).toProvider(new LongProvider(p)); break; case BOOLEAN: binder.bind(Key.get(Boolean.class, n)).toProvider(new BooleanProvider(p)); break; case BYTE: binder.bind(Key.get(Byte.class, n)).toProvider(new ByteProvider(p)); break; case CHARACTER: binder.bind(Key.get(Character.class, n)).toProvider(new CharacterProvider(p)); break; case DOUBLE: binder.bind(Key.get(Double.class, n)).toProvider(new DoubleProvider(p)); break; case FLOAT: binder.bind(Key.get(Float.class, n)).toProvider(new FloatProvider(p)); break; case SHORT: binder.bind(Key.get(Short.class, n)).toProvider(new ShortProvider(p)); break; case BIG_DECIMAL: binder.bind(Key.get(BigDecimal.class, n)).toProvider(new BigDecimalProvider(p)); break; case BIG_INTEGER: binder.bind(Key.get(BigInteger.class, n)).toProvider(new BigIntegerProvider(p)); break; } } } for (String namespace : knownNamespaces) { Settings s = settings.get(namespace); bind(Settings.class).annotatedWith(new NamespaceImpl(namespace)).toInstance(s); for (String key : s) { Provider<String> p = new PropertyProvider(key, Providers.of(s)); Value n = new ValueImpl(key, namespace); for (SettingsBindings type : settingsBindings) { switch (type) { case INT: binder.bind(Key.get(Integer.class, n)).toProvider(new IntProvider(p)); break; case STRING: binder.bind(Key.get(String.class, n)).toProvider(p); break; case LONG: binder.bind(Key.get(Long.class, n)).toProvider(new LongProvider(p)); break; case BOOLEAN: binder.bind(Key.get(Boolean.class, n)).toProvider(new BooleanProvider(p)); break; case BYTE: binder.bind(Key.get(Byte.class, n)).toProvider(new ByteProvider(p)); break; case CHARACTER: binder.bind(Key.get(Character.class, n)).toProvider(new CharacterProvider(p)); break; case DOUBLE: binder.bind(Key.get(Double.class, n)).toProvider(new DoubleProvider(p)); break; case FLOAT: binder.bind(Key.get(Float.class, n)).toProvider(new FloatProvider(p)); break; case SHORT: binder.bind(Key.get(Short.class, n)).toProvider(new ShortProvider(p)); break; case BIG_DECIMAL: binder.bind(Key.get(BigDecimal.class, n)).toProvider(new BigDecimalProvider(p)); break; case BIG_INTEGER: binder.bind(Key.get(BigInteger.class, n)).toProvider(new BigIntegerProvider(p)); break; } } } } bind(Settings.class).toProvider(namespacedSettings); //Provide a binding to bind(MutableSettings.class).toProvider(new MutableSettingsProvider(namespacedSettings, currentType)); //A hack, but it works boolean isUsingNamespaces = knownNamespaces.size() > 1 || (knownNamespaces.size() == 1 && !Namespace.DEFAULT.equals(knownNamespaces.iterator().next())); if (isUsingNamespaces) { binder.bindListener(Matchers.any(), new ProvisionListenerImpl()); } } catch (Exception ioe) { throw new ConfigurationError(ioe); } } private class ProvisionListenerImpl implements ProvisionListener { public ProvisionListenerImpl() { } @Override public <T> void onProvision(ProvisionInvocation<T> provision) { TypeLiteral<?> old = currentType.get(); prevType.set(old); try (QuietAutoCloseable pc = prevType.set(currentType.get())) { try (QuietAutoCloseable ac = currentType.set(provision.getBinding().getKey().getTypeLiteral())) { T obj = provision.provision(); } } } } } static void log(String s) { if (Boolean.getBoolean(Dependencies.class.getName() + ".log")) { System.out.println(s); } } private static class MutableSettingsProvider implements Provider<MutableSettings> { private final Provider<Settings> namespaced; private final ProtectedThreadLocal<?> injectingInto; public MutableSettingsProvider(Provider<Settings> namespaced, ProtectedThreadLocal<?> injectingInto) { this.namespaced = namespaced; this.injectingInto = injectingInto; } @Override public MutableSettings get() { Settings result = namespaced.get(); if (result instanceof MutableSettings) { return (MutableSettings) result; } else { try { new Exception(injectingInto.get() + " is requesting MutableSettings, " + "but none was bound. Creating ephemeral settings, " + "but probably nothing but this object will see " + "the contents.").printStackTrace(); return new SettingsBuilder().add(result).buildMutableSettings(); } catch (IOException ex) { return Exceptions.chuck(ex); } } } } private static final Pattern pat = Pattern.compile("(.*)\\..*?"); private static class NamespacedSettingsProvider implements Provider<Settings> { private final Dependencies deps; @Inject public NamespacedSettingsProvider(Dependencies deps) { this.deps = deps; } @Override public Settings get() { TypeLiteral<?> t = deps.prevType.get(); String namespace = Namespace.DEFAULT; if (t != null) { Class<?> type = t.getRawType(); Namespace ns = type.getAnnotation(Namespace.class); if (ns == null) { Package pkg = type.getPackage(); if (pkg != null) { do { ns = pkg.getAnnotation(Namespace.class); if (ns == null) { String nm = pkg.getName(); java.util.regex.Matcher m = pat.matcher(nm); if (!m.find()) { break; } else { pkg = Package.getPackage(m.group(1)); if (pkg == null || pkg.getName().equals("")) { break; } } } } while (ns == null); } } if (ns != null) { namespace = ns.value(); } } log("INJECTING INTO " + t + " WITH NAMESPACE " + namespace); Settings s = deps.settings.get(namespace); return s; } } public static Set<String> loadNamespaceListsFromClasspath() throws IOException { Set<String> all = new HashSet<>(); String listPathOnClasspath = Defaults.DEFAULT_PATH + "namespaces.list"; InputStream[] streams = Streams.locate(listPathOnClasspath); if (streams != null) { for (InputStream in : streams) { try { Reader reader = new InputStreamReader(in); NamespaceAnnotationProcessor.readNamepaces(reader, all); } finally { in.close(); } } log("Loaded namespace files: " + all); } else { log("No input streams for namespaces " + all + " - no classpath files " + listPathOnClasspath); } return all; } private static class ByteProvider implements Provider<Byte> { private final Provider<String> p; ByteProvider(Provider<String> p) { this.p = p; } @Override public Byte get() { String s = p.get(); return s == null ? null : Byte.parseByte(s); } } private static class FloatProvider implements Provider<Float> { private final Provider<String> p; FloatProvider(Provider<String> p) { this.p = p; } @Override public Float get() { String s = p.get(); return s == null ? null : Float.parseFloat(s); } } private static class DoubleProvider implements Provider<Double> { private final Provider<String> p; DoubleProvider(Provider<String> p) { this.p = p; } @Override public Double get() { String s = p.get(); return s == null ? null : Double.parseDouble(s); } } private static class ShortProvider implements Provider<Short> { private final Provider<String> p; ShortProvider(Provider<String> p) { this.p = p; } @Override public Short get() { String s = p.get(); return s == null ? null : Short.parseShort(s); } } private static class CharacterProvider implements Provider<Character> { private final Provider<String> p; CharacterProvider(Provider<String> p) { this.p = p; } @Override public Character get() { String s = p.get(); return s == null ? null : s.length() == 0 ? 0 : s.charAt(0); } } private static class LongProvider implements Provider<Long> { private final Provider<String> p; LongProvider(Provider<String> p) { this.p = p; } @Override public Long get() { String s = p.get(); return s == null ? null : Long.parseLong(s); } } private static class IntProvider implements Provider<Integer> { private final Provider<String> p; IntProvider(Provider<String> p) { this.p = p; } @Override public Integer get() { String s = p.get(); return s == null ? null : Integer.parseInt(s); } } private static class BooleanProvider implements Provider<Boolean> { private final Provider<String> p; BooleanProvider(Provider<String> p) { this.p = p; } @Override public Boolean get() { String s = p.get(); return s == null ? false : Boolean.valueOf(s); } } private static class PropertyProvider implements Provider<String> { private final String key; private final Provider<Settings> props; PropertyProvider(String key, Provider<Settings> props) { this.key = key; this.props = props; } @Override public String get() { return props.get().getString(key); } } private static class BigDecimalProvider implements Provider<BigDecimal> { private final Provider<String> p; BigDecimalProvider(Provider<String> p) { this.p = p; } @Override public BigDecimal get() { String s = p.get(); return s == null ? null : new BigDecimal(s); } } private static class BigIntegerProvider implements Provider<BigInteger> { private final Provider<String> p; BigIntegerProvider(Provider<String> p) { this.p = p; } @Override public BigInteger get() { String s = p.get(); return s == null ? null : new BigInteger(s); } } }