aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From zma...@apache.org
Subject [30/51] [partial] aurora git commit: Move packages from com.twitter.common to org.apache.aurora.common
Date Wed, 26 Aug 2015 21:00:20 GMT
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<Field> 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<Field> selectPackage(final Package pkg) {
+    Preconditions.checkNotNull(pkg);
+    return new Predicate<Field>() {
+      @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<Field> selectAllPackagesUnderHere(final Package pkg) {
+    Preconditions.checkNotNull(pkg);
+    final String prefix = pkg.getName() + '.';
+    return Predicates.or(selectPackage(pkg), new Predicate<Field>() {
+      @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<Field> selectClass(final Class<?> clazz) {
+    Preconditions.checkNotNull(clazz);
+    return new Predicate<Field>() {
+      @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<Field> selectClasses(final Class<?> ... cls) {
+    Preconditions.checkNotNull(cls);
+    final Set<Class<?>> listOfClasses = ImmutableSet.copyOf(cls);
+    return new Predicate<Field>() {
+      @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<Field> selectCmdLineArg(Class<?> clazz, final String name) {
+    MorePreconditions.checkNotBlank(name);
+    return Predicates.and(selectClass(clazz), new Predicate<Field>() {
+      @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<OptionInfo<?>, String> GET_OPTION_INFO_NAME =
+      new Function<OptionInfo<?>, String>() {
+        @Override public String apply(OptionInfo<?> optionInfo) {
+          return optionInfo.getName();
+        }
+      };
+
+  public static final Ordering<OptionInfo<?>> ORDER_BY_NAME =
+      Ordering.natural().onResultOf(GET_OPTION_INFO_NAME);
+
+  private static final Function<String, String> ARG_NAME_TO_FLAG = new Function<String, String>() {
+    @Override public String apply(String argName) {
+      return "-" + argName;
+    }
+  };
+
+  private static final Predicate<OptionInfo<?>> IS_BOOLEAN =
+      new Predicate<OptionInfo<?>>() {
+        @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<OptionInfo<?>, String> GET_OPTION_INFO_NEGATED_NAME =
+      new Function<OptionInfo<?>, 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<OptionInfo<?>, String> GET_CANONICAL_ARG_NAME =
+      new Function<OptionInfo<?>, String>() {
+        @Override public String apply(OptionInfo<?> optionInfo) {
+          return optionInfo.getCanonicalName();
+        }
+      };
+
+  /**
+   * Gets the canonical negated name for an @Arg.
+   */
+  private static final Function<OptionInfo<?>, String> GET_CANONICAL_NEGATED_ARG_NAME =
+      new Function<OptionInfo<?>, 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<String> 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<Field> filter, Iterable<String> args) {
+    Preconditions.checkNotNull(filter);
+    ImmutableList<String> 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<String> args) {
+    Preconditions.checkNotNull(argsInfo);
+    ImmutableList<String> arguments = ImmutableList.copyOf(args);
+
+    ParserOracle parserOracle = Parsers.fromConfiguration(argsInfo.getConfiguration());
+    Verifiers verifiers = Verifiers.fromConfiguration(argsInfo.getConfiguration());
+    Pair<ImmutableMap<String, String>, List<String>> 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<String> joinKeysToValues(Iterable<String> args) {
+    List<String> 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<ImmutableMap<String, String>, List<String>> mapArguments(
+      Iterable<String> args) {
+
+    ImmutableMap.Builder<String, String> argMap = ImmutableMap.builder();
+    List<String> 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 <T> Set<T> dropCollisions(Iterable<T> input) {
+    Set<T> copy = Sets.newHashSet();
+    Set<T> collisions = Sets.newHashSet();
+    for (T entry : input) {
+      if (!copy.add(entry)) {
+        collisions.add(entry);
+      }
+    }
+
+    copy.removeAll(collisions);
+    return copy;
+  }
+
+  private static Set<String> getNoCollisions(Iterable<? extends OptionInfo<?>> optionInfos) {
+    Iterable<String> argShortNames = Iterables.transform(optionInfos, GET_OPTION_INFO_NAME);
+    Iterable<String> argShortNegNames =
+        Iterables.transform(Iterables.filter(optionInfos, IS_BOOLEAN),
+            GET_OPTION_INFO_NEGATED_NAME);
+    Iterable<String> argAllShortNames = Iterables.concat(argShortNames, argShortNegNames);
+    Set<String> argAllShortNamesNoCollisions = dropCollisions(argAllShortNames);
+    Set<String> 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<String, String> args,
+      List<String> positionalArgs) {
+
+    if (!Sets.intersection(args.keySet(), ArgumentInfo.HELP_ARGS).isEmpty()) {
+      printHelp(verifiers, argsInfo);
+      return false;
+    }
+
+    Optional<? extends PositionalInfo<?>> positionalInfoOptional = argsInfo.getPositionalInfo();
+    checkArgument(positionalInfoOptional.isPresent() || positionalArgs.isEmpty(),
+        "Positional arguments have been supplied but there is no Arg annotated to received them.");
+
+    Iterable<? extends OptionInfo<?>> optionInfos = argsInfo.getOptionInfos();
+
+    final Set<String> argsFailedToParse = Sets.newHashSet();
+    final Set<String> argsConstraintsFailed = Sets.newHashSet();
+
+    Set<String> argAllShortNamesNoCollisions = getNoCollisions(optionInfos);
+
+    final Map<String, OptionInfo<?>> argsByName =
+        ImmutableMap.<String, OptionInfo<?>>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<String> 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<String> commandLineArgumentInfos = Sets.newTreeSet();
+
+    Iterable<? extends ArgumentInfo<?>> 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<String, String> warningMessages =
+        ImmutableMultimap.<String, String>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<String, Collection<String>> 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<String> requiredHelps = ImmutableList.builder();
+    ImmutableList.Builder<String> optionalHelps = ImmutableList.builder();
+    Optional<String> firstArgFileArgumentName = Optional.absent();
+    for (OptionInfo<?> optionInfo
+        : ORDER_BY_NAME.immutableSortedCopy(argsInfo.getOptionInfos())) {
+      Arg<?> arg = optionInfo.getArg();
+      Object defaultValue = arg.uncheckedGet();
+      ImmutableList<String> 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<? extends PositionalInfo<?>> positionalInfoOptional = argsInfo.getPositionalInfo();
+    if (positionalInfoOptional.isPresent()) {
+      infoLog("\nPositional args:");
+      PositionalInfo<?> positionalInfo = positionalInfoOptional.get();
+      Arg<?> arg = positionalInfo.getArg();
+      Object defaultValue = arg.uncheckedGet();
+      ImmutableList<String> 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<String> required = requiredHelps.build();
+    if (!required.isEmpty()) {
+      infoLog("\nRequired flags:"); // yes - this should actually throw!
+      infoLog(Joiner.on('\n').join(required));
+    }
+    ImmutableList<String> 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<String> 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<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
+  }
+}

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<T> {
+  static final ImmutableSet<String> 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<T> arg;
+  private final TypeToken<T> type;
+  private final List<Annotation> verifierAnnotations;
+  @Nullable private final Class<? extends Parser<? extends T>> 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<T> arg,
+      TypeToken<T> type,
+      List<Annotation> verifierAnnotations,
+      @Nullable Class<? extends Parser<? extends T>> 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<T> 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<T> 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<? extends T> 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<T> {
+    private final Verifier<? super T> verifier;
+    private final Annotation annotation;
+
+    ValueVerifier(Verifier<? super T> 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<? extends T> rawType) {
+      return verifier.toString(rawType, annotation);
+    }
+  }
+
+  private Iterable<ValueVerifier<T>> getVerifiers(final Verifiers verifierOracle) {
+    Function<Annotation, Optional<ValueVerifier<T>>> toVerifier =
+        new Function<Annotation, Optional<ValueVerifier<T>>>() {
+          @Override public Optional<ValueVerifier<T>> apply(Annotation annotation) {
+            @Nullable Verifier<? super T> verifier = verifierOracle.get(type, annotation);
+            if (verifier != null) {
+              return Optional.of(new ValueVerifier<T>(verifier, annotation));
+            } else {
+              return Optional.absent();
+            }
+          }
+        };
+    return Optional.presentInstances(Iterables.transform(verifierAnnotations, toVerifier));
+  }
+
+  void verify(Verifiers verifierOracle) {
+    @Nullable T value = getArg().uncheckedGet();
+    for (ValueVerifier<T> valueVerifier : getVerifiers(verifierOracle)) {
+      valueVerifier.verify(value);
+    }
+  }
+
+  ImmutableList<String> collectConstraints(Verifiers verifierOracle) {
+    @SuppressWarnings("unchecked") // type.getType() is T
+    final Class<? extends T> rawType = (Class<? extends T>) type.getRawType();
+    return FluentIterable.from(getVerifiers(verifierOracle)).transform(
+        new Function<ValueVerifier<T>, String>() {
+          @Override public String apply(ValueVerifier<T> 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<T> extends ArgumentInfo<T> {
+  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<String, String> canonicalizer = new Function<String, String>() {
+      @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<String, String> canonicalizer;
+
+  private OptionInfo(
+      Function<String, String> canonicalizer,
+      String name,
+      String help,
+      boolean argFile,
+      Arg<T> arg,
+      TypeToken<T> type,
+      List<Annotation> verifierAnnotations,
+      @Nullable Class<? extends Parser<T>> 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<? extends T> 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<ParserInfo, Class<?>> INFO_TO_PARSED_TYPE =
+      new Function<ParserInfo, Class<?>>() {
+        @Override public Class<?> apply(ParserInfo parserInfo) {
+          try {
+            return Class.forName(parserInfo.parsedType);
+          } catch (ClassNotFoundException e) {
+            throw new ConfigurationException(e);
+          }
+        }
+      };
+
+  @VisibleForTesting
+  static final Function<ParserInfo, Parser<?>> INFO_TO_PARSER =
+      new Function<ParserInfo, Parser<?>>() {
+        @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<Class<?>, Parser<?>> registry;
+
+  /**
+   * Creates a new parser registry over the specified {@code parsers}.
+   *
+   * @param parsers The parsers to register.
+   */
+  public Parsers(Map<Class<?>, Parser<?>> parsers) {
+    Preconditions.checkNotNull(parsers);
+    registry = ImmutableMap.copyOf(parsers);
+  }
+
+  @Override
+  public <T> Parser<T> get(TypeToken<T> 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<T> parserT = (Parser<T>) parser;
+
+    return parserT;
+  }
+
+  static Parsers fromConfiguration(Configuration configuration) {
+    Map<Class<?>, 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<T> extends ArgumentInfo<List<T>> {
+  /**
+   * Factory method to create a PositionalInfo from a field.
+   *
+   * @param field The field must contain a {@link Arg Arg&lt;List&lt;?&gt;&gt;}. The List&lt;?&gt;
+   *     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&lt;List&lt;?&gt;&gt;}. The List&lt;?&gt;
+   *     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<List<?>> 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<T> elementType;
+
+  private PositionalInfo(
+      String canonicalName,
+      String name,
+      String help,
+      Arg<List<T>> arg,
+      TypeToken<List<T>> type,
+      TypeToken<T> elementType,
+      List<Annotation> verifierAnnotations,
+      @Nullable Class<? extends Parser<? extends List<T>>> 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<String> positionalArgs) {
+    final Parser<? extends T> parser = parserOracle.get(elementType);
+    List<T> assignmentValue = Lists.newArrayList(Iterables.transform(positionalArgs,
+      new Function<String, T>() {
+        @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<Type, Type> GET_TYPE = new Function<Type, Type>() {
+    @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<Type> 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<Type> 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<Pair<Class<?>, Class<? extends Annotation>>,
+                             Verifier<?>> registry;
+
+  private Verifiers(Map<Pair<Class<?>, Class<? extends Annotation>>,
+                        Verifier<?>> registry) {
+
+    this.registry = ImmutableMap.copyOf(registry);
+  }
+
+  @Nullable
+  <T> Verifier<T> get(TypeToken<T> type, Annotation constraint) {
+    for (Map.Entry<Pair<Class<?>, Class<? extends Annotation>>, 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<T> verifier = (Verifier<T>) entry.getValue();
+        return verifier;
+      }
+    }
+
+    return null;
+  }
+
+  static Verifiers fromConfiguration(Configuration configuration) {
+    ImmutableMap.Builder<Pair<Class<?>, Class<? extends Annotation>>,
+                         Verifier<?>> registry = ImmutableMap.builder();
+
+    for (Configuration.VerifierInfo info : configuration.verifierInfo()) {
+      Class<?> verifiedType = forName(info.verifiedType);
+      Class<? extends Annotation> verifyingAnnotation = forName(info.verifyingAnnotation);
+      Class<? extends Verifier<?>> verifierClass = forName(info.verifierClass);
+      try {
+        registry.put(
+            Pair.<Class<?>, Class<? extends Annotation>>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 <T> Class<T> forName(String name) {
+    try {
+      return (Class<T>) 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<File> {
+  @Override
+  public void verify(File value, Annotation annotation) {
+    checkArgument(value.canExecute(), "File must be executable");
+  }
+
+  @Override
+  public String toString(Class<? extends File> 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<File> {
+  @Override
+  public void verify(File value, Annotation annotation) {
+    checkArgument(value.canRead(), "File must be readable");
+  }
+
+  @Override
+  public String toString(Class<? extends File> 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<File> {
+  @Override
+  public void verify(File value, Annotation annotation) {
+    checkArgument(value.canWrite(), "File must be writable.");
+  }
+
+  @Override
+  public String toString(Class<? extends File> 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<File> {
+  @Override
+  public void verify(File value, Annotation annotation) {
+    checkArgument(value.exists(), "file must exist");
+  }
+
+  @Override
+  public String toString(Class<? extends File> 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 {
+}


Mime
View raw message