/* * Licensed to Metamarkets Group Inc. (Metamarkets) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Metamarkets licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package io.druid.guice; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.inject.Binder; import com.google.inject.Inject; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.util.Types; import java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.util.Properties; /** * Provides a singleton value of type {@code <T>} from {@code Properties} bound in guice. * <br/> * <h3>Usage</h3> * To install this provider, bind it in your guice module, like below. * * <pre> * JsonConfigProvider.bind(binder, "druid.server", DruidServerConfig.class); * </pre> * <br/> * In the above case, {@code druid.server} should be a key found in the {@code Properties} bound elsewhere. * The value of that key should directly relate to the fields in {@code DruidServerConfig.class}. * * <h3>Implementation</h3> * <br/> * The state of {@code <T>} is defined by the value of the property {@code propertyBase}. * This value is a json structure, decoded via {@link JsonConfigurator#configurate(java.util.Properties, String, Class)}. * <br/> * * An example might be if DruidServerConfig.class were * * <pre> * public class DruidServerConfig * { * @JsonProperty @NotNull public String hostname = null; * @JsonProperty @Min(1025) public int port = 8080; * } * </pre> * * And your Properties object had in it * * <pre> * druid.server.hostname=0.0.0.0 * druid.server.port=3333 * </pre> * * Then this would bind a singleton instance of a DruidServerConfig object with hostname = "0.0.0.0" and port = 3333. * * If the port weren't set in the properties, then the default of 8080 would be taken. Essentially, it is the same as * subtracting the "druid.server" prefix from the properties and building a Map which is then passed into * ObjectMapper.convertValue() * * @param <T> type of config object to provide. */ public class JsonConfigProvider<T> implements Provider<Supplier<T>> { @SuppressWarnings("unchecked") public static <T> void bind(Binder binder, String propertyBase, Class<T> classToProvide) { bind( binder, propertyBase, classToProvide, Key.get(classToProvide), (Key) Key.get(Types.newParameterizedType(Supplier.class, classToProvide)) ); } @SuppressWarnings("unchecked") public static <T> void bind(Binder binder, String propertyBase, Class<T> classToProvide, Annotation annotation) { bind( binder, propertyBase, classToProvide, Key.get(classToProvide, annotation), (Key) Key.get(Types.newParameterizedType(Supplier.class, classToProvide), annotation) ); } @SuppressWarnings("unchecked") public static <T> void bind( Binder binder, String propertyBase, Class<T> classToProvide, Class<? extends Annotation> annotation ) { bind( binder, propertyBase, classToProvide, Key.get(classToProvide, annotation), (Key) Key.get(Types.newParameterizedType(Supplier.class, classToProvide), annotation) ); } @SuppressWarnings("unchecked") public static <T> void bind( Binder binder, String propertyBase, Class<T> clazz, Key<T> instanceKey, Key<Supplier<T>> supplierKey ) { binder.bind(supplierKey).toProvider((Provider) of(propertyBase, clazz)).in(LazySingleton.class); binder.bind(instanceKey).toProvider(new SupplierProvider<T>(supplierKey)); } @SuppressWarnings("unchecked") public static <T> void bindInstance( Binder binder, Key<T> bindKey, T instance ) { binder.bind(bindKey).toInstance(instance); final ParameterizedType supType = Types.newParameterizedType(Supplier.class, bindKey.getTypeLiteral().getType()); final Key supplierKey; if (bindKey.getAnnotationType() != null) { supplierKey = Key.get(supType, bindKey.getAnnotationType()); } else if (bindKey.getAnnotation() != null) { supplierKey = Key.get(supType, bindKey.getAnnotation()); } else { supplierKey = Key.get(supType); } binder.bind(supplierKey).toInstance(Suppliers.<T>ofInstance(instance)); } public static <T> JsonConfigProvider<T> of(String propertyBase, Class<T> classToProvide) { return new JsonConfigProvider<T>(propertyBase, classToProvide); } private final String propertyBase; private final Class<T> classToProvide; private Properties props; private JsonConfigurator configurator; private Supplier<T> retVal = null; public JsonConfigProvider( String propertyBase, Class<T> classToProvide ) { this.propertyBase = propertyBase; this.classToProvide = classToProvide; } @Inject public void inject( Properties props, JsonConfigurator configurator ) { this.props = props; this.configurator = configurator; } @Override public Supplier<T> get() { if (retVal != null) { return retVal; } try { final T config = configurator.configurate(props, propertyBase, classToProvide); retVal = Suppliers.ofInstance(config); } catch (RuntimeException e) { // When a runtime exception gets thrown out, this provider will get called again if the object is asked for again. // This will have the same failed result, 'cause when it's called no parameters will have actually changed. // Guice will then report the same error multiple times, which is pretty annoying. Cache a null supplier and // return that instead. This is technically enforcing a singleton, but such is life. retVal = Suppliers.ofInstance(null); throw e; } return retVal; } }