/* * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed 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 com.google.devrel.gmscore.tools.common; import com.google.common.base.Throwables; import com.google.inject.Binder; import com.google.inject.BindingAnnotation; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.ProvisionException; import com.google.inject.spi.Message; import com.google.inject.util.Providers; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** A helper class for running a Guice injected application with some flag validation added. */ public class InjectedApplication { private final Injector injector; private InjectedApplication(Module... modules) { injector = Guice.createInjector(modules); } /** * Gets an instance from Guice for the provided class. Any missing flags or bindings will be * printed in the error message if there was a problem retrieving the class instance. * * @param cls The class type to retrieve from the Guice injector. * @param <T> The type of the class that is being returned. * @return The class instance retrieved from Guice. */ public <T> T get(Class<T> cls) { try { return injector.getInstance(cls); } catch (ProvisionException e) { System.err.println("Could not start application:"); for (Message msg : e.getErrorMessages()) { System.err.println(" " + msg.toString()); } System.exit(1); } throw new IllegalStateException("Did not get an instance, and did not get an exception?"); } /** * Allows for the creation of {@link InjectedApplication} with Guice modules and flag parameters. */ public static class Builder { private final Set<Class<?>> parameters = new HashSet<>(); private final List<Module> modules = new ArrayList<>(); private final String[] arguments; /** Creates a builder with the given CLI {@code arguments}. */ public Builder(String... arguments) { this.arguments = arguments; } /** Adds class(es) containing one or more fields with a {@link Parameter} annotation. */ public Builder withParameter(Class<?>... parameters) { this.parameters.addAll(Arrays.asList(parameters)); return this; } /** Adds module(s) to the {@link InjectedApplication}. */ public Builder withModule(Module... modules) { this.modules.addAll(Arrays.asList(modules)); return this; } /** Builds an {@link InjectedApplication}. */ public InjectedApplication build() { // This works by providing all flag parameters as the first module. Alternatively, a module // could be created for each parameter. modules.add(0, new Module() { @Override public void configure(Binder binder) { Map<Class<?>, Object> modules = new HashMap<>(); for (Class<?> parameter : parameters) { try { modules.put(parameter, parameter.newInstance()); } catch (InstantiationException | IllegalAccessException e) { throw Throwables.propagate(e); } } new JCommander(modules.values(), arguments); for (Map.Entry<Class<?>, Object> entry : modules.entrySet()) { bindFlagModule(binder, entry.getKey(), entry.getValue()); } } }); return new InjectedApplication(modules.toArray(new Module[modules.size()])); } /** * Binds all fields with {@link BindingAnnotation}-annotated annotations in {@code type} to * {@code binder}. */ private <T> void bindFlagModule(Binder binder, Class<T> type, Object module) { try { for (Field field : type.getDeclaredFields()) { bindFieldAnnotations(binder, field, field.getType(), module); } binder.bind(type).toInstance(type.cast(module)); } catch (IllegalAccessException e) { throw Throwables.propagate(e); } } private <T> void bindFieldAnnotations(Binder binder, Field field, Class<T> fieldType, Object object) throws IllegalAccessException { for (Annotation annotation : getBindingAnnotations(field)) { field.setAccessible(true); // Needed to allow immutable flags. bindAnnotation(binder, fieldType, fieldType.cast(field.get(object)), annotation); } } private <T, U extends T> void bindAnnotation(Binder binder, Class<T> type, @Nullable U object, Annotation annotation) { if (object != null && !type.isInstance(object)) { throw new RuntimeException("Impossible state while binding flag annotations."); } binder.bind(type).annotatedWith(annotation).toProvider(Providers.of(object)); } private Collection<Annotation> getBindingAnnotations(Field field) { List<Annotation> result = new ArrayList<>(); for (Annotation annotation : field.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(BindingAnnotation.class)) { result.add(annotation); } } return result; } } }