com.twitter.common.args.Args.java Source code

Java tutorial

Introduction

Here is the source code for com.twitter.common.args.Args.java

Source

// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

import com.twitter.common.args.apt.Configuration;
import com.twitter.common.args.apt.Configuration.ArgInfo;

import static com.twitter.common.args.apt.Configuration.ConfigurationException;

/**
 * Utility that can load static {@literal @CmdLine} and {@literal @Positional} arg field info from
 * a configuration database or from explicitly listed containing classes or objects.
 */
public final class Args {
    @VisibleForTesting
    static final Function<ArgInfo, Optional<Field>> TO_FIELD = new Function<ArgInfo, Optional<Field>>() {
        @Override
        public Optional<Field> apply(ArgInfo info) {
            try {
                return Optional.of(Class.forName(info.className).getDeclaredField(info.fieldName));
            } catch (NoSuchFieldException e) {
                throw new ConfigurationException(e);
            } catch (ClassNotFoundException e) {
                throw new ConfigurationException(e);
            } catch (NoClassDefFoundError e) {
                // A compilation had this class available at the time the ArgInfo was deposited, but
                // the classes have been re-bundled with some subset including the class this ArgInfo
                // points to no longer available.  If the re-bundling is correct, then the arg truly is
                // not needed.
                LOG.fine(String.format("Not on current classpath, skipping %s", info));
                return Optional.absent();
            }
        }
    };

    private static final Logger LOG = Logger.getLogger(Args.class.getName());

    private static final Function<Field, OptionInfo<?>> TO_OPTION_INFO = new Function<Field, OptionInfo<?>>() {
        @Override
        public OptionInfo<?> apply(Field field) {
            @Nullable
            CmdLine cmdLine = field.getAnnotation(CmdLine.class);
            if (cmdLine == null) {
                throw new ConfigurationException("No @CmdLine Arg annotation for field " + field);
            }
            return OptionInfo.createFromField(field);
        }
    };

    private static final Function<Field, PositionalInfo<?>> TO_POSITIONAL_INFO = new Function<Field, PositionalInfo<?>>() {
        @Override
        public PositionalInfo<?> apply(Field field) {
            @Nullable
            Positional positional = field.getAnnotation(Positional.class);
            if (positional == null) {
                throw new ConfigurationException("No @Positional Arg annotation for field " + field);
            }
            return PositionalInfo.createFromField(field);
        }
    };

    /**
     * An opaque container for all the positional and optional {@link Arg} metadata in-play for a
     * command line parse.
     */
    public static final class ArgsInfo {
        private final Configuration configuration;
        private final Optional<? extends PositionalInfo<?>> positionalInfo;
        private final ImmutableList<? extends OptionInfo<?>> optionInfos;

        ArgsInfo(Configuration configuration, Optional<? extends PositionalInfo<?>> positionalInfo,
                Iterable<? extends OptionInfo<?>> optionInfos) {

            this.configuration = Preconditions.checkNotNull(configuration);
            this.positionalInfo = Preconditions.checkNotNull(positionalInfo);
            this.optionInfos = ImmutableList.copyOf(optionInfos);
        }

        Configuration getConfiguration() {
            return configuration;
        }

        Optional<? extends PositionalInfo<?>> getPositionalInfo() {
            return positionalInfo;
        }

        ImmutableList<? extends OptionInfo<?>> getOptionInfos() {
            return optionInfos;
        }
    }

