aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From zma...@apache.org
Subject [35/37] aurora git commit: Import of Twitter Commons.
Date Tue, 25 Aug 2015 18:19:49 GMT
http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/Registration.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/http/Registration.java b/commons/src/main/java/com/twitter/common/application/http/Registration.java
new file mode 100644
index 0000000..b17bd85
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/http/Registration.java
@@ -0,0 +1,142 @@
+package com.twitter.common.application.http;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.net.URL;
+
+import javax.servlet.Filter;
+import javax.servlet.http.HttpServlet;
+
+import com.google.common.io.Resources;
+import com.google.inject.Binder;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.multibindings.Multibinder;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Utility class for registering HTTP servlets and assets.
+ */
+public final class Registration {
+
+  private Registration() {
+    // Utility class.
+  }
+
+  /**
+   * Equivalent to
+   * {@code registerServlet(binder, new HttpServletConfig(path, servletClass, silent))}.
+   */
+  public static void registerServlet(Binder binder, String path,
+      Class<? extends HttpServlet> servletClass, boolean silent) {
+    registerServlet(binder, new HttpServletConfig(path, servletClass, silent));
+  }
+
+  /**
+   * Registers a binding for an {@link javax.servlet.http.HttpServlet} to be exported at a specified
+   * path.
+   *
+   * @param binder a guice binder to register the handler with
+   * @param config a servlet mounting specification
+   */
+  public static void registerServlet(Binder binder, HttpServletConfig config) {
+    Multibinder.newSetBinder(binder, HttpServletConfig.class).addBinding().toInstance(config);
+  }
+
+  /**
+   * A binding annotation applied to the set of additional index page links bound via
+   * {@link #Registration#registerEndpoint()}
+   */
+  @BindingAnnotation
+  @Target({FIELD, PARAMETER, METHOD})
+  @Retention(RUNTIME)
+  public @interface IndexLink { }
+
+  /**
+   * Gets the multibinder used to bind links on the root servlet.
+   * The resulting {@link java.util.Set} is bound with the {@link IndexLink} annotation.
+   *
+   * @param binder a guice binder to associate the multibinder with.
+   * @return The multibinder to bind index links against.
+   */
+  public static Multibinder<String> getEndpointBinder(Binder binder) {
+    return Multibinder.newSetBinder(binder, String.class, IndexLink.class);
+  }
+
+  /**
+   * Registers a link to display on the root servlet.
+   *
+   * @param binder a guice binder to register the link with.
+   * @param endpoint Endpoint URI to include.
+   */
+  public static void registerEndpoint(Binder binder, String endpoint) {
+    getEndpointBinder(binder).addBinding().toInstance(endpoint);
+  }
+
+  /**
+   * Registers a binding for a URL asset to be served by the HTTP server, with an optional
+   * entity tag for cache control.
+   *
+   * @param binder a guice binder to register the handler with
+   * @param servedPath Path to serve the resource from in the HTTP server.
+   * @param asset Resource to be served.
+   * @param assetType MIME-type for the asset.
+   * @param silent Whether the server should hide this asset on the index page.
+   */
+  public static void registerHttpAsset(Binder binder, String servedPath, URL asset,
+      String assetType, boolean silent) {
+    Multibinder.newSetBinder(binder, HttpAssetConfig.class).addBinding().toInstance(
+        new HttpAssetConfig(servedPath, asset, assetType, silent));
+  }
+
+  /**
+   * Registers a binding for a classpath resource to be served by the HTTP server, using a resource
+   * path relative to a class.
+   *
+   * @param binder a guice binder to register the handler with
+   * @param servedPath Path to serve the asset from in the HTTP server.
+   * @param contextClass Context class for defining the relative path to the asset.
+   * @param assetRelativePath Path to the served asset, relative to {@code contextClass}.
+   * @param assetType MIME-type for the asset.
+   * @param silent Whether the server should hide this asset on the index page.
+   */
+  public static void registerHttpAsset(
+      Binder binder,
+      String servedPath,
+      Class<?> contextClass,
+      String assetRelativePath,
+      String assetType,
+      boolean silent) {
+
+    registerHttpAsset(binder, servedPath, Resources.getResource(contextClass, assetRelativePath),
+        assetType, silent);
+  }
+
+  /**
+   * Gets the multibinder used to bind HTTP filters.
+   *
+   * @param binder a guice binder to associate the multibinder with.
+   * @return The multibinder to bind HTTP filter configurations against.
+   */
+  public static Multibinder<HttpFilterConfig> getFilterBinder(Binder binder) {
+    return Multibinder.newSetBinder(binder, HttpFilterConfig.class);
+  }
+
+  /**
+   * Registers an HTTP servlet filter.
+   *
+   * @param binder a guice binder to register the filter with.
+   * @param filterClass Filter class to register.
+   * @param pathSpec Path spec that the filter should be activated on.
+   */
+  public static void registerServletFilter(
+      Binder binder,
+      Class<? extends Filter> filterClass,
+      String pathSpec) {
+
+    getFilterBinder(binder).addBinding().toInstance(new HttpFilterConfig(filterClass, pathSpec));
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/AppLauncherModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/AppLauncherModule.java b/commons/src/main/java/com/twitter/common/application/modules/AppLauncherModule.java
new file mode 100644
index 0000000..0145e02
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/AppLauncherModule.java
@@ -0,0 +1,53 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+
+import com.twitter.common.stats.Stats;
+import com.twitter.common.util.BuildInfo;
+
+/**
+ * Binding module for the bare minimum requirements for the
+ * {@link com.twitter.common.application.AppLauncher}.
+ *
+ * @author William Farner
+ */
+public class AppLauncherModule extends AbstractModule {
+
+  private static final Logger LOG = Logger.getLogger(AppLauncherModule.class.getName());
+  private static final AtomicLong UNCAUGHT_EXCEPTIONS = Stats.exportLong("uncaught_exceptions");
+
+  @Override
+  protected void configure() {
+    bind(BuildInfo.class).in(Singleton.class);
+    bind(UncaughtExceptionHandler.class).to(LoggingExceptionHandler.class);
+  }
+
+  public static class LoggingExceptionHandler implements UncaughtExceptionHandler {
+    @Override public void uncaughtException(Thread t, Throwable e) {
+      UNCAUGHT_EXCEPTIONS.incrementAndGet();
+      LOG.log(Level.SEVERE, "Uncaught exception from " + t + ":" + e, e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/LifecycleModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/LifecycleModule.java b/commons/src/main/java/com/twitter/common/application/modules/LifecycleModule.java
new file mode 100644
index 0000000..49f4780
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/LifecycleModule.java
@@ -0,0 +1,198 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.Multibinder;
+
+import com.twitter.common.application.Lifecycle;
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.application.ShutdownRegistry.ShutdownRegistryImpl;
+import com.twitter.common.application.ShutdownStage;
+import com.twitter.common.application.StartupRegistry;
+import com.twitter.common.application.StartupStage;
+import com.twitter.common.application.modules.LocalServiceRegistry.LocalService;
+import com.twitter.common.base.Command;
+import com.twitter.common.base.ExceptionalCommand;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Binding module for startup and shutdown controller and registries.
+ *
+ * Bindings provided by this module:
+ * <ul>
+ *   <li>{@code @StartupStage ExceptionalCommand} - Command to execute all startup actions.
+ *   <li>{@code ShutdownRegistry} - Registry for adding shutdown actions.
+ *   <li>{@code @ShutdownStage Command} - Command to execute all shutdown commands.
+ * </ul>
+ *
+ * If you would like to register a startup action that starts a local network service, please
+ * consider using {@link LocalServiceRegistry}.
+ *
+ * @author William Farner
+ */
+public class LifecycleModule extends AbstractModule {
+
+  /**
+   * Binding annotation used for local services.
+   * This is used to ensure the LocalService bindings are visibile within the package only, to
+   * prevent injection inadvertently triggering a service launch.
+   */
+  @BindingAnnotation
+  @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
+  @interface Service { }
+
+  @Override
+  protected void configure() {
+    bind(Lifecycle.class).in(Singleton.class);
+
+    bind(Key.get(ExceptionalCommand.class, StartupStage.class)).to(StartupRegistry.class);
+    bind(StartupRegistry.class).in(Singleton.class);
+
+    bind(ShutdownRegistry.class).to(ShutdownRegistryImpl.class);
+    bind(Key.get(Command.class, ShutdownStage.class)).to(ShutdownRegistryImpl.class);
+    bind(ShutdownRegistryImpl.class).in(Singleton.class);
+    bindStartupAction(binder(), ShutdownHookRegistration.class);
+
+    bind(LocalServiceRegistry.class).in(Singleton.class);
+
+    // Ensure that there is at least an empty set for the service runners.
+    runnerBinder(binder());
+
+    bindStartupAction(binder(), LocalServiceLauncher.class);
+  }
+
+  /**
+   * Thrown when a local service fails to launch.
+   */
+  public static class LaunchException extends Exception {
+    public LaunchException(String msg) {
+      super(msg);
+    }
+
+    public LaunchException(String msg, Throwable cause) {
+      super(msg, cause);
+    }
+  }
+
+  /**
+   * Responsible for starting and stopping a local service.
+   */
+  public interface ServiceRunner {
+
+    /**
+     * Launches the local service.
+     *
+     * @return Information about the launched service.
+     * @throws LaunchException If the service failed to launch.
+     */
+    LocalService launch() throws LaunchException;
+  }
+
+  @VisibleForTesting
+  static Multibinder<ServiceRunner> runnerBinder(Binder binder) {
+    return Multibinder.newSetBinder(binder, ServiceRunner.class, Service.class);
+  }
+
+  /**
+   * Binds a service runner that will start and stop a local service.
+   *
+   * @param binder Binder to bind against.
+   * @param launcher Launcher class for a service.
+   */
+  public static void bindServiceRunner(Binder binder, Class<? extends ServiceRunner> launcher) {
+    runnerBinder(binder).addBinding().to(launcher);
+    binder.bind(launcher).in(Singleton.class);
+  }
+
+  /**
+   * Binds a local service instance, without attaching an explicit lifecycle.
+   *
+   * @param binder Binder to bind against.
+   * @param service Local service instance to bind.
+   */
+  public static void bindLocalService(Binder binder, final LocalService service) {
+    runnerBinder(binder).addBinding().toInstance(
+        new ServiceRunner() {
+          @Override public LocalService launch() {
+            return service;
+          }
+        });
+  }
+
+  /**
+   * Adds a startup action to the startup registry binding.
+   *
+   * @param binder Binder to bind against.
+   * @param actionClass Class to bind (and instantiate via guice) for execution at startup.
+   */
+  public static void bindStartupAction(Binder binder,
+      Class<? extends ExceptionalCommand> actionClass) {
+
+    Multibinder.newSetBinder(binder, ExceptionalCommand.class, StartupStage.class)
+        .addBinding().to(actionClass);
+  }
+
+  /**
+   * Startup command to register the shutdown registry as a process shutdown hook.
+   */
+  private static class ShutdownHookRegistration implements Command {
+    private final Command shutdownCommand;
+
+    @Inject ShutdownHookRegistration(@ShutdownStage Command shutdownCommand) {
+      this.shutdownCommand = checkNotNull(shutdownCommand);
+    }
+
+    @Override public void execute() {
+      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+        @Override public void run() {
+          shutdownCommand.execute();
+        }
+      }, "ShutdownRegistry-Hook"));
+    }
+  }
+
+  /**
+   * Startup command that ensures startup and shutdown of local services.
+   */
+  private static class LocalServiceLauncher implements Command {
+    private final LocalServiceRegistry serviceRegistry;
+
+    @Inject LocalServiceLauncher(LocalServiceRegistry serviceRegistry) {
+      this.serviceRegistry = checkNotNull(serviceRegistry);
+    }
+
+    @Override public void execute() {
+      serviceRegistry.ensureLaunched();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/LocalServiceRegistry.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/LocalServiceRegistry.java b/commons/src/main/java/com/twitter/common/application/modules/LocalServiceRegistry.java
new file mode 100644
index 0000000..63f50cb
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/LocalServiceRegistry.java
@@ -0,0 +1,261 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.application.modules.LifecycleModule.LaunchException;
+import com.twitter.common.application.modules.LifecycleModule.Service;
+import com.twitter.common.application.modules.LifecycleModule.ServiceRunner;
+import com.twitter.common.base.Command;
+import com.twitter.common.base.MorePreconditions;
+import com.twitter.common.net.InetSocketAddressHelper;
+
+/**
+ * Registry for services that should be exported from the application.
+ *
+ * Example of announcing and registering a port:
+ * <pre>
+ * class MyLauncher implements Provider<LocalService> {
+ *   public LocalService get() {
+ *     // Launch service.
+ *   }
+ * }
+ *
+ * class MyServiceModule extends AbstractModule {
+ *   public void configure() {
+ *     LifeCycleModule.bindServiceLauncher(binder(), MyLauncher.class);
+ *   }
+ * }
+ * </pre>
+ */
+public class LocalServiceRegistry {
+
+  private static final Predicate<LocalService> IS_PRIMARY = new Predicate<LocalService>() {
+    @Override public boolean apply(LocalService service) {
+      return service.primary;
+    }
+  };
+
+  private static final Function<LocalService, InetSocketAddress> SERVICE_TO_SOCKET =
+      new Function<LocalService, InetSocketAddress>() {
+        @Override public InetSocketAddress apply(LocalService service) {
+          try {
+            return InetSocketAddressHelper.getLocalAddress(service.port);
+          } catch (UnknownHostException e) {
+            throw new RuntimeException("Failed to resolve local address for " + service, e);
+          }
+        }
+      };
+
+  private static final Function<LocalService, String> GET_NAME =
+      new Function<LocalService, String>() {
+        @Override public String apply(LocalService service) {
+          return Iterables.getOnlyElement(service.names);
+        }
+      };
+
+  private final ShutdownRegistry shutdownRegistry;
+  private final Provider<Set<ServiceRunner>> runnerProvider;
+
+  private Optional<InetSocketAddress> primarySocket = null;
+  private Map<String, InetSocketAddress> auxiliarySockets = null;
+
+  /**
+   * Creates a new local service registry.
+   *
+   * @param runnerProvider provider of registered local services.
+   * @param shutdownRegistry Shutdown registry to tear down launched services.
+   */
+  @Inject
+  public LocalServiceRegistry(@Service Provider<Set<ServiceRunner>> runnerProvider,
+      ShutdownRegistry shutdownRegistry) {
+    this.runnerProvider = Preconditions.checkNotNull(runnerProvider);
+    this.shutdownRegistry = Preconditions.checkNotNull(shutdownRegistry);
+  }
+
+  private static final Function<LocalService, Iterable<LocalService>> AUX_NAME_BREAKOUT =
+      new Function<LocalService, Iterable<LocalService>>() {
+        @Override public Iterable<LocalService> apply(final LocalService service) {
+          Preconditions.checkArgument(!service.primary);
+          Function<String, LocalService> oneNameService = new Function<String, LocalService>() {
+            @Override public LocalService apply(String name) {
+              return LocalService.auxiliaryService(name, service.port, service.shutdownCommand);
+            }
+          };
+          return Iterables.transform(service.names, oneNameService);
+        }
+      };
+
+  /**
+   * Launches the local services if not already launched, otherwise this is a no-op.
+   */
+  void ensureLaunched() {
+    if (primarySocket == null) {
+      ImmutableList.Builder<LocalService> builder = ImmutableList.builder();
+
+      for (ServiceRunner runner : runnerProvider.get()) {
+        try {
+          LocalService service = runner.launch();
+          builder.add(service);
+          shutdownRegistry.addAction(service.shutdownCommand);
+        } catch (LaunchException e) {
+          throw new IllegalStateException("Failed to launch " + runner, e);
+        }
+      }
+
+      List<LocalService> localServices = builder.build();
+      Iterable<LocalService> primaries = Iterables.filter(localServices, IS_PRIMARY);
+      switch (Iterables.size(primaries)) {
+        case 0:
+          primarySocket = Optional.absent();
+          break;
+
+        case 1:
+          primarySocket = Optional.of(SERVICE_TO_SOCKET.apply(Iterables.getOnlyElement(primaries)));
+          break;
+
+        default:
+          throw new IllegalArgumentException("More than one primary local service: " + primaries);
+      }
+
+      Iterable<LocalService> auxSinglyNamed = Iterables.concat(
+          FluentIterable.from(localServices)
+              .filter(Predicates.not(IS_PRIMARY))
+              .transform(AUX_NAME_BREAKOUT));
+
+      Map<String, LocalService> byName;
+      try {
+        byName = Maps.uniqueIndex(auxSinglyNamed, GET_NAME);
+      } catch (IllegalArgumentException e) {
+        throw new IllegalArgumentException("Auxiliary services with identical names.", e);
+      }
+
+      auxiliarySockets = ImmutableMap.copyOf(Maps.transformValues(byName, SERVICE_TO_SOCKET));
+    }
+  }
+
+  /**
+   * Gets the mapping from auxiliary port name to socket.
+   *
+   * @return Auxiliary port mapping.
+   */
+  public synchronized Map<String, InetSocketAddress> getAuxiliarySockets() {
+    ensureLaunched();
+    return auxiliarySockets;
+  }
+
+  /**
+   * Gets the optional primary socket address, and returns an unresolved local socket address
+   * representing that port.
+   *
+   * @return Local socket address for the primary port.
+   * @throws IllegalStateException If the primary port was not set.
+   */
+  public synchronized Optional<InetSocketAddress> getPrimarySocket() {
+    ensureLaunched();
+    return primarySocket;
+  }
+
+  /**
+   * An individual local service.
+   */
+  public static final class LocalService {
+    private final boolean primary;
+    private final Set<String> names;
+    private final int port;
+    private final Command shutdownCommand;
+
+    private LocalService(boolean primary, Set<String> names, int port,
+        Command shutdownCommand) {
+      this.primary = primary;
+      this.names = names;
+      this.port = port;
+      this.shutdownCommand = Preconditions.checkNotNull(shutdownCommand);
+    }
+
+    @Override
+    public String toString() {
+      return new ToStringBuilder(this)
+          .append("primary", primary)
+          .append("name", names)
+          .append("port", port)
+          .toString();
+    }
+
+    /**
+     * Creates a primary local service.
+     *
+     * @param port Service port.
+     * @param shutdownCommand A command that will shut down the service.
+     * @return A new primary local service.
+     */
+    public static LocalService primaryService(int port, Command shutdownCommand) {
+      return new LocalService(true, ImmutableSet.<String>of(), port, shutdownCommand);
+    }
+
+    /**
+     * Creates a named auxiliary service.
+     *
+     * @param name Service name.
+     * @param port Service port.
+     * @param shutdownCommand A command that will shut down the service.
+     * @return A new auxiliary local service.
+     */
+    public static LocalService auxiliaryService(String name, int port, Command shutdownCommand) {
+      return auxiliaryService(ImmutableSet.of(name), port, shutdownCommand);
+    }
+
+    /**
+     * Creates an auxiliary service identified by multiple names.
+     *
+     * @param names Service names.
+     * @param port Service port.
+     * @param shutdownCommand A command that will shut down the service.
+     * @return A new auxiliary local service.
+     */
+    public static LocalService auxiliaryService(
+        Set<String> names,
+        int port,
+        Command shutdownCommand) {
+
+      MorePreconditions.checkNotBlank(names);
+      return new LocalService(false, names, port, shutdownCommand);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/LogModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/LogModule.java b/commons/src/main/java/com/twitter/common/application/modules/LogModule.java
new file mode 100644
index 0000000..b019c3e
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/LogModule.java
@@ -0,0 +1,120 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.io.File;
+import java.util.logging.Logger;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import com.twitter.common.args.Arg;
+import com.twitter.common.args.CmdLine;
+import com.twitter.common.args.constraints.CanRead;
+import com.twitter.common.args.constraints.Exists;
+import com.twitter.common.args.constraints.IsDirectory;
+import com.twitter.common.base.Command;
+import com.twitter.common.logging.LogUtil;
+import com.twitter.common.logging.RootLogConfig;
+import com.twitter.common.logging.RootLogConfig.Configuration;
+import com.twitter.common.net.http.handlers.LogPrinter;
+import com.twitter.common.stats.StatImpl;
+import com.twitter.common.stats.Stats;
+
+/**
+ * Binding module for logging-related bindings, such as the log directory.
+ *
+ * This module uses a single optional command line argument 'log_dir'.  If unset, the logging
+ * directory will be auto-discovered via:
+ * {@link com.twitter.common.logging.LogUtil#getLogManagerLogDir()}.
+ *
+ * Bindings provided by this module:
+ * <ul>
+ *   <li>{@code @Named(LogPrinter.LOG_DIR_KEY) File} - Log directory.
+ *   <li>{@code Optional&lt;RootLogConfig.Configuraton&gt;} - If glog is enabled the configuration
+ *       used.
+ * </ul>
+ *
+ * Default bindings that may be overridden:
+ * <ul>
+ *   <li>Log directory: directory where application logs are written.  May be overridden by binding
+ *       to: {@code bind(File.class).annotatedWith(Names.named(LogPrinter.LOG_DIR_KEY))}.
+ * </ul>
+ *
+ * @author William Farner
+ */
+public class LogModule extends AbstractModule {
+
+  private static final Logger LOG = Logger.getLogger(LogModule.class.getName());
+
+  @Exists
+  @CanRead
+  @IsDirectory
+  @CmdLine(name = "log_dir",
+           help = "The directory where application logs are written.")
+  private static final Arg<File> LOG_DIR = Arg.create(null);
+
+  @CmdLine(name = "use_glog",
+           help = "True to use the new glog-based configuration for the root logger.")
+  private static final Arg<Boolean> USE_GLOG = Arg.create(true);
+
+  @Override
+  protected void configure() {
+    // Bind the default log directory.
+    bind(File.class).annotatedWith(Names.named(LogPrinter.LOG_DIR_KEY)).toInstance(getLogDir());
+
+    LifecycleModule.bindStartupAction(binder(), ExportLogDir.class);
+
+    Configuration configuration = null;
+    if (USE_GLOG.get()) {
+      configuration = RootLogConfig.configurationFromFlags();
+      configuration.apply();
+    }
+    bind(new TypeLiteral<Optional<Configuration>>() { })
+        .toInstance(Optional.fromNullable(configuration));
+  }
+
+  private File getLogDir() {
+    File logDir = LOG_DIR.get();
+    if (logDir == null) {
+      logDir = LogUtil.getLogManagerLogDir();
+      LOG.info("From logging properties, parsed log directory " + logDir.getAbsolutePath());
+    }
+    return logDir;
+  }
+
+  public static final class ExportLogDir implements Command {
+    private final File logDir;
+
+    @Inject ExportLogDir(@Named(LogPrinter.LOG_DIR_KEY) final File logDir) {
+      this.logDir = Preconditions.checkNotNull(logDir);
+    }
+
+    @Override public void execute() {
+      Stats.exportStatic(new StatImpl<String>("logging_dir") {
+        @Override public String read() {
+          return logDir.getAbsolutePath();
+        }
+      });
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/StatsExportModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/StatsExportModule.java b/commons/src/main/java/com/twitter/common/application/modules/StatsExportModule.java
new file mode 100644
index 0000000..82e4cf0
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/StatsExportModule.java
@@ -0,0 +1,88 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.args.Arg;
+import com.twitter.common.args.CmdLine;
+import com.twitter.common.base.Closure;
+import com.twitter.common.base.Command;
+import com.twitter.common.quantity.Amount;
+import com.twitter.common.quantity.Time;
+import com.twitter.common.stats.NumericStatExporter;
+
+/**
+ * Module to enable periodic exporting of registered stats to an external service.
+ *
+ * This modules supports a single command line argument, {@code stat_export_interval}, which
+ * controls the export interval (defaulting to 1 minute).
+ *
+ * Bindings required by this module:
+ * <ul>
+ *   <li>{@code @ShutdownStage ShutdownRegistry} - Shutdown action registry.
+ * </ul>
+ *
+ * @author William Farner
+ */
+public class StatsExportModule extends AbstractModule {
+
+  @CmdLine(name = "stat_export_interval",
+           help = "Amount of time to wait between stat exports.")
+  private static final Arg<Amount<Long, Time>> EXPORT_INTERVAL =
+      Arg.create(Amount.of(1L, Time.MINUTES));
+
+  @Override
+  protected void configure() {
+    requireBinding(Key.get(new TypeLiteral<Closure<Map<String, ? extends Number>>>() { }));
+    LifecycleModule.bindStartupAction(binder(), StartCuckooExporter.class);
+  }
+
+  public static final class StartCuckooExporter implements Command {
+
+    private final Closure<Map<String, ? extends Number>> statSink;
+    private final ShutdownRegistry shutdownRegistry;
+
+    @Inject StartCuckooExporter(
+        Closure<Map<String, ? extends Number>> statSink,
+        ShutdownRegistry shutdownRegistry) {
+
+      this.statSink = Preconditions.checkNotNull(statSink);
+      this.shutdownRegistry = Preconditions.checkNotNull(shutdownRegistry);
+    }
+
+    @Override public void execute() {
+      ThreadFactory threadFactory =
+          new ThreadFactoryBuilder().setNameFormat("CuckooExporter-%d").setDaemon(true).build();
+
+      final NumericStatExporter exporter = new NumericStatExporter(statSink,
+          Executors.newScheduledThreadPool(1, threadFactory), EXPORT_INTERVAL.get());
+
+      exporter.start(shutdownRegistry);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/StatsModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/StatsModule.java b/commons/src/main/java/com/twitter/common/application/modules/StatsModule.java
new file mode 100644
index 0000000..4262aa7
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/StatsModule.java
@@ -0,0 +1,149 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import com.google.common.base.Supplier;
+import com.google.common.primitives.Longs;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+import com.twitter.common.application.ShutdownRegistry;
+import com.twitter.common.args.Arg;
+import com.twitter.common.args.CmdLine;
+import com.twitter.common.base.Command;
+import com.twitter.common.quantity.Amount;
+import com.twitter.common.quantity.Time;
+import com.twitter.common.stats.JvmStats;
+import com.twitter.common.stats.Stat;
+import com.twitter.common.stats.StatImpl;
+import com.twitter.common.stats.StatRegistry;
+import com.twitter.common.stats.Stats;
+import com.twitter.common.stats.TimeSeriesRepository;
+import com.twitter.common.stats.TimeSeriesRepositoryImpl;
+import com.twitter.common.util.BuildInfo;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Binding module for injections related to the in-process stats system.
+ *
+ * This modules supports two command line arguments:
+ * <ul>
+ *   <li>{@code stat_sampling_interval} - Statistic value sampling interval.
+ *   <li>{@code stat_retention_period} - Time for a stat to be retained in memory before expring.
+ * </ul>
+ *
+ * Bindings required by this module:
+ * <ul>
+ *   <li>{@code ShutdownRegistry} - Shutdown hook registry.
+ *   <li>{@code BuildInfo} - Build information for the application.
+ * </ul>
+ *
+ * @author William Farner
+ */
+public class StatsModule extends AbstractModule {
+
+  @CmdLine(name = "stat_sampling_interval", help = "Statistic value sampling interval.")
+  private static final Arg<Amount<Long, Time>> SAMPLING_INTERVAL =
+      Arg.create(Amount.of(1L, Time.SECONDS));
+
+  @CmdLine(name = "stat_retention_period",
+      help = "Time for a stat to be retained in memory before expiring.")
+  private static final Arg<Amount<Long, Time>> RETENTION_PERIOD =
+      Arg.create(Amount.of(1L, Time.HOURS));
+
+  public static Amount<Long, Time> getSamplingInterval() {
+    return SAMPLING_INTERVAL.get();
+  }
+
+  @Override
+  protected void configure() {
+    requireBinding(ShutdownRegistry.class);
+    requireBinding(BuildInfo.class);
+
+    // Bindings for TimeSeriesRepositoryImpl.
+    bind(StatRegistry.class).toInstance(Stats.STAT_REGISTRY);
+    bind(new TypeLiteral<Amount<Long, Time>>() { })
+        .annotatedWith(Names.named(TimeSeriesRepositoryImpl.SAMPLE_RETENTION_PERIOD))
+        .toInstance(RETENTION_PERIOD.get());
+    bind(new TypeLiteral<Amount<Long, Time>>() { })
+        .annotatedWith(Names.named(TimeSeriesRepositoryImpl.SAMPLE_PERIOD))
+        .toInstance(SAMPLING_INTERVAL.get());
+    bind(TimeSeriesRepository.class).to(TimeSeriesRepositoryImpl.class).in(Singleton.class);
+
+    bind(new TypeLiteral<Supplier<Iterable<Stat<?>>>>() { }).toInstance(
+        new Supplier<Iterable<Stat<?>>>() {
+          @Override public Iterable<Stat<?>> get() {
+            return Stats.getVariables();
+          }
+        }
+    );
+
+    LifecycleModule.bindStartupAction(binder(), StartStatPoller.class);
+  }
+
+  public static final class StartStatPoller implements Command {
+    private static final Logger LOG = Logger.getLogger(StartStatPoller.class.getName());
+    private final ShutdownRegistry shutdownRegistry;
+    private final BuildInfo buildInfo;
+    private final TimeSeriesRepository timeSeriesRepository;
+
+    @Inject StartStatPoller(
+        ShutdownRegistry shutdownRegistry,
+        BuildInfo buildInfo,
+        TimeSeriesRepository timeSeriesRepository) {
+
+      this.shutdownRegistry = checkNotNull(shutdownRegistry);
+      this.buildInfo = checkNotNull(buildInfo);
+      this.timeSeriesRepository = checkNotNull(timeSeriesRepository);
+    }
+
+    @Override public void execute() {
+      Properties properties = buildInfo.getProperties();
+      LOG.info("Build information: " + properties);
+      for (String name : properties.stringPropertyNames()) {
+        final String stringValue = properties.getProperty(name);
+        if (stringValue == null) {
+          continue;
+        }
+        final Long longValue = Longs.tryParse(stringValue);
+        if (longValue != null) {
+          Stats.exportStatic(new StatImpl<Long>(Stats.normalizeName(name)) {
+            @Override public Long read() {
+              return longValue;
+            }
+          });
+        } else {
+          Stats.exportString(new StatImpl<String>(Stats.normalizeName(name)) {
+            @Override public String read() {
+              return stringValue;
+            }
+          });
+        }
+      }
+
+      JvmStats.export();
+      timeSeriesRepository.start(shutdownRegistry);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/modules/ThriftModule.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/application/modules/ThriftModule.java b/commons/src/main/java/com/twitter/common/application/modules/ThriftModule.java
new file mode 100644
index 0000000..f55cafb
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/application/modules/ThriftModule.java
@@ -0,0 +1,44 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.application.modules;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+
+import com.twitter.common.application.http.Registration;
+import com.twitter.common.net.http.handlers.ThriftServlet;
+import com.twitter.common.net.monitoring.TrafficMonitor;
+
+/**
+ * Binding module for thrift traffic monitor servlets, to ensure an empty set is available for
+ * the thrift traffic monitor servlet.
+ *
+ * @author William Farner
+ */
+public class ThriftModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    // Make sure that there is at least an empty set bound to client andserver monitors.
+    Multibinder.newSetBinder(binder(), TrafficMonitor.class,
+        Names.named(ThriftServlet.THRIFT_CLIENT_MONITORS));
+    Multibinder.newSetBinder(binder(), TrafficMonitor.class,
+        Names.named(ThriftServlet.THRIFT_SERVER_MONITORS));
+
+    Registration.registerServlet(binder(), "/thrift", ThriftServlet.class, false);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/args/ArgFilters.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/args/ArgFilters.java b/commons/src/main/java/com/twitter/common/args/ArgFilters.java
new file mode 100644
index 0000000..2b5442b
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/args/ArgFilters.java
@@ -0,0 +1,128 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.args;
+
+import java.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 com.twitter.common.base.MorePreconditions;
+
+/**
+ * Utilities for generating {@literal @CmdLine} {@link Arg} filters suitable for use with
+ * {@link com.twitter.common.args.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 com.twitter.common.args.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/86a547b9/commons/src/main/java/com/twitter/common/args/ArgScanner.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/args/ArgScanner.java b/commons/src/main/java/com/twitter/common/args/ArgScanner.java
new file mode 100644
index 0000000..a6ca87e
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/args/ArgScanner.java
@@ -0,0 +1,563 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.args;
+
+import java.io.IOException;
+import java.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 com.twitter.common.args.Args.ArgsInfo;
+import com.twitter.common.args.apt.Configuration;
+import com.twitter.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 com.twitter.common.args.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/86a547b9/commons/src/main/java/com/twitter/common/args/Args.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/com/twitter/common/args/Args.java b/commons/src/main/java/com/twitter/common/args/Args.java
new file mode 100644
index 0000000..12a2f4b
--- /dev/null
+++ b/commons/src/main/java/com/twitter/common/args/Args.java
@@ -0,0 +1,227 @@
+// =================================================================================================
+// Copyright 2011 Twitter, Inc.
+// -------------------------------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this work except in compliance with the License.
+// You may obtain a copy of the License in the LICENSE file, or at:
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// =================================================================================================
+
+package com.twitter.common.args;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+import com.twitter.common.args.apt.Configuration;
+import com.twitter.common.args.apt.Configuration.ArgInfo;
+
+import static com.twitter.common.args.apt.Configuration.ConfigurationException;
+
+/**
+ * Utility that can load static {@literal @CmdLine} and {@literal @Positional} arg field info from
+ * a configuration database or from explicitly listed containing classes or objects.
+ */
+public final class Args {
+  @VisibleForTesting
+  static final Function<ArgInfo, Optional<Field>> TO_FIELD =
+      new Function<ArgInfo, Optional<Field>>() {
+        @Override public Optional<Field> apply(ArgInfo info) {
+          try {
+            return Optional.of(Class.forName(info.className).getDeclaredField(info.fieldName));
+          } catch (NoSuchFieldException e) {
+            throw new ConfigurationException(e);
+          } catch (ClassNotFoundException e) {
+            throw new ConfigurationException(e);
+          } catch (NoClassDefFoundError e) {
+            // A compilation had this class available at the time the ArgInfo was deposited, but
+            // the classes have been re-bundled with some subset including the class this ArgInfo
+            // points to no longer available.  If the re-bundling is correct, then the arg truly is
+            // not needed.
+            LOG.fine(String.format("Not on current classpath, skipping %s", info));
+            return Optional.absent();
+          }
+        }
+      };
+
+  private static final Logger LOG = Logger.getLogger(Args.class.getName());
+
+  private static final Function<Field, OptionInfo<?>> TO_OPTION_INFO =
+      new Function<Field, OptionInfo<?>>() {
+        @Override public OptionInfo<?> apply(Field field) {
+          @Nullable CmdLine cmdLine = field.getAnnotation(CmdLine.class);
+          if (cmdLine == null) {
+            throw new ConfigurationException("No @CmdLine Arg annotation for field " + field);
+          }
+          return OptionInfo.createFromField(field);
+        }
+      };
+
+  private static final Function<Field, PositionalInfo<?>> TO_POSITIONAL_INFO =
+      new Function<Field, PositionalInfo<?>>() {
+        @Override public PositionalInfo<?> apply(Field field) {
+          @Nullable Positional positional = field.getAnnotation(Positional.class);
+          if (positional == null) {
+            throw new ConfigurationException("No @Positional Arg annotation for field " + field);
+          }
+          return PositionalInfo.createFromField(field);
+        }
+      };
+
+  /**
+   * An opaque container for all the positional and optional {@link Arg} metadata in-play for a
+   * command line parse.
+   */
+  public static final class ArgsInfo {
+    private final Configuration configuration;
+    private final Optional<? extends PositionalInfo<?>> positionalInfo;
+    private final ImmutableList<? extends OptionInfo<?>> optionInfos;
+
+    ArgsInfo(Configuration configuration,
+             Optional<? extends PositionalInfo<?>> positionalInfo,
+             Iterable<? extends OptionInfo<?>> optionInfos) {
+
+      this.configuration = Preconditions.checkNotNull(configuration);
+      this.positionalInfo = Preconditions.checkNotNull(positionalInfo);
+      this.optionInfos = ImmutableList.copyOf(optionInfos);
+    }
+
+    Configuration getConfiguration() {
+      return configuration;
+    }
+
+    Optional<? extends PositionalInfo<?>> getPositionalInfo() {
+      return positionalInfo;
+    }
+
+    ImmutableList<? extends OptionInfo<?>> getOptionInfos() {
+      return optionInfos;
+    }
+  }
+
+  /**
+   * Hydrates configured {@literal @CmdLine} arg fields and selects a desired set with the supplied
+   * {@code filter}.
+   *
+   * @param configuration The configuration to find candidate {@literal @CmdLine} arg fields in.
+   * @param filter A predicate to select fields with.
+   * @return The desired hydrated {@literal @CmdLine} arg fields and optional {@literal @Positional}
+   *     arg field.
+   */
+  static ArgsInfo fromConfiguration(Configuration configuration, Predicate<Field> filter) {
+    ImmutableSet<Field> positionalFields =
+        ImmutableSet.copyOf(filterFields(configuration.positionalInfo(), filter));
+
+    if (positionalFields.size() > 1) {
+      throw new IllegalArgumentException(
+          String.format("Found %d fields marked for @Positional Args after applying filter - "
+              + "only 1 is allowed:\n\t%s", positionalFields.size(),
+              Joiner.on("\n\t").join(positionalFields)));
+    }
+
+    Optional<? extends PositionalInfo<?>> positionalInfo =
+        Optional.fromNullable(
+            Iterables.getOnlyElement(
+                Iterables.transform(positionalFields, TO_POSITIONAL_INFO), null));
+
+    Iterable<? extends OptionInfo<?>> optionInfos = Iterables.transform(
+        filterFields(configuration.optionInfo(), filter), TO_OPTION_INFO);
+
+    return new ArgsInfo(configuration, positionalInfo, optionInfos);
+  }
+
+  private static Iterable<Field> filterFields(Iterable<ArgInfo> infos, Predicate<Field> filter) {
+    return Iterables.filter(
+        Optional.presentInstances(Iterables.transform(infos, TO_FIELD)),
+        filter);
+  }
+
+  /**
+   * Equivalent to calling {@code from(Predicates.alwaysTrue(), Arrays.asList(sources)}.
+   */
+  public static ArgsInfo from(Object... sources) throws IOException {
+    return from(ImmutableList.copyOf(sources));
+  }
+
+  /**
+   * Equivalent to calling {@code from(filter, Arrays.asList(sources)}.
+   */
+  public static ArgsInfo from(Predicate<Field> filter, Object... sources) throws IOException {
+    return from(filter, ImmutableList.copyOf(sources));
+  }
+
+  /**
+   * Equivalent to calling {@code from(Predicates.alwaysTrue(), sources}.
+   */
+  public static ArgsInfo from(Iterable<?> sources) throws IOException {
+    return from(Predicates.<Field>alwaysTrue(), sources);
+  }
+
+  /**
+   * Loads arg info from the given sources in addition to the default compile-time configuration.
+   *
+   * @param filter A predicate to select fields with.
+   * @param sources Classes or object instances to scan for {@link Arg} fields.
+   * @return The args info describing all discovered {@link Arg args}.
+   * @throws IOException If there was a problem loading the default Args configuration.
+   */
+  public static ArgsInfo from(Predicate<Field> filter, Iterable<?> sources) throws IOException {
+    Preconditions.checkNotNull(filter);
+    Preconditions.checkNotNull(sources);
+
+    Configuration configuration = Configuration.load();
+    ArgsInfo staticInfo = Args.fromConfiguration(configuration, filter);
+
+    final ImmutableSet.Builder<PositionalInfo<?>> positionalInfos =
+        ImmutableSet.<PositionalInfo<?>>builder().addAll(staticInfo.getPositionalInfo().asSet());
+    final ImmutableSet.Builder<OptionInfo<?>> optionInfos =
+        ImmutableSet.<OptionInfo<?>>builder().addAll(staticInfo.getOptionInfos());
+
+    for (Object source : sources) {
+      Class<?> clazz = source instanceof Class ? (Class) source : source.getClass();
+      for (Field field : clazz.getDeclaredFields()) {
+        if (filter.apply(field)) {
+          boolean cmdLine = field.isAnnotationPresent(CmdLine.class);
+          boolean positional = field.isAnnotationPresent(Positional.class);
+          if (cmdLine && positional) {
+            throw new IllegalArgumentException(
+                "An Arg cannot be annotated with both @CmdLine and @Positional, found bad Arg "
+                 + "field: " + field);
+          } else if (cmdLine) {
+            optionInfos.add(OptionInfo.createFromField(field, source));
+          } else if (positional) {
+            positionalInfos.add(PositionalInfo.createFromField(field, source));
+          }
+        }
+      }
+    }
+
+    @Nullable PositionalInfo<?> positionalInfo =
+        Iterables.getOnlyElement(positionalInfos.build(), null);
+    return new ArgsInfo(configuration, Optional.fromNullable(positionalInfo), optionInfos.build());
+  }
+
+  private Args() {
+    // utility
+  }
+}


Mime
View raw message