Return-Path: X-Original-To: apmail-aurora-commits-archive@minotaur.apache.org Delivered-To: apmail-aurora-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 4DDFC18A34 for ; Wed, 26 Aug 2015 20:59:53 +0000 (UTC) Received: (qmail 99934 invoked by uid 500); 26 Aug 2015 20:59:53 -0000 Delivered-To: apmail-aurora-commits-archive@aurora.apache.org Received: (qmail 99855 invoked by uid 500); 26 Aug 2015 20:59:53 -0000 Mailing-List: contact commits-help@aurora.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@aurora.apache.org Delivered-To: mailing list commits@aurora.apache.org Received: (qmail 98340 invoked by uid 99); 26 Aug 2015 20:59:52 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 26 Aug 2015 20:59:52 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id C8BBCE715C; Wed, 26 Aug 2015 20:59:51 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: zmanji@apache.org To: commits@aurora.apache.org Date: Wed, 26 Aug 2015 21:00:20 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [30/51] [partial] aurora git commit: Move packages from com.twitter.common to org.apache.aurora.common http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/ArgFilters.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/ArgFilters.java b/commons/src/main/java/org/apache/aurora/common/args/ArgFilters.java new file mode 100644 index 0000000..9a4d441 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/ArgFilters.java @@ -0,0 +1,125 @@ +/** + * 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.lang.reflect.Field; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableSet; + +import org.apache.aurora.common.base.MorePreconditions; + +/** + * Utilities for generating {@literal @CmdLine} {@link Arg} filters suitable for use with + * {@link ArgScanner#parse(Predicate, Iterable)}. These filters assume the + * fields parsed will all be annotated with {@link CmdLine}. + * + * @author John Sirois + */ +public final class ArgFilters { + + /** + * A filter that selects all {@literal @CmdLine} {@link Arg}s found on the classpath. + */ + public static final Predicate SELECT_ALL = Predicates.alwaysTrue(); + + private ArgFilters() { + // utility + } + + /** + * Creates a filter that selects all {@literal @CmdLine} {@link Arg}s found in classes that are + * members of the given package. Note that this will not select subpackages. + * + * @param pkg The exact package of classes whose command line args will be selected. + * @return A filter that selects only command line args declared in classes that are members of + * the given {@code pkg}. + */ + public static Predicate selectPackage(final Package pkg) { + Preconditions.checkNotNull(pkg); + return new Predicate() { + @Override public boolean apply(Field field) { + return field.getDeclaringClass().getPackage().equals(pkg); + } + }; + } + + /** + * Creates a filter that selects all {@literal @CmdLine} {@link Arg}s found in classes that are + * members of the given package or its sub-packages. + * + * @param pkg The ancestor package of classes whose command line args will be selected. + * @return A filter that selects only command line args declared in classes that are members of + * the given {@code pkg} or its sub-packages. + */ + public static Predicate selectAllPackagesUnderHere(final Package pkg) { + Preconditions.checkNotNull(pkg); + final String prefix = pkg.getName() + '.'; + return Predicates.or(selectPackage(pkg), new Predicate() { + @Override public boolean apply(Field field) { + return field.getDeclaringClass().getPackage().getName().startsWith(prefix); + } + }); + } + + /** + * Creates a filter that selects all {@literal @CmdLine} {@link Arg}s found in the given class. + * + * @param clazz The class whose command line args will be selected. + * @return A filter that selects only command line args declared in the given {@code clazz}. + */ + public static Predicate selectClass(final Class clazz) { + Preconditions.checkNotNull(clazz); + return new Predicate() { + @Override public boolean apply(Field field) { + return field.getDeclaringClass().equals(clazz); + } + }; + } + + /** + * Creates a filter that selects all {@literal @CmdLine} {@link Arg}s found in the given classes. + * + * @param cls The classes whose command line args will be selected. + * @return A filter that selects only command line args declared in the given classes. + */ + public static Predicate selectClasses(final Class ... cls) { + Preconditions.checkNotNull(cls); + final Set> listOfClasses = ImmutableSet.copyOf(cls); + return new Predicate() { + @Override public boolean apply(Field field) { + return listOfClasses.contains(field.getDeclaringClass()); + } + }; + } + + /** + * Creates a filter that selects a single {@literal @CmdLine} {@link Arg}. + * + * @param clazz The class that declares the command line arg to be selected. + * @param name The {@link CmdLine#name()} of the arg to select. + * @return A filter that selects a single specified command line arg. + */ + public static Predicate selectCmdLineArg(Class clazz, final String name) { + MorePreconditions.checkNotBlank(name); + return Predicates.and(selectClass(clazz), new Predicate() { + @Override public boolean apply(Field field) { + return field.getAnnotation(CmdLine.class).name().equals(name); + } + }); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/ArgScanner.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/ArgScanner.java b/commons/src/main/java/org/apache/aurora/common/args/ArgScanner.java new file mode 100644 index 0000000..cc4710c --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/ArgScanner.java @@ -0,0 +1,560 @@ +/** + * 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.io.PrintStream; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; + +import org.apache.aurora.common.args.Args.ArgsInfo; +import org.apache.aurora.common.args.apt.Configuration; +import org.apache.aurora.common.collections.Pair; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Argument scanning, parsing, and validating system. This class is designed recursively scan a + * package for declared arguments, parse the values based on the declared type, and validate against + * any constraints that the arugment is decorated with. + * + * The supported argument formats are: + * -arg_name=arg_value + * -arg_name arg_value + * Where {@code arg_value} may be single or double-quoted if desired or necessary to prevent + * splitting by the terminal application. + * + * A special format for boolean arguments is also supported. The following syntaxes all set the + * {@code bool_arg} to {@code true}: + * -bool_arg + * -bool_arg=true + * -no_bool_arg=false (double negation) + * + * Likewise, the following would set {@code bool_arg} to {@code false}: + * -no_bool_arg + * -bool_arg=false + * -no_bool_arg=true (negation) + * + * As with the general argument format, spaces may be used in place of equals for boolean argument + * assignment. + * + * TODO(William Farner): Make default verifier and parser classes package-private and in this + * package. + */ +public final class ArgScanner { + + private static final Function, String> GET_OPTION_INFO_NAME = + new Function, String>() { + @Override public String apply(OptionInfo optionInfo) { + return optionInfo.getName(); + } + }; + + public static final Ordering> ORDER_BY_NAME = + Ordering.natural().onResultOf(GET_OPTION_INFO_NAME); + + private static final Function ARG_NAME_TO_FLAG = new Function() { + @Override public String apply(String argName) { + return "-" + argName; + } + }; + + private static final Predicate> IS_BOOLEAN = + new Predicate>() { + @Override public boolean apply(OptionInfo optionInfo) { + return optionInfo.isBoolean(); + } + }; + + // Regular expression to identify a possible dangling assignment. + // A dangling assignment occurs in two cases: + // - The command line used spaces between arg names and values, causing the name and value to + // end up in different command line arg array elements. + // - The command line is using the short form for a boolean argument, + // such as -use_feature, or -no_use_feature. + private static final String DANGLING_ASSIGNMENT_RE = + String.format("^-%s", OptionInfo.ARG_NAME_RE); + private static final Pattern DANGLING_ASSIGNMENT_PATTERN = + Pattern.compile(DANGLING_ASSIGNMENT_RE); + + // Pattern to identify a full assignment, which would be disassociated from a preceding dangling + // assignment. + private static final Pattern ASSIGNMENT_PATTERN = + Pattern.compile(String.format("%s=.+", DANGLING_ASSIGNMENT_RE)); + + /** + * Extracts the name from an @OptionInfo. + */ + private static final Function, String> GET_OPTION_INFO_NEGATED_NAME = + new Function, String>() { + @Override public String apply(OptionInfo optionInfo) { + return optionInfo.getNegatedName(); + } + }; + + /** + * Gets the canonical name for an @Arg, based on the class containing the field it annotates. + */ + private static final Function, String> GET_CANONICAL_ARG_NAME = + new Function, String>() { + @Override public String apply(OptionInfo optionInfo) { + return optionInfo.getCanonicalName(); + } + }; + + /** + * Gets the canonical negated name for an @Arg. + */ + private static final Function, String> GET_CANONICAL_NEGATED_ARG_NAME = + new Function, String>() { + @Override public String apply(OptionInfo optionInfo) { + return optionInfo.getCanonicalNegatedName(); + } + }; + + private static final Logger LOG = Logger.getLogger(ArgScanner.class.getName()); + + // Pattern for the required argument format. + private static final Pattern ARG_PATTERN = + Pattern.compile(String.format("-(%s)(?:(?:=| +)(.*))?", OptionInfo.ARG_NAME_RE)); + + private static final Pattern QUOTE_PATTERN = Pattern.compile("(['\"])([^\\\1]*)\\1"); + + private final PrintStream out; + + /** + * Equivalent to calling {@link #ArgScanner(PrintStream)} passing {@link System#out}. + */ + public ArgScanner() { + this(System.out); + } + + /** + * Creates a new ArgScanner that prints help on arg parse failure or when help is requested to + * {@code out} or else prints applied argument information to {@code out} when parsing is + * successful. + * + * @param out An output stream to write help and parsed argument info to. + */ + public ArgScanner(PrintStream out) { + this.out = Preconditions.checkNotNull(out); + } + + /** + * Applies the provided argument values to all {@literal @CmdLine} {@code Arg} fields discovered + * on the classpath. + * + * @param args Argument values to map, parse, validate, and apply. + * @return {@code true} if the given {@code args} were successfully applied to their corresponding + * {@link Arg} fields. + * @throws ArgScanException if there was a problem loading {@literal @CmdLine} argument + * definitions + * @throws IllegalArgumentException If the arguments provided are invalid based on the declared + * arguments found. + */ + public boolean parse(Iterable args) { + return parse(ArgFilters.SELECT_ALL, ImmutableList.copyOf(args)); + } + + /** + * Applies the provided argument values to any {@literal @CmdLine} or {@literal @Positional} + * {@code Arg} fields discovered on the classpath and accepted by the given {@code filter}. + * + * @param filter A predicate that selects or rejects scanned {@literal @CmdLine} fields for + * argument application. + * @param args Argument values to map, parse, validate, and apply. + * @return {@code true} if the given {@code args} were successfully applied to their corresponding + * {@link Arg} fields. + * @throws ArgScanException if there was a problem loading {@literal @CmdLine} argument + * definitions + * @throws IllegalArgumentException If the arguments provided are invalid based on the declared + * arguments found. + */ + public boolean parse(Predicate filter, Iterable args) { + Preconditions.checkNotNull(filter); + ImmutableList arguments = ImmutableList.copyOf(args); + + Configuration configuration = load(); + ArgsInfo argsInfo = Args.fromConfiguration(configuration, filter); + return parse(argsInfo, arguments); + } + + /** + * Parse command line arguments given a {@link ArgsInfo} + * + * @param argsInfo A description of any optional and positional arguments to parse. + * @param args Argument values to map, parse, validate, and apply. + * @return {@code true} if the given {@code args} were successfully applied to their corresponding + * {@link Arg} fields. + * @throws ArgScanException if there was a problem loading {@literal @CmdLine} argument + * definitions + * @throws IllegalArgumentException If the arguments provided are invalid based on the declared + * arguments found. + */ + public boolean parse(ArgsInfo argsInfo, Iterable args) { + Preconditions.checkNotNull(argsInfo); + ImmutableList arguments = ImmutableList.copyOf(args); + + ParserOracle parserOracle = Parsers.fromConfiguration(argsInfo.getConfiguration()); + Verifiers verifiers = Verifiers.fromConfiguration(argsInfo.getConfiguration()); + Pair, List> results = mapArguments(arguments); + return process(parserOracle, verifiers, argsInfo, results.getFirst(), results.getSecond()); + } + + private Configuration load() { + try { + return Configuration.load(); + } catch (IOException e) { + throw new ArgScanException(e); + } + } + + @VisibleForTesting static List joinKeysToValues(Iterable args) { + List joinedArgs = Lists.newArrayList(); + String unmappedKey = null; + for (String arg : args) { + if (unmappedKey == null) { + if (DANGLING_ASSIGNMENT_PATTERN.matcher(arg).matches()) { + // Beginning of a possible dangling assignment. + unmappedKey = arg; + } else { + joinedArgs.add(arg); + } + } else { + if (ASSIGNMENT_PATTERN.matcher(arg).matches()) { + // Full assignment, disassociate from dangling assignment. + joinedArgs.add(unmappedKey); + joinedArgs.add(arg); + unmappedKey = null; + } else if (DANGLING_ASSIGNMENT_PATTERN.matcher(arg).find()) { + // Another dangling assignment, this could be two sequential boolean args. + joinedArgs.add(unmappedKey); + unmappedKey = arg; + } else { + // Join the dangling key with its value. + joinedArgs.add(unmappedKey + "=" + arg); + unmappedKey = null; + } + } + } + + if (unmappedKey != null) { + joinedArgs.add(unmappedKey); + } + + return joinedArgs; + } + + private static String stripQuotes(String str) { + Matcher matcher = QUOTE_PATTERN.matcher(str); + return matcher.matches() ? matcher.group(2) : str; + } + + /** + * Scans through args, mapping keys to values even if the arg values are 'dangling' and reside + * in different array entries than the respective keys. + * + * @param args Arguments to build into a map. + * @return A map from argument key (arg name) to value paired with a list of any leftover + * positional arguments. + */ + private static Pair, List> mapArguments( + Iterable args) { + + ImmutableMap.Builder argMap = ImmutableMap.builder(); + List positionalArgs = Lists.newArrayList(); + for (String arg : joinKeysToValues(args)) { + if (!arg.startsWith("-")) { + positionalArgs.add(arg); + } else { + Matcher matcher = ARG_PATTERN.matcher(arg); + checkArgument(matcher.matches(), + String.format("Argument '%s' does not match required format -arg_name=arg_value", arg)); + + String rawValue = matcher.group(2); + // An empty string denotes that the argument was passed with no value. + rawValue = rawValue == null ? "" : stripQuotes(rawValue); + argMap.put(matcher.group(1), rawValue); + } + } + + return Pair.of(argMap.build(), positionalArgs); + } + + private static Set dropCollisions(Iterable input) { + Set copy = Sets.newHashSet(); + Set collisions = Sets.newHashSet(); + for (T entry : input) { + if (!copy.add(entry)) { + collisions.add(entry); + } + } + + copy.removeAll(collisions); + return copy; + } + + private static Set getNoCollisions(Iterable> optionInfos) { + Iterable argShortNames = Iterables.transform(optionInfos, GET_OPTION_INFO_NAME); + Iterable argShortNegNames = + Iterables.transform(Iterables.filter(optionInfos, IS_BOOLEAN), + GET_OPTION_INFO_NEGATED_NAME); + Iterable argAllShortNames = Iterables.concat(argShortNames, argShortNegNames); + Set argAllShortNamesNoCollisions = dropCollisions(argAllShortNames); + Set collisionsDropped = Sets.difference(ImmutableSet.copyOf(argAllShortNames), + argAllShortNamesNoCollisions); + if (!collisionsDropped.isEmpty()) { + LOG.warning("Found argument name collisions, args must be referenced by canonical names: " + + collisionsDropped); + } + return argAllShortNamesNoCollisions; + } + + /** + * Applies argument values to fields based on their annotations. + * + * @param parserOracle ParserOracle available to parse raw args with. + * @param verifiers Verifiers available to verify argument constraints with. + * @param argsInfo Fields to apply argument values to. + * @param args Unparsed argument values. + * @param positionalArgs The unparsed positional arguments. + * @return {@code true} if the given {@code args} were successfully applied to their + * corresponding {@link Arg} fields. + */ + private boolean process(final ParserOracle parserOracle, + Verifiers verifiers, + ArgsInfo argsInfo, + Map args, + List positionalArgs) { + + if (!Sets.intersection(args.keySet(), ArgumentInfo.HELP_ARGS).isEmpty()) { + printHelp(verifiers, argsInfo); + return false; + } + + Optional> positionalInfoOptional = argsInfo.getPositionalInfo(); + checkArgument(positionalInfoOptional.isPresent() || positionalArgs.isEmpty(), + "Positional arguments have been supplied but there is no Arg annotated to received them."); + + Iterable> optionInfos = argsInfo.getOptionInfos(); + + final Set argsFailedToParse = Sets.newHashSet(); + final Set argsConstraintsFailed = Sets.newHashSet(); + + Set argAllShortNamesNoCollisions = getNoCollisions(optionInfos); + + final Map> argsByName = + ImmutableMap.>builder() + // Map by short arg name -> arg def. + .putAll(Maps.uniqueIndex(Iterables.filter(optionInfos, + Predicates.compose(Predicates.in(argAllShortNamesNoCollisions), GET_OPTION_INFO_NAME)), + GET_OPTION_INFO_NAME)) + // Map by canonical arg name -> arg def. + .putAll(Maps.uniqueIndex(optionInfos, GET_CANONICAL_ARG_NAME)) + // Map by negated short arg name (for booleans) + .putAll(Maps.uniqueIndex( + Iterables.filter(Iterables.filter(optionInfos, IS_BOOLEAN), + Predicates.compose(Predicates.in(argAllShortNamesNoCollisions), + GET_OPTION_INFO_NEGATED_NAME)), + GET_OPTION_INFO_NEGATED_NAME)) + // Map by negated canonical arg name (for booleans) + .putAll(Maps.uniqueIndex(Iterables.filter(optionInfos, IS_BOOLEAN), + GET_CANONICAL_NEGATED_ARG_NAME)) + .build(); + + // TODO(William Farner): Make sure to disallow duplicate arg specification by short and + // canonical names. + + // TODO(William Farner): Support non-atomic argument constraints. @OnlyIfSet, @OnlyIfNotSet, + // @ExclusiveOf to define inter-argument constraints. + + Set recognizedArgs = Sets.intersection(argsByName.keySet(), args.keySet()); + + for (String argName : recognizedArgs) { + String argValue = args.get(argName); + OptionInfo optionInfo = argsByName.get(argName); + + try { + optionInfo.load(parserOracle, argName, argValue); + } catch (IllegalArgumentException e) { + argsFailedToParse.add(argName + " - " + e.getMessage()); + } + } + + if (positionalInfoOptional.isPresent()) { + PositionalInfo positionalInfo = positionalInfoOptional.get(); + positionalInfo.load(parserOracle, positionalArgs); + } + + Set commandLineArgumentInfos = Sets.newTreeSet(); + + Iterable> allArguments = argsInfo.getOptionInfos(); + + if (positionalInfoOptional.isPresent()) { + PositionalInfo positionalInfo = positionalInfoOptional.get(); + allArguments = Iterables.concat(optionInfos, ImmutableList.of(positionalInfo)); + } + + for (ArgumentInfo anArgumentInfo : allArguments) { + Arg arg = anArgumentInfo.getArg(); + + commandLineArgumentInfos.add(String.format("%s (%s): %s", + anArgumentInfo.getName(), anArgumentInfo.getCanonicalName(), + arg.uncheckedGet())); + + try { + anArgumentInfo.verify(verifiers); + } catch (IllegalArgumentException e) { + argsConstraintsFailed.add(anArgumentInfo.getName() + " - " + e.getMessage()); + } + } + + ImmutableMultimap warningMessages = + ImmutableMultimap.builder() + .putAll("Unrecognized arguments", Sets.difference(args.keySet(), argsByName.keySet())) + .putAll("Failed to parse", argsFailedToParse) + .putAll("Value did not meet constraints", argsConstraintsFailed) + .build(); + + if (!warningMessages.isEmpty()) { + printHelp(verifiers, argsInfo); + StringBuilder sb = new StringBuilder(); + for (Map.Entry> warnings : warningMessages.asMap().entrySet()) { + sb.append(warnings.getKey()).append(":\n\t").append(Joiner.on("\n\t") + .join(warnings.getValue())).append("\n"); + } + throw new IllegalArgumentException(sb.toString()); + } + + LOG.info("-------------------------------------------------------------------------"); + LOG.info("Command line argument values"); + for (String commandLineArgumentInfo : commandLineArgumentInfos) { + LOG.info(commandLineArgumentInfo); + } + LOG.info("-------------------------------------------------------------------------"); + return true; + } + + private void printHelp(Verifiers verifiers, ArgsInfo argsInfo) { + ImmutableList.Builder requiredHelps = ImmutableList.builder(); + ImmutableList.Builder optionalHelps = ImmutableList.builder(); + Optional firstArgFileArgumentName = Optional.absent(); + for (OptionInfo optionInfo + : ORDER_BY_NAME.immutableSortedCopy(argsInfo.getOptionInfos())) { + Arg arg = optionInfo.getArg(); + Object defaultValue = arg.uncheckedGet(); + ImmutableList constraints = optionInfo.collectConstraints(verifiers); + String help = formatHelp(optionInfo, constraints, defaultValue); + if (!arg.hasDefault()) { + requiredHelps.add(help); + } else { + optionalHelps.add(help); + } + if (optionInfo.argFile() && !firstArgFileArgumentName.isPresent()) { + firstArgFileArgumentName = Optional.of(optionInfo.getName()); + } + } + + infoLog("-------------------------------------------------------------------------"); + infoLog(String.format("%s to print this help message", + Joiner.on(" or ").join(Iterables.transform(ArgumentInfo.HELP_ARGS, ARG_NAME_TO_FLAG)))); + Optional> positionalInfoOptional = argsInfo.getPositionalInfo(); + if (positionalInfoOptional.isPresent()) { + infoLog("\nPositional args:"); + PositionalInfo positionalInfo = positionalInfoOptional.get(); + Arg arg = positionalInfo.getArg(); + Object defaultValue = arg.uncheckedGet(); + ImmutableList constraints = positionalInfo.collectConstraints(verifiers); + infoLog(String.format("%s%s\n\t%s\n\t(%s)", + defaultValue != null ? "default " + defaultValue : "", + Iterables.isEmpty(constraints) + ? "" + : " [" + Joiner.on(", ").join(constraints) + "]", + positionalInfo.getHelp(), + positionalInfo.getCanonicalName())); + // TODO: https://github.com/twitter/commons/issues/353, in the future we may + // want to support @argfile format for positional arguments. We should check + // to update firstArgFileArgumentName for them as well. + } + ImmutableList required = requiredHelps.build(); + if (!required.isEmpty()) { + infoLog("\nRequired flags:"); // yes - this should actually throw! + infoLog(Joiner.on('\n').join(required)); + } + ImmutableList optional = optionalHelps.build(); + if (!optional.isEmpty()) { + infoLog("\nOptional flags:"); + infoLog(Joiner.on('\n').join(optional)); + } + if (firstArgFileArgumentName.isPresent()) { + infoLog(String.format("\n" + + "For arguments that support @argfile format: @argfile is a text file that contains " + + "cmdline argument values. For example: -%s=@/tmp/%s_value.txt. The format " + + "of the argfile content should be exactly the same as it would be specified on the " + + "cmdline.", firstArgFileArgumentName.get(), firstArgFileArgumentName.get())); + } + infoLog("-------------------------------------------------------------------------"); + } + + private String formatHelp(ArgumentInfo argumentInfo, Iterable constraints, + @Nullable Object defaultValue) { + + return String.format("-%s%s%s\n\t%s\n\t(%s)", + argumentInfo.getName(), + defaultValue != null ? "=" + defaultValue : "", + Iterables.isEmpty(constraints) + ? "" + : " [" + Joiner.on(", ").join(constraints) + "]", + argumentInfo.getHelp(), + argumentInfo.getCanonicalName()); + } + + private void infoLog(String msg) { + out.println(msg); + } + + /** + * Indicates a problem scanning {@literal @CmdLine} arg definitions. + */ + public static class ArgScanException extends RuntimeException { + public ArgScanException(Throwable cause) { + super(cause); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/Args.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/Args.java b/commons/src/main/java/org/apache/aurora/common/args/Args.java new file mode 100644 index 0000000..ad0b299 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/Args.java @@ -0,0 +1,224 @@ +/** + * 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 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 org.apache.aurora.common.args.apt.Configuration; +import org.apache.aurora.common.args.apt.Configuration.ArgInfo; + +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 { + @VisibleForTesting + static final Function> TO_FIELD = + new Function>() { + @Override public Optional 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> TO_OPTION_INFO = + new Function>() { + @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> TO_POSITIONAL_INFO = + new Function>() { + @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> positionalInfo; + private final ImmutableList> optionInfos; + + ArgsInfo(Configuration configuration, + Optional> positionalInfo, + Iterable> optionInfos) { + + this.configuration = Preconditions.checkNotNull(configuration); + this.positionalInfo = Preconditions.checkNotNull(positionalInfo); + this.optionInfos = ImmutableList.copyOf(optionInfos); + } + + Configuration getConfiguration() { + return configuration; + } + + Optional> getPositionalInfo() { + return positionalInfo; + } + + ImmutableList> 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 filter) { + ImmutableSet 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> positionalInfo = + Optional.fromNullable( + Iterables.getOnlyElement( + Iterables.transform(positionalFields, TO_POSITIONAL_INFO), null)); + + Iterable> optionInfos = Iterables.transform( + filterFields(configuration.optionInfo(), filter), TO_OPTION_INFO); + + return new ArgsInfo(configuration, positionalInfo, optionInfos); + } + + private static Iterable filterFields(Iterable infos, Predicate 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 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.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 filter, Iterable sources) throws IOException { + Preconditions.checkNotNull(filter); + Preconditions.checkNotNull(sources); + + Configuration configuration = Configuration.load(); + ArgsInfo staticInfo = Args.fromConfiguration(configuration, filter); + + final ImmutableSet.Builder> positionalInfos = + ImmutableSet.>builder().addAll(staticInfo.getPositionalInfo().asSet()); + final ImmutableSet.Builder> optionInfos = + ImmutableSet.>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 + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/ArgumentInfo.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/ArgumentInfo.java b/commons/src/main/java/org/apache/aurora/common/args/ArgumentInfo.java new file mode 100644 index 0000000..051e3f9 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/ArgumentInfo.java @@ -0,0 +1,247 @@ +/** + * 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.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.reflect.TypeToken; + +import org.apache.aurora.common.args.constraints.NotNullVerifier; +import org.apache.aurora.common.base.MorePreconditions; + +/** + * Description of a command line {@link Arg} instance. + */ +public abstract class ArgumentInfo { + static final ImmutableSet HELP_ARGS = ImmutableSet.of("h", "help"); + + /** + * Extracts the {@code Arg} from the given field. + * + * @param field The field containing the {@code Arg}. + * @param instance An optional object instance containing the field. + * @return The extracted {@code} Arg. + * @throws IllegalArgumentException If the field does not contain an arg. + */ + protected static Arg getArgForField(Field field, Optional instance) { + Preconditions.checkArgument(field.getType() == Arg.class, + "Field is annotated for argument parsing but is not of Arg type: " + field); + Preconditions.checkArgument(Modifier.isStatic(field.getModifiers()) || instance.isPresent(), + "Non-static argument fields are not supported, found " + field); + + field.setAccessible(true); + try { + return (Arg) field.get(instance.orNull()); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot get arg value for " + field); + } + } + + private final String canonicalName; + private final String name; + private final String help; + private final boolean argFile; + private final Arg arg; + private final TypeToken type; + private final List verifierAnnotations; + @Nullable private final Class> parser; + + /** + * Creates a new {@code ArgsInfo}. + * + * @param canonicalName A fully qualified name for the argument. + * @param name The simple name for the argument. + * @param help Help string. + * @param argFile If argument file is allowed. + * @param arg Argument object. + * @param type Concrete argument type. + * @param verifierAnnotations {@link Verifier} annotations for this + * argument. + * @param parser Parser for the argument type. + */ + protected ArgumentInfo( + String canonicalName, + String name, + String help, + boolean argFile, + Arg arg, + TypeToken type, + List verifierAnnotations, + @Nullable Class> parser) { + + this.canonicalName = MorePreconditions.checkNotBlank(canonicalName); + this.name = MorePreconditions.checkNotBlank(name); + this.help = MorePreconditions.checkNotBlank(help); + this.argFile = argFile; + this.arg = Preconditions.checkNotNull(arg); + this.type = Preconditions.checkNotNull(type); + this.verifierAnnotations = ImmutableList.copyOf(verifierAnnotations); + this.parser = parser; + } + + /** + * Return the name of the command line argument. In an optional argument, this is expressed on + * the command line by "-name=value"; whereas, for a positional argument, the name indicates + * the type/function. + */ + public final String getName() { + return name; + } + + /** + * Return the fully-qualified name of the command line argument. This is used as a command-line + * optional argument, as in: -prefix.name=value. Prefix is typically a java package and class like + * "com.twitter.myapp.MyClass". The difference between a canonical name and a regular name is that + * it is in some circumstances for two names to collide; the canonical name, then, disambiguates. + */ + public final String getCanonicalName() { + return canonicalName; + } + + /** + * Returns the instructions for this command-line argument. This is typically used when the + * executable is passed the -help flag. + */ + public String getHelp() { + return help; + } + + /** + * Returns whether an argument file is allowed for this argument. + */ + public boolean argFile() { + return argFile; + } + + /** + * Returns the Arg associated with this command-line argument. The Arg is a mutable container + * cell that holds the value passed-in on the command line, after parsing and validation. + */ + public Arg getArg() { + return arg; + } + + /** + * Sets the value of the {@link Arg} described by this {@code ArgumentInfo}. + * + * @param value The value to set. + */ + protected void setValue(@Nullable T value) { + arg.set(value); + } + + /** + * Returns the TypeToken that represents the type of this command-line argument. + */ + public TypeToken getType() { + return type; + } + + @Override + public boolean equals(Object object) { + return (object instanceof ArgumentInfo) && arg.equals(((ArgumentInfo) object).arg); + } + + @Override + public int hashCode() { + return arg.hashCode(); + } + + /** + * Finds an appropriate parser for this args underlying value type. + * + * @param parserOracle The registry of known parsers. + * @return A parser that can parse strings into the underlying argument type. + * @throws IllegalArgumentException If no parser was found for the underlying argument type. + */ + protected Parser getParser(ParserOracle parserOracle) { + Preconditions.checkNotNull(parserOracle); + if (parser == null || NoParser.class.equals(parser)) { + return parserOracle.get(type); + } else { + try { + return parser.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("Failed to instantiate parser " + parser); + } catch (IllegalAccessException e) { + throw new RuntimeException("No access to instantiate parser " + parser); + } + } + } + + static class ValueVerifier { + private final Verifier verifier; + private final Annotation annotation; + + ValueVerifier(Verifier verifier, Annotation annotation) { + this.verifier = verifier; + this.annotation = annotation; + } + + void verify(@Nullable T value) { + if (value != null || verifier instanceof NotNullVerifier) { + verifier.verify(value, annotation); + } + } + + String toString(Class rawType) { + return verifier.toString(rawType, annotation); + } + } + + private Iterable> getVerifiers(final Verifiers verifierOracle) { + Function>> toVerifier = + new Function>>() { + @Override public Optional> apply(Annotation annotation) { + @Nullable Verifier verifier = verifierOracle.get(type, annotation); + if (verifier != null) { + return Optional.of(new ValueVerifier(verifier, annotation)); + } else { + return Optional.absent(); + } + } + }; + return Optional.presentInstances(Iterables.transform(verifierAnnotations, toVerifier)); + } + + void verify(Verifiers verifierOracle) { + @Nullable T value = getArg().uncheckedGet(); + for (ValueVerifier valueVerifier : getVerifiers(verifierOracle)) { + valueVerifier.verify(value); + } + } + + ImmutableList collectConstraints(Verifiers verifierOracle) { + @SuppressWarnings("unchecked") // type.getType() is T + final Class rawType = (Class) type.getRawType(); + return FluentIterable.from(getVerifiers(verifierOracle)).transform( + new Function, String>() { + @Override public String apply(ValueVerifier verifier) { + return verifier.toString(rawType); + } + }).toList(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/OptionInfo.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/OptionInfo.java b/commons/src/main/java/org/apache/aurora/common/args/OptionInfo.java new file mode 100644 index 0000000..2e22a92 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/OptionInfo.java @@ -0,0 +1,201 @@ +/** + * 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.File; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import com.google.common.base.Charsets; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import com.google.common.base.Strings; +import com.google.common.io.Files; +import com.google.common.reflect.TypeToken; + +import org.apache.aurora.common.args.apt.Configuration; +import org.apache.aurora.common.base.Function; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Description of a command line option/flag such as -foo=bar. + */ +public final class OptionInfo extends ArgumentInfo { + static final String ARG_NAME_RE = "[\\w\\-\\.]+"; + static final String ARG_FILE_HELP_TEMPLATE + = "%s This argument supports @argfile format. See details below."; + + private static final Pattern ARG_NAME_PATTERN = Pattern.compile(ARG_NAME_RE); + private static final String NEGATE_BOOLEAN = "no_"; + private static final String ARG_FILE_INDICATOR = "@"; + + /** + * Factory method to create a OptionInfo from a field. + * + * @param field The field must contain a {@link Arg}. + * @return an OptionInfo describing the field. + */ + static OptionInfo createFromField(Field field) { + return createFromField(field, null); + } + + /** + * Factory method to create a OptionInfo from a field. + * + * @param field The field must contain a {@link Arg}. + * @param instance The object containing the non-static Arg instance or else null if the Arg + * field is static. + * @return an OptionInfo describing the field. + */ + static OptionInfo createFromField(final Field field, @Nullable Object instance) { + CmdLine cmdLine = field.getAnnotation(CmdLine.class); + if (cmdLine == null) { + throw new Configuration.ConfigurationException( + "No @CmdLine Arg annotation for field " + field); + } + + String name = cmdLine.name(); + Preconditions.checkNotNull(name); + checkArgument(!HELP_ARGS.contains(name), + String.format("Argument name '%s' is reserved for builtin argument help", name)); + checkArgument(ARG_NAME_PATTERN.matcher(name).matches(), + String.format("Argument name '%s' does not match required pattern %s", + name, ARG_NAME_RE)); + + Function canonicalizer = new Function() { + @Override public String apply(String name) { + return field.getDeclaringClass().getCanonicalName() + "." + name; + } + }; + + @SuppressWarnings({"unchecked", "rawtypes"}) // we have no way to know the type here + OptionInfo optionInfo = new OptionInfo( + canonicalizer, + name, + getCmdLineHelp(cmdLine), + cmdLine.argFile(), + getArgForField(field, Optional.fromNullable(instance)), + TypeUtil.getTypeParamTypeToken(field), + Arrays.asList(field.getAnnotations()), + cmdLine.parser()); + + return optionInfo; + } + + private static String getCmdLineHelp(CmdLine cmdLine) { + String help = cmdLine.help(); + + if (cmdLine.argFile()) { + help = String.format(ARG_FILE_HELP_TEMPLATE, help, cmdLine.name(), cmdLine.name()); + } + + return help; + } + + private final Function canonicalizer; + + private OptionInfo( + Function canonicalizer, + String name, + String help, + boolean argFile, + Arg arg, + TypeToken type, + List verifierAnnotations, + @Nullable Class> parser) { + + super(canonicalizer.apply(name), name, help, argFile, arg, type, + verifierAnnotations, parser); + this.canonicalizer = canonicalizer; + } + + /** + * Parses the value and store result in the {@link Arg} contained in this {@code OptionInfo}. + */ + void load(ParserOracle parserOracle, String optionName, String value) { + Parser parser = getParser(parserOracle); + + String finalValue = value; + + // If "-arg=@file" is allowed and specified, then we read the value from the file + // and use it as the raw value to be parsed for the argument. + if (argFile() + && !Strings.isNullOrEmpty(value) + && value.startsWith(ARG_FILE_INDICATOR)) { + finalValue = getArgFileContent(optionName, value.substring(ARG_FILE_INDICATOR.length())); + } + + Object result = parser.parse(parserOracle, getType().getType(), finalValue); // [A] + + // If the arg type is boolean, check if the command line uses the negated boolean form. + if (isBoolean()) { + if (Predicates.in(Arrays.asList(getNegatedName(), getCanonicalNegatedName())) + .apply(optionName)) { + result = !(Boolean) result; // [B] + } + } + + // We know result is T at line [A] but throw this type information away to allow negation if T + // is Boolean at line [B] + @SuppressWarnings("unchecked") + T parsed = (T) result; + + setValue(parsed); + } + + boolean isBoolean() { + return getType().getRawType() == Boolean.class; + } + + /** + * Similar to the simple name, but with boolean arguments appends "no_", as in: + * {@code -no_fire=false} + */ + String getNegatedName() { + return NEGATE_BOOLEAN + getName(); + } + + /** + * Similar to the canonical name, but with boolean arguments appends "no_", as in: + * {@code -com.twitter.common.MyApp.no_fire=false} + */ + String getCanonicalNegatedName() { + return canonicalizer.apply(getNegatedName()); + } + + private String getArgFileContent(String optionName, String argFilePath) + throws IllegalArgumentException { + if (argFilePath.isEmpty()) { + throw new IllegalArgumentException( + String.format("Invalid null/empty value for argument '%s'.", optionName)); + } + + try { + return Files.toString(new File(argFilePath), Charsets.UTF_8); + } catch (IOException e) { + throw new IllegalArgumentException( + String.format("Unable to read argument '%s' value from file '%s'.", + optionName, argFilePath), + e); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/Parsers.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/Parsers.java b/commons/src/main/java/org/apache/aurora/common/args/Parsers.java new file mode 100644 index 0000000..62564a6 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/Parsers.java @@ -0,0 +1,114 @@ +/** + * 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.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.reflect.TypeToken; + +import org.apache.aurora.common.args.apt.Configuration; +import org.apache.aurora.common.args.apt.Configuration.ParserInfo; + +import static com.google.common.base.Preconditions.checkArgument; + +import static org.apache.aurora.common.args.apt.Configuration.ConfigurationException; + +/** + * A registry of Parsers for different supported argument types. + * + * @author William Farner + */ +public final class Parsers implements ParserOracle { + + public static final Splitter MULTI_VALUE_SPLITTER = + Splitter.on(",").trimResults().omitEmptyStrings(); + + private static final Function> INFO_TO_PARSED_TYPE = + new Function>() { + @Override public Class apply(ParserInfo parserInfo) { + try { + return Class.forName(parserInfo.parsedType); + } catch (ClassNotFoundException e) { + throw new ConfigurationException(e); + } + } + }; + + @VisibleForTesting + static final Function> INFO_TO_PARSER = + new Function>() { + @Override public Parser apply(ParserInfo parserInfo) { + try { + Class parserClass = Class.forName(parserInfo.parserClass); + Constructor constructor = parserClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return (Parser) constructor.newInstance(); + } catch (ClassNotFoundException e) { + throw new ConfigurationException(e); + } catch (InstantiationException e) { + throw new ConfigurationException(e); + } catch (IllegalAccessException e) { + throw new ConfigurationException(e); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(e); + } catch (InvocationTargetException e) { + throw new ConfigurationException(e); + } + } + }; + + private final ImmutableMap, Parser> registry; + + /** + * Creates a new parser registry over the specified {@code parsers}. + * + * @param parsers The parsers to register. + */ + public Parsers(Map, Parser> parsers) { + Preconditions.checkNotNull(parsers); + registry = ImmutableMap.copyOf(parsers); + } + + @Override + public Parser get(TypeToken type) throws IllegalArgumentException { + Parser parser; + Class explicitClass = type.getRawType(); + while (((parser = registry.get(explicitClass)) == null) && (explicitClass != null)) { + explicitClass = explicitClass.getSuperclass(); + } + checkArgument(parser != null, "No parser found for " + type); + + // We control loading of the registry which ensures a proper mapping of class -> parser + @SuppressWarnings("unchecked") + Parser parserT = (Parser) parser; + + return parserT; + } + + static Parsers fromConfiguration(Configuration configuration) { + Map, Parser> parsers = + Maps.transformValues( + Maps.uniqueIndex(configuration.parserInfo(), INFO_TO_PARSED_TYPE), + INFO_TO_PARSER); + return new Parsers(parsers); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/PositionalInfo.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/PositionalInfo.java b/commons/src/main/java/org/apache/aurora/common/args/PositionalInfo.java new file mode 100644 index 0000000..3da1812 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/PositionalInfo.java @@ -0,0 +1,116 @@ +/** + * 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.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.reflect.TypeToken; + +import org.apache.aurora.common.args.apt.Configuration; + +/** + * Description of a positional command line argument. + */ +public final class PositionalInfo extends ArgumentInfo> { + /** + * Factory method to create a PositionalInfo from a field. + * + * @param field The field must contain a {@link Arg Arg<List<?>>}. The List<?> + * represents zero or more positional arguments. + * @return a PositionalInfo describing the field. + */ + static PositionalInfo createFromField(Field field) { + return createFromField(field, null); + } + + /** + * Factory method to create a PositionalInfo from a field. + * + * @param field The field must contain a {@link Arg Arg<List<?>>}. The List<?> + * represents zero or more positional arguments. + * @param instance The object containing the non-static Arg instance or else null if the Arg + * field is static. + * @return a PositionalInfo describing the field. + */ + static PositionalInfo createFromField(Field field, @Nullable Object instance) { + Preconditions.checkNotNull(field); + Positional positional = field.getAnnotation(Positional.class); + if (positional == null) { + throw new Configuration.ConfigurationException( + "No @Positional Arg annotation for field " + field); + } + + Preconditions.checkArgument( + TypeUtil.getRawType(TypeUtil.getTypeParam(field)) == List.class, + "Field is annotated for positional parsing but is not of Arg> type"); + Type nestedType = TypeUtil.extractTypeToken(TypeUtil.getTypeParam(field)); + + @SuppressWarnings({"unchecked", "rawtypes"}) // we have no way to know the type here + PositionalInfo positionalInfo = new PositionalInfo( + field.getDeclaringClass().getCanonicalName() + "." + field.getName(), + "[positional args]", + positional.help(), + ArgumentInfo.getArgForField(field, Optional.fromNullable(instance)), + TypeUtil.getTypeParamTypeToken(field), + TypeToken.of(nestedType), + Arrays.asList(field.getAnnotations()), + positional.parser()); + + return positionalInfo; + } + + private final TypeToken elementType; + + private PositionalInfo( + String canonicalName, + String name, + String help, + Arg> arg, + TypeToken> type, + TypeToken elementType, + List verifierAnnotations, + @Nullable Class>> parser) { + + // TODO: https://github.com/twitter/commons/issues/353, consider future support of + // argFile for Positional arguments. + super(canonicalName, name, help, false, arg, type, verifierAnnotations, parser); + this.elementType = elementType; + } + + /** + * Parses the positional args and stores the results in the {@link Arg} described by this + * {@code PositionalInfo}. + */ + void load(final ParserOracle parserOracle, List positionalArgs) { + final Parser parser = parserOracle.get(elementType); + List assignmentValue = Lists.newArrayList(Iterables.transform(positionalArgs, + new Function() { + @Override public T apply(String argValue) { + return parser.parse(parserOracle, elementType.getType(), argValue); + } + })); + setValue(assignmentValue); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/TypeUtil.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/TypeUtil.java b/commons/src/main/java/org/apache/aurora/common/args/TypeUtil.java new file mode 100644 index 0000000..80cbdd0 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/TypeUtil.java @@ -0,0 +1,120 @@ +/** + * 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.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.reflect.TypeToken; + +/** + * Utility class to extract generic type information. + * + * TODO(William Farner): Move this into a common library, integrate with EasyMockTest.Clazz. + * + * @author William Farner + */ +public final class TypeUtil { + + private static final Function GET_TYPE = new Function() { + @Override public Type apply(Type type) { + if (type instanceof WildcardType) { + return apply(((WildcardType) type).getUpperBounds()[0]); + } + return type; + } + }; + + private TypeUtil() { + // Utility. + } + + /** + * Gets the types that a type is type-parameterized with, in declaration order. + * + * @param type The type to extract type parameters from. + * @return The types that {@code type} is parameterized with. + */ + public static List getTypeParams(Type type) { + if (type instanceof WildcardType) { + return getTypeParams(GET_TYPE.apply(type)); + } + return Lists.transform(Arrays.asList( + ((ParameterizedType) type).getActualTypeArguments()), GET_TYPE); + } + + /** + * Finds the raw class of type. + * + * @param type The type to get the raw class of. + * @return The raw class of type. + */ + public static Class getRawType(Type type) { + if (type instanceof ParameterizedType) { + return getRawType(((ParameterizedType) type).getRawType()); + } + if (type instanceof WildcardType) { + return getRawType(((WildcardType) type).getUpperBounds()[0]); + } + return (Class) type; + } + + /** + * Convenience method to call {@link #getTypeParam(Field)}, with the requirement that there + * is exactly one type parameter on the field. + * + * @param field The field to extract type parameters from. + * @return The raw classes of types that {@code field} is parameterized with. + */ + public static TypeToken getTypeParamTypeToken(Field field) { + List typeParams = getTypeParams(field.getGenericType()); + Preconditions.checkArgument(typeParams.size() == 1, + "Expected exactly one type parameter for field " + field); + return TypeToken.of(typeParams.get(0)); + } + + /** + * Gets the type parameter from a field. Assumes that there is at least one type parameter. + * + * @param field The field to extract the type parameter from. + * @return The field type parameter. + */ + public static Type getTypeParam(Field field) { + return extractTypeToken(field.getGenericType()); + } + + /** + * Extracts the actual type parameter for a singly parameterized type. + * + * @param type The parameterized type to extract the type argument from. + * @return The type of the single specified type parameter for {@code type}. + * @throws IllegalArgumentException if the supplied type does not have exactly one specified type + * parameter + */ + public static Type extractTypeToken(Type type) { + Preconditions.checkNotNull(type); + Preconditions.checkArgument(type instanceof ParameterizedType, "Missing type parameter."); + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + Preconditions.checkArgument(typeArguments.length == 1, + "Expected a type with exactly 1 type argument"); + return typeArguments[0]; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/Verifiers.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/Verifiers.java b/commons/src/main/java/org/apache/aurora/common/args/Verifiers.java new file mode 100644 index 0000000..0212873 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/Verifiers.java @@ -0,0 +1,89 @@ +/** + * 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.lang.annotation.Annotation; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +import org.apache.aurora.common.args.apt.Configuration; +import org.apache.aurora.common.collections.Pair; + +/** + * Utility class to manage relationships between constraints and types. + * + * @author William Farner + */ +public final class Verifiers { + + private final ImmutableMap, Class>, + Verifier> registry; + + private Verifiers(Map, Class>, + Verifier> registry) { + + this.registry = ImmutableMap.copyOf(registry); + } + + @Nullable + Verifier get(TypeToken type, Annotation constraint) { + for (Map.Entry, Class>, Verifier> entry + : registry.entrySet()) { + if (entry.getKey().getSecond() == constraint.annotationType() + && entry.getKey().getFirst().isAssignableFrom(type.getRawType())) { + + // We control the registry which ensures a proper mapping of class -> verifier. + @SuppressWarnings("unchecked") + Verifier verifier = (Verifier) entry.getValue(); + return verifier; + } + } + + return null; + } + + static Verifiers fromConfiguration(Configuration configuration) { + ImmutableMap.Builder, Class>, + Verifier> registry = ImmutableMap.builder(); + + for (Configuration.VerifierInfo info : configuration.verifierInfo()) { + Class verifiedType = forName(info.verifiedType); + Class verifyingAnnotation = forName(info.verifyingAnnotation); + Class> verifierClass = forName(info.verifierClass); + try { + registry.put( + Pair., Class>of(verifiedType, verifyingAnnotation), + verifierClass.newInstance()); + } catch (InstantiationException e) { + throw new Configuration.ConfigurationException(e); + } catch (IllegalAccessException e) { + throw new Configuration.ConfigurationException(e); + } + } + return new Verifiers(registry.build()); + } + + @SuppressWarnings("unchecked") + private static Class forName(String name) { + try { + return (Class) Class.forName(name); + } catch (ClassNotFoundException e) { + throw new Configuration.ConfigurationException(e); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecute.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecute.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecute.java new file mode 100644 index 0000000..a26b8a2 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecute.java @@ -0,0 +1,30 @@ +/** + * 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.constraints; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that indicates an entity must be executable. + * + * @author Steven Nie + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface CanExecute { +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecuteFileVerifier.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecuteFileVerifier.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecuteFileVerifier.java new file mode 100644 index 0000000..5d9b360 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanExecuteFileVerifier.java @@ -0,0 +1,40 @@ +/** + * 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.constraints; + +import java.io.File; +import java.lang.annotation.Annotation; + +import org.apache.aurora.common.args.Verifier; +import org.apache.aurora.common.args.VerifierFor; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Verifier to ensure that a file is executable. + * + * @author Steven Nie + */ +@VerifierFor(CanExecute.class) +public class CanExecuteFileVerifier implements Verifier { + @Override + public void verify(File value, Annotation annotation) { + checkArgument(value.canExecute(), "File must be executable"); + } + + @Override + public String toString(Class argType, Annotation annotation) { + return "file must be executable"; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanRead.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanRead.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanRead.java new file mode 100644 index 0000000..3fef6a9 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanRead.java @@ -0,0 +1,30 @@ +/** + * 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.constraints; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that indicates an entity must be readable. + * + * @author William Farner + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface CanRead { +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanReadFileVerifier.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanReadFileVerifier.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanReadFileVerifier.java new file mode 100644 index 0000000..8c26304 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanReadFileVerifier.java @@ -0,0 +1,40 @@ +/** + * 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.constraints; + +import java.io.File; +import java.lang.annotation.Annotation; + +import org.apache.aurora.common.args.Verifier; +import org.apache.aurora.common.args.VerifierFor; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Verifier to ensure that a file is readable. + * + * @author William Farner + */ +@VerifierFor(CanRead.class) +public class CanReadFileVerifier implements Verifier { + @Override + public void verify(File value, Annotation annotation) { + checkArgument(value.canRead(), "File must be readable"); + } + + @Override + public String toString(Class argType, Annotation annotation) { + return "file must be readable"; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWrite.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWrite.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWrite.java new file mode 100644 index 0000000..c2beeeb --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWrite.java @@ -0,0 +1,30 @@ +/** + * 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.constraints; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that indicates an entity must be writable. + * + * @author William Farner + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface CanWrite { +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWriteFileVerifier.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWriteFileVerifier.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWriteFileVerifier.java new file mode 100644 index 0000000..eac2738 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/CanWriteFileVerifier.java @@ -0,0 +1,40 @@ +/** + * 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.constraints; + +import java.io.File; +import java.lang.annotation.Annotation; + +import org.apache.aurora.common.args.Verifier; +import org.apache.aurora.common.args.VerifierFor; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Verifier to ensure that a file can be written to. + * + * @author William Farner + */ +@VerifierFor(CanWrite.class) +public class CanWriteFileVerifier implements Verifier { + @Override + public void verify(File value, Annotation annotation) { + checkArgument(value.canWrite(), "File must be writable."); + } + + @Override + public String toString(Class argType, Annotation annotation) { + return "file must be writeable"; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/Exists.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/Exists.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/Exists.java new file mode 100644 index 0000000..217d10e --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/Exists.java @@ -0,0 +1,30 @@ +/** + * 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.constraints; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that indicates an entity must exist. + * + * @author William Farner + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface Exists { +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/ExistsFileVerifier.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/ExistsFileVerifier.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/ExistsFileVerifier.java new file mode 100644 index 0000000..e79f547 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/ExistsFileVerifier.java @@ -0,0 +1,40 @@ +/** + * 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.constraints; + +import java.io.File; +import java.lang.annotation.Annotation; + +import org.apache.aurora.common.args.Verifier; +import org.apache.aurora.common.args.VerifierFor; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Verifier to ensure that a file exists. + * + * @author William Farner + */ +@VerifierFor(Exists.class) +public class ExistsFileVerifier implements Verifier { + @Override + public void verify(File value, Annotation annotation) { + checkArgument(value.exists(), "file must exist"); + } + + @Override + public String toString(Class argType, Annotation annotation) { + return "file must exist"; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/args/constraints/IsDirectory.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/args/constraints/IsDirectory.java b/commons/src/main/java/org/apache/aurora/common/args/constraints/IsDirectory.java new file mode 100644 index 0000000..d909994 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/args/constraints/IsDirectory.java @@ -0,0 +1,30 @@ +/** + * 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.constraints; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that indicates an entity must represent a directory. + * + * @author William Farner + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface IsDirectory { +}