// =================================================================================================
// 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
}
}