/**
* 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 org.apache.aurora.common.args;
import java.io.IOException;
import java.lang.reflect.Field;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
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 org.apache.aurora.common.args.apt.Configuration;
import org.apache.aurora.common.args.apt.Configuration.ArgInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.aurora.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 {
private static final Logger LOG = LoggerFactory.getLogger(Args.class);
@VisibleForTesting
static final Function<ArgInfo, Optional<Field>> TO_FIELD =
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.debug("Not on current classpath, skipping {}", info);
return Optional.absent();
}
};
private static final Function<Field, OptionInfo<?>> TO_OPTION_INFO =
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);
};
/**
* 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 ImmutableList<? extends OptionInfo<?>> optionInfos;
ArgsInfo(Configuration configuration, Iterable<? extends OptionInfo<?>> optionInfos) {
this.configuration = Preconditions.checkNotNull(configuration);
this.optionInfos = ImmutableList.copyOf(optionInfos);
}
Configuration getConfiguration() {
return configuration;
}
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) {
Iterable<? extends OptionInfo<?>> optionInfos = Iterables.transform(
filterFields(configuration.optionInfo(), filter), TO_OPTION_INFO);
return new ArgsInfo(configuration, 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<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) && field.isAnnotationPresent(CmdLine.class)) {
optionInfos.add(OptionInfo.createFromField(field, source));
}
}
}
return new ArgsInfo(configuration, optionInfos.build());
}
private Args() {
// utility
}
}