    /**
     * Hydrates configured {@literal @CmdLine} arg fields and selects a desired set with the supplied
     * {@code filter}.
     *
     * @param configuration The configuration to find candidate {@literal @CmdLine} arg fields in.
     * @param filter A predicate to select fields with.
     * @return The desired hydrated {@literal @CmdLine} arg fields and optional {@literal @Positional}
     *     arg field.
     */
    static ArgsInfo fromConfiguration(Configuration configuration, Predicate<Field> filter) {
        ImmutableSet<Field> positionalFields = ImmutableSet
                .copyOf(filterFields(configuration.positionalInfo(), filter));

        if (positionalFields.size() > 1) {
            throw new IllegalArgumentException(String.format(
                    "Found %d fields marked for @Positional Args after applying filter - "
                            + "only 1 is allowed:\n\t%s",
                    positionalFields.size(), Joiner.on("\n\t").join(positionalFields)));
        }

        Optional<? extends PositionalInfo<?>> positionalInfo = Optional.fromNullable(
                Iterables.getOnlyElement(Iterables.transform(positionalFields, TO_POSITIONAL_INFO), null));

        Iterable<? extends OptionInfo<?>> optionInfos = Iterables
                .transform(filterFields(configuration.optionInfo(), filter), TO_OPTION_INFO);

        return new ArgsInfo(configuration, positionalInfo, optionInfos);
    }

    private static Iterable<Field> filterFields(Iterable<ArgInfo> infos, Predicate<Field> filter) {
        return Iterables.filter(Optional.presentInstances(Iterables.transform(infos, TO_FIELD)), filter);
    }

    /**
     * Equivalent to calling {@code from(Predicates.alwaysTrue(), Arrays.asList(sources)}.
     */
    public static ArgsInfo from(Object... sources) throws IOException {
        return from(ImmutableList.copyOf(sources));
    }

    /**
     * Equivalent to calling {@code from(filter, Arrays.asList(sources)}.
     */
    public static ArgsInfo from(Predicate<Field> filter, Object... sources) throws IOException {
        return from(filter, ImmutableList.copyOf(sources));
    }

    /**
     * Equivalent to calling {@code from(Predicates.alwaysTrue(), sources}.
     */
    public static ArgsInfo from(Iterable<?> sources) throws IOException {
        return from(Predicates.<Field>alwaysTrue(), sources);
    }

    /**
     * Loads arg info from the given sources in addition to the default compile-time configuration.
     *
     * @param filter A predicate to select fields with.
     * @param sources Classes or object instances to scan for {@link Arg} fields.
     * @return The args info describing all discovered {@link Arg args}.
     * @throws IOException If there was a problem loading the default Args configuration.
     */
    public static ArgsInfo from(Predicate<Field> filter, Iterable<?> sources) throws IOException {
        Preconditions.checkNotNull(filter);
        Preconditions.checkNotNull(sources);

        Configuration configuration = Configuration.load();
        ArgsInfo staticInfo = Args.fromConfiguration(configuration, filter);

        final ImmutableSet.Builder<PositionalInfo<?>> positionalInfos = ImmutableSet.<PositionalInfo<?>>builder()
                .addAll(staticInfo.getPositionalInfo().asSet());
        final ImmutableSet.Builder<OptionInfo<?>> optionInfos = ImmutableSet.<OptionInfo<?>>builder()
                .addAll(staticInfo.getOptionInfos());

        for (Object source : sources) {
            Class<?> clazz = source instanceof Class ? (Class) source : source.getClass();
            for (Field field : clazz.getDeclaredFields()) {
                if (filter.apply(field)) {
                    boolean cmdLine = field.isAnnotationPresent(CmdLine.class);
                    boolean positional = field.isAnnotationPresent(Positional.class);
                    if (cmdLine && positional) {
                        throw new IllegalArgumentException(
                                "An Arg cannot be annotated with both @CmdLine and @Positional, found bad Arg "
                                        + "field: " + field);
                    } else if (cmdLine) {
                        optionInfos.add(OptionInfo.createFromField(field, source));
                    } else if (positional) {
                        positionalInfos.add(PositionalInfo.createFromField(field, source));
                    }
                }
            }
        }

        @Nullable
        PositionalInfo<?> positionalInfo = Iterables.getOnlyElement(positionalInfos.build(), null);
        return new ArgsInfo(configuration, Optional.fromNullable(positionalInfo), optionInfos.build());
    }

    private Args() {
        // utility
    }
}