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 + } +}
