You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by zm...@apache.org on 2015/08/25 20:19:49 UTC
[35/37] aurora git commit: Import of Twitter Commons.
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<RootLogConfig.Configuraton>} - 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
+ }
+}