edu.uchicago.lowasser.flaginjection.Flags.java Source code

Java tutorial

Introduction

Here is the source code for edu.uchicago.lowasser.flaginjection.Flags.java

Source

/**
 * Copyright 2012 Louis Wasserman
 *
 * 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 edu.uchicago.lowasser.flaginjection;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.Module;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.name.Named;
import com.google.inject.util.Modules;
import com.google.inject.util.Providers;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.reflections.Reflections;

/**
 * Framework for pulling together the fields annotated with @Flag from a collection of classes, and
 * bootstrapping a Guice module that will provide the flag values at runtime.
 * 
 * <p>
 * The bootstrapping process for flags works like this: you construct modules using
 * {@link #flagBindings} that announce to Guice all of the {@code @Flag}-annotated fields in the
 * specified classes. Calling {@link #bootstrapFlagInjector(String[], Module...)} with these
 * modules and possibly others, Guice first creates an injector capable of generating the
 * {@link Options} object that encapsulates all the flags requested. This base injector, in
 * combination with the specified command line arguments, is used to construct (via injection!) a
 * module that binds all the desired flags to their values. This module is used to create a
 * bootstrapped injector that properly injects flag values to the desired locations.
 * 
 * @author lowasser
 */
public final class Flags {
    private Flags() {
    }

    public static void addFlagBinding(Binder binder, final Flag flagAnnotation,
            final TypeLiteral<?> parameterType) {
        MapBinder.newMapBinder(binder, Flag.class, Type.class).addBinding(flagAnnotation)
                .toInstance(parameterType.getType());
    }

    public static Module flagBindings(final Class<?>... classes) {
        return new AbstractModule() {
            @Override
            protected void configure() {
                for (Class<?> clazz : classes) {
                    addFlagBindings(binder(), TypeLiteral.get(clazz));
                }
            }
        };
    }

    public static void addFlagBindings(Binder binder, TypeLiteral<?> literal) {
        for (Field field : literal.getRawType().getDeclaredFields()) {
            if (field.isAnnotationPresent(Flag.class)) {
                Flag annot = field.getAnnotation(Flag.class);
                addFlagBinding(binder, annot, literal.getFieldType(field));
            }
        }
        for (Constructor<?> constructor : literal.getRawType().getDeclaredConstructors()) {
            List<TypeLiteral<?>> parameterTypes = literal.getParameterTypes(constructor);
            Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
            for (int i = 0; i < parameterTypes.size(); i++) {
                Annotation[] annotations = parameterAnnotations[i];
                TypeLiteral<?> typ = parameterTypes.get(i);
                for (Annotation annot : annotations) {
                    if (annot instanceof Flag) {
                        addFlagBinding(binder, (Flag) annot, typ);
                    }
                }
            }
        }
    }

    public static Injector bootstrapFlagInjector(final String[] args, String mainClassName, List<String> packages,
            Module... baseModules) {
        Logger logger = Logger.getLogger("org.learningu.scheduling.flags.Flags");
        AbstractModule linkingModule = new AbstractModule() {

            @Override
            protected void configure() {
            }

            @Provides
            @RuntimeArguments
            String[] commandLineArguments() {
                return args;
            }

            @Provides
            @Singleton
            Options options(Map<Flag, Type> flagsMap) {
                Options options = new Options();
                for (Flag flag : flagsMap.keySet()) {
                    OptionBuilder.hasArgs();
                    OptionBuilder.withLongOpt(flag.name());
                    OptionBuilder.withDescription(flag.description());
                    OptionBuilder.withArgName(flagsMap.get(flag).toString());
                    options.addOption(OptionBuilder.create());
                }
                return options;
            }

            @Provides
            @Singleton
            CommandLine commandLine(Options options, @RuntimeArguments String[] args) {
                try {
                    return new PosixParser().parse(options, args);
                } catch (ParseException e) {
                    throw Throwables.propagate(e);
                }
            }
        };
        logger.fine("Built Options module");
        Injector baseInjector = Guice.createInjector(new FlagErrorModule(mainClassName),
                Modules.combine(Iterables.concat(Arrays.asList(baseModules), ImmutableList.of(linkingModule))));
        logger.fine("Bootstrapping flag injector with command line arguments");
        Injector createdInjector = baseInjector
                .createChildInjector(baseInjector.getInstance(FlagBootstrapModule.class));
        // Use reflection to instantiate the variables in FlagClass classes
        for (String packageName : packages) {
            Reflections reflections = new Reflections(packageName);
            Set<Class<? extends FlagsClass>> classes = reflections.getSubTypesOf(FlagsClass.class);
            for (Class<? extends FlagsClass> flagClass : classes) {
                createdInjector.getInstance(flagClass);
            }
        }
        return createdInjector;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private static final class FlagBootstrapModule extends AbstractModule {
        private final CommandLine commandLine;
        private final Map<Flag, Type> flagsMap;
        private final Options options;
        private final String main;

        // injected
        @Inject
        FlagBootstrapModule(CommandLine commandLine, Map<Flag, Type> flagsMap, Options options,
                @Named("main") String main) {
            this.commandLine = commandLine;
            this.flagsMap = flagsMap;
            this.options = options;
            this.main = main;
        }

        @Override
        protected void configure() {
            for (Map.Entry<Flag, Type> entry : flagsMap.entrySet()) {
                Flag flagAnnotation = entry.getKey();
                final TypeLiteral literal = TypeLiteral.get(entry.getValue());

                @Nullable
                final String value = commandLine.getOptionValue(flagAnnotation.name());

                try {
                    Object result;

                    if (value == null || (result = Converters.converterFor(literal).parse(value)) == null) {
                        if (flagAnnotation.optional()) {
                            bind(literal).annotatedWith(flagAnnotation).toProvider(Providers.of(null));
                            continue;
                        } else {
                            throw new RuntimeException();
                        }
                    } else {
                        bind(literal).annotatedWith(flagAnnotation).toInstance(result);
                    }
                } catch (RuntimeException e) {
                    if (!flagAnnotation.optional()) {
                        new HelpFormatter().printHelp(130, main, "", options, "");
                        throw Throwables.propagate(new ParseException("Missing flag: " + flagAnnotation));
                    }
                }
            }
        }
    }

    private static final class FlagErrorModule extends AbstractModule {
        private final String name;

        FlagErrorModule(String name) {
            this.name = name;
        }

        @Override
        protected void configure() {
        }

        @Provides
        @Named("main")
        public String mainName() {
            // To print help messages, Apache CLI needs the name of the main class.
            return name;
        }
    }
}