http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons-args/src/main/java/com/twitter/common/args/apt/CmdLineProcessor.java ---------------------------------------------------------------------- diff --git a/commons-args/src/main/java/com/twitter/common/args/apt/CmdLineProcessor.java b/commons-args/src/main/java/com/twitter/common/args/apt/CmdLineProcessor.java new file mode 100644 index 0000000..ab1f255 --- /dev/null +++ b/commons-args/src/main/java/com/twitter/common/args/apt/CmdLineProcessor.java @@ -0,0 +1,680 @@ +// ================================================================================================= +// 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.apt; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.SimpleAnnotationValueVisitor6; +import javax.lang.model.util.SimpleTypeVisitor6; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Optional; +import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.ArgParser; +import com.twitter.common.args.CmdLine; +import com.twitter.common.args.Parser; +import com.twitter.common.args.Positional; +import com.twitter.common.args.Verifier; +import com.twitter.common.args.VerifierFor; +import com.twitter.common.args.apt.Configuration.ParserInfo; + +import static com.twitter.common.args.apt.Configuration.ArgInfo; +import static com.twitter.common.args.apt.Configuration.VerifierInfo; + +/** + * Processes {@literal @CmdLine} annotated fields and {@literal @ArgParser} and + * {@literal @VerifierFor} parser and verifier registrations and stores configuration data listing + * these fields, parsers and verifiers on the classpath for discovery via + * {@link com.twitter.common.args.apt.Configuration#load()}. + * + * <p>Supports an apt option useful for some build setups that create monolithic jars aggregating + * many library jars, one or more of which have embedded arg definitions themselves. By adding the + * following flag to a javac invocation: + * <code>-Acom.twitter.common.args.apt.CmdLineProcessor.main</code> + * you signal this apt processor that the compilation target is a leaf target that will comprise one + * or more executable mains (as opposed to a library jar). As a result, the embedded arg + * definitions generated will occupy a special resource that is always checked for first during + * runtime arg parsing. + */ +@SupportedOptions({ + CmdLineProcessor.MAIN_OPTION, + CmdLineProcessor.CHECK_LINKAGE_OPTION +}) +public class CmdLineProcessor extends AbstractProcessor { + static final String MAIN_OPTION = + "com.twitter.common.args.apt.CmdLineProcessor.main"; + static final String CHECK_LINKAGE_OPTION = + "com.twitter.common.args.apt.CmdLineProcessor.check_linkage"; + + private static final Function<Class<?>, String> GET_NAME = new Function<Class<?>, String>() { + @Override public String apply(Class<?> type) { + return type.getName(); + } + }; + + private final Supplier<Configuration> configSupplier = + Suppliers.memoize(new Supplier<Configuration>() { + @Override public Configuration get() { + try { + Configuration configuration = Configuration.load(); + for (ArgInfo argInfo : configuration.positionalInfo()) { + configBuilder.addPositionalInfo(argInfo); + } + for (ArgInfo argInfo : configuration.optionInfo()) { + configBuilder.addCmdLineArg(argInfo); + } + for (ParserInfo parserInfo : configuration.parserInfo()) { + configBuilder.addParser(parserInfo); + } + for (VerifierInfo verifierInfo : configuration.verifierInfo()) { + configBuilder.addVerifier(verifierInfo); + } + return configuration; + } catch (IOException e) { + error("Problem loading existing flags on compile time classpath: %s", + Throwables.getStackTraceAsString(e)); + return null; + } + } + }); + + private final Configuration.Builder configBuilder = new Configuration.Builder(); + private final ImmutableSet.Builder<String> contributingClassNamesBuilder = ImmutableSet.builder(); + + private Types typeUtils; + private Elements elementUtils; + private boolean isMain; + private boolean isCheckLinkage; + + private static boolean getBooleanOption(Map<String, String> options, String name, + boolean defaultValue) { + + if (!options.containsKey(name)) { + return defaultValue; + } + + // We want to map the presence of a boolean option without a value to indicate true, giving the + // following accepted boolean option formats: + // -Afoo -> true + // -Afoo=false -> false + // -Afoo=true -> true + + String isOption = options.get(name); + return (isOption == null) || Boolean.parseBoolean(isOption); + } + + @Override + public void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + + typeUtils = processingEnv.getTypeUtils(); + elementUtils = processingEnv.getElementUtils(); + + Map<String, String> options = processingEnv.getOptions(); + isMain = getBooleanOption(options, MAIN_OPTION, false); + isCheckLinkage = getBooleanOption(options, CHECK_LINKAGE_OPTION, true); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public Set<String> getSupportedAnnotationTypes() { + return ImmutableSet.copyOf(Iterables.transform( + ImmutableList.of(Positional.class, CmdLine.class, ArgParser.class, VerifierFor.class), + GET_NAME)); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + try { + @Nullable Configuration classpathConfiguration = configSupplier.get(); + + Set<? extends Element> parsers = getAnnotatedElements(roundEnv, ArgParser.class); + contributingClassNamesBuilder.addAll(extractClassNames(parsers)); + @Nullable Set<String> parsedTypes = getParsedTypes(classpathConfiguration, parsers); + + Set<? extends Element> cmdlineArgs = getAnnotatedElements(roundEnv, CmdLine.class); + contributingClassNamesBuilder.addAll(extractEnclosingClassNames(cmdlineArgs)); + Set<? extends Element> positionalArgs = getAnnotatedElements(roundEnv, Positional.class); + contributingClassNamesBuilder.addAll(extractEnclosingClassNames(positionalArgs)); + + ImmutableSet<? extends Element> invalidArgs = + Sets.intersection(cmdlineArgs, positionalArgs).immutableCopy(); + if (!invalidArgs.isEmpty()) { + error("An Arg cannot be annotated with both @CmdLine and @Positional, found bad Arg " + + "fields: %s", invalidArgs); + } + + for (ArgInfo cmdLineInfo : processAnnotatedArgs(parsedTypes, cmdlineArgs, CmdLine.class)) { + configBuilder.addCmdLineArg(cmdLineInfo); + } + + for (ArgInfo positionalInfo + : processAnnotatedArgs(parsedTypes, positionalArgs, Positional.class)) { + + configBuilder.addPositionalInfo(positionalInfo); + } + checkPositionalArgsAreLists(roundEnv); + + processParsers(parsers); + + Set<? extends Element> verifiers = getAnnotatedElements(roundEnv, VerifierFor.class); + contributingClassNamesBuilder.addAll(extractClassNames(verifiers)); + processVerifiers(verifiers); + + if (roundEnv.processingOver()) { + if (classpathConfiguration != null + && (!classpathConfiguration.isEmpty() || !configBuilder.isEmpty())) { + + @Nullable Resource cmdLinePropertiesResource = + openCmdLinePropertiesResource(classpathConfiguration); + if (cmdLinePropertiesResource != null) { + Writer writer = cmdLinePropertiesResource.getWriter(); + try { + configBuilder.build(classpathConfiguration).store(writer, + "Generated via apt by " + getClass().getName()); + } finally { + closeQuietly(writer); + } + + writeResourceMapping(contributingClassNamesBuilder.build(), + cmdLinePropertiesResource.getResource()); + } + } + } + // TODO(John Sirois): Investigate narrowing this catch - its not clear there is any need to be + // so general. + // SUPPRESS CHECKSTYLE RegexpSinglelineJava + } catch (RuntimeException e) { + // Catch internal errors - when these bubble more useful queued error messages are lost in + // some javac implementations. + error("Unexpected error completing annotation processing:\n%s", + Throwables.getStackTraceAsString(e)); + } + return true; + } + + private void writeResourceMapping( + Set<String> contributingClassNames, + FileObject cmdLinePropertiesResourcePath) { + + // TODO(John Sirois): Lift the compiler resource-mappings writer to its own class/artifact to be + // re-used by other apt processors: https://github.com/twitter/commons/issues/319 + + // NB: javac rejects a package name with illegal package name characters like '-' so we just + // pass the empty package and the fully qualified resource file name. + @Nullable Resource resource = openResource("", + "META-INF/compiler/resource-mappings/" + getClass().getName()); + if (resource != null) { + PrintWriter writer = new PrintWriter(resource.getWriter()); + writer.printf("resources by class name:\n"); + writer.printf("%d items\n", contributingClassNames.size()); + try { + for (String className : contributingClassNames) { + writer.printf("%s -> %s\n", className, cmdLinePropertiesResourcePath.toUri().getPath()); + } + } finally { + closeQuietly(writer); + } + } + } + + private static final Function<Element, Element> EXTRACT_ENCLOSING_CLASS = + new Function<Element, Element>() { + @Override public Element apply(Element element) { + return element.getEnclosingElement(); + } + }; + + private final Function<Element, String> extractClassName = new Function<Element, String>() { + @Override public String apply(Element element) { + return getBinaryName((TypeElement) element); + } + }; + + private final Function<Element, String> extractEnclosingClassName = + Functions.compose(extractClassName, EXTRACT_ENCLOSING_CLASS); + + private Iterable<String> extractEnclosingClassNames(Iterable<? extends Element> elements) { + return Iterables.transform(elements, extractEnclosingClassName); + } + + private Iterable<String> extractClassNames(Iterable<? extends Element> elements) { + return Iterables.transform(elements, extractClassName); + } + + private void closeQuietly(Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + log(Kind.MANDATORY_WARNING, "Failed to close %s: %s", closeable, e); + } + } + + private void checkPositionalArgsAreLists(RoundEnvironment roundEnv) { + for (Element positionalArg : getAnnotatedElements(roundEnv, Positional.class)) { + @Nullable TypeMirror typeArgument = + getTypeArgument(positionalArg.asType(), typeElement(Arg.class)); + if ((typeArgument == null) + || !typeUtils.isSubtype(typeElement(List.class).asType(), typeArgument)) { + error("Found @Positional %s %s.%s that is not a List", + positionalArg.asType(), positionalArg.getEnclosingElement(), positionalArg); + } + } + } + + @Nullable + private Set<String> getParsedTypes(@Nullable Configuration configuration, + Set<? extends Element> parsers) { + + if (!isCheckLinkage) { + return null; + } + + Iterable<String> parsersFor = Optional.presentInstances(Iterables.transform(parsers, + new Function<Element, Optional<String>>() { + @Override public Optional<String> apply(Element parser) { + TypeMirror parsedType = getTypeArgument(parser.asType(), typeElement(Parser.class)); + if (parsedType == null) { + error("failed to find a type argument for Parser: %s", parser); + return Optional.absent(); + } + // Equals on TypeMirrors doesn't work - so we compare string representations :/ + return Optional.of(typeUtils.erasure(parsedType).toString()); + } + })); + if (configuration != null) { + parsersFor = Iterables.concat(parsersFor, Iterables.filter( + Iterables.transform(configuration.parserInfo(), + new Function<ParserInfo, String>() { + @Override @Nullable public String apply(ParserInfo parserInfo) { + TypeElement typeElement = elementUtils.getTypeElement(parserInfo.parsedType); + // We may not have a type on the classpath for a previous round - this is fine as + // long as the no Args in this round that are of the type. + return (typeElement == null) + ? null : typeUtils.erasure(typeElement.asType()).toString(); + } + }), Predicates.notNull())); + } + return ImmutableSet.copyOf(parsersFor); + } + + private Iterable<ArgInfo> processAnnotatedArgs( + @Nullable final Set<String> parsedTypes, + Set<? extends Element> args, + final Class<? extends Annotation> argAnnotation) { + + return Optional.presentInstances(Iterables.transform(args, + new Function<Element, Optional<ArgInfo>>() { + @Override public Optional<ArgInfo> apply(Element arg) { + @Nullable TypeElement containingType = processArg(parsedTypes, arg, argAnnotation); + if (containingType == null) { + return Optional.absent(); + } else { + return Optional.of(new ArgInfo(getBinaryName(containingType), + arg.getSimpleName().toString())); + } + } + })); + } + + private Set<? extends Element> getAnnotatedElements(RoundEnvironment roundEnv, + Class<? extends Annotation> argAnnotation) { + return roundEnv.getElementsAnnotatedWith(typeElement(argAnnotation)); + } + + @Nullable + private TypeElement processArg(@Nullable Set<String> parsedTypes, Element annotationElement, + Class<? extends Annotation> annotationType) { + + TypeElement parserType = typeElement(Parser.class); + if (annotationElement.getKind() != ElementKind.FIELD) { + error("Found a @%s annotation on a non-field %s", + annotationType.getSimpleName(), annotationElement); + return null; + } else { + // Only types contain fields so this cast is safe. + TypeElement containingType = (TypeElement) annotationElement.getEnclosingElement(); + + if (!isAssignable(annotationElement.asType(), Arg.class)) { + error("Found a @%s annotation on a non-Arg %s.%s", + annotationType.getSimpleName(), containingType, annotationElement); + return null; + } + if (!annotationElement.getModifiers().contains(Modifier.STATIC)) { + return null; + } + + if (parsedTypes != null) { + // Check Parser<T> linkage for the Arg<T> type T. + TypeMirror typeArgument = + getTypeArgument(annotationElement.asType(), typeElement(Arg.class)); + @Nullable AnnotationMirror cmdLine = + getAnnotationMirror(annotationElement, typeElement(annotationType)); + if (cmdLine != null) { + TypeMirror customParserType = getClassType(cmdLine, "parser", parserType).asType(); + if (typeUtils.isSameType(parserType.asType(), customParserType)) { + if (!checkTypePresent(parsedTypes, typeArgument)) { + error("No parser registered for %s, %s.%s is un-parseable", + typeArgument, containingType, annotationElement); + } + } else { + TypeMirror customParsedType = getTypeArgument(customParserType, parserType); + if (!isAssignable(typeArgument, customParsedType)) { + error("Custom parser %s parses %s but registered for %s.%s with Arg type %s", + customParserType, customParsedType, containingType, annotationElement, + typeArgument); + } + } + } + } + + // TODO(John Sirois): Add additional compile-time @CmdLine verification for: + // 1.) for each @CmdLine Arg<T> annotated with @VerifierFor.annotation: T is a subtype of + // V where there is a Verifier<V> + // 2.) name checks, including dups + + return containingType; + } + } + + private boolean checkTypePresent(Set<String> types, TypeMirror type) { + Iterable<TypeMirror> allTypes = getAllTypes(type); + for (TypeMirror t : allTypes) { + if (types.contains(typeUtils.erasure(t).toString())) { + return true; + } + } + return false; + } + + private void processParsers(Set<? extends Element> elements) { + TypeElement parserType = typeElement(Parser.class); + for (Element element : elements) { + if (element.getKind() != ElementKind.CLASS) { + error("Found an @ArgParser annotation on a non-class %s", element); + } else { + TypeElement parser = (TypeElement) element; + if (!isAssignable(parser, Parser.class)) { + error("Found an @ArgParser annotation on a non-Parser %s", element); + return; + } + + @Nullable String parsedType = getTypeArgument(parser, parserType); + if (parsedType != null) { + configBuilder.addParser(parsedType, getBinaryName(parser)); + } + } + } + } + + private void processVerifiers(Set<? extends Element> elements) { + TypeElement verifierType = typeElement(Verifier.class); + TypeElement verifierForType = typeElement(VerifierFor.class); + for (Element element : elements) { + if (element.getKind() != ElementKind.CLASS) { + error("Found a @VerifierFor annotation on a non-class %s", element); + } else { + TypeElement verifier = (TypeElement) element; + if (!isAssignable(verifier, Verifier.class)) { + error("Found a @Verifier annotation on a non-Verifier %s", element); + return; + } + + @Nullable AnnotationMirror verifierFor = getAnnotationMirror(verifier, verifierForType); + if (verifierFor != null) { + @Nullable TypeElement verifyAnnotationType = getClassType(verifierFor, "value", null); + if (verifyAnnotationType != null) { + @Nullable String verifiedType = getTypeArgument(verifier, verifierType); + if (verifiedType != null) { + String verifyAnnotationClassName = + elementUtils.getBinaryName(verifyAnnotationType).toString(); + configBuilder.addVerifier(verifiedType, verifyAnnotationClassName, + getBinaryName(verifier)); + } + } + } + } + } + } + + @Nullable + private String getTypeArgument(TypeElement annotatedType, final TypeElement baseType) { + TypeMirror typeArgument = getTypeArgument(annotatedType.asType(), baseType); + return typeArgument == null + ? null + : getBinaryName((TypeElement) typeUtils.asElement(typeArgument)); + } + + private Iterable<TypeMirror> getAllTypes(TypeMirror type) { + return getAllTypes(new HashSet<String>(), Lists.<TypeMirror>newArrayList(), type); + } + + private Iterable<TypeMirror> getAllTypes(Set<String> visitedTypes, List<TypeMirror> types, + TypeMirror type) { + + String typeName = typeUtils.erasure(type).toString(); + if (!visitedTypes.contains(typeName)) { + types.add(type); + visitedTypes.add(typeName); + for (TypeMirror superType : typeUtils.directSupertypes(type)) { + getAllTypes(visitedTypes, types, superType); + } + } + return types; + } + + @Nullable + private TypeMirror getTypeArgument(TypeMirror annotatedType, final TypeElement baseType) { + for (TypeMirror type : getAllTypes(annotatedType)) { + TypeMirror typeArgument = type.accept(new SimpleTypeVisitor6<TypeMirror, Void>() { + @Override public TypeMirror visitDeclared(DeclaredType t, Void aVoid) { + if (isAssignable(t, baseType)) { + List<? extends TypeMirror> typeArguments = t.getTypeArguments(); + if (!typeArguments.isEmpty()) { + return typeUtils.erasure(typeArguments.get(0)); + } + } + return null; + } + }, null); + + if (typeArgument != null) { + return typeArgument; + } + } + error("Failed to find a type argument for %s in %s", baseType, annotatedType); + return null; + } + + @Nullable + private AnnotationMirror getAnnotationMirror(Element element, TypeElement annotationType) { + for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { + if (typeUtils.isSameType(annotationMirror.getAnnotationType(), annotationType.asType())) { + return annotationMirror; + } + } + error("Failed to find an annotation of type %s on %s", annotationType, element); + return null; + } + + @SuppressWarnings("unchecked") + private TypeElement getClassType(AnnotationMirror annotationMirror, String methodName, + TypeElement defaultClassType) { + + for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry + : annotationMirror.getElementValues().entrySet()) { + if (entry.getKey().getSimpleName().equals(elementUtils.getName(methodName))) { + TypeElement classType = entry.getValue().accept( + new SimpleAnnotationValueVisitor6<TypeElement, Void>() { + @Override public TypeElement visitType(TypeMirror t, Void unused) { + return (TypeElement) processingEnv.getTypeUtils().asElement(t); + } + }, null); + + if (classType != null) { + return classType; + } + } + } + if (defaultClassType == null) { + error("Could not find a class type for %s.%s", annotationMirror, methodName); + } + return defaultClassType; + } + + @Nullable + private FileObject createCommandLineDb(Configuration configuration) { + String name = isMain ? Configuration.mainResourceName() : configuration.nextResourceName(); + return createResource(Configuration.DEFAULT_RESOURCE_PACKAGE, name); + } + + @Nullable + private FileObject createResource(String packageName, String name) { + try { + return processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, + packageName, name); + } catch (IOException e) { + error("Failed to create resource file to store %s/%s: %s", + packageName, name, Throwables.getStackTraceAsString(e)); + return null; + } + } + + private static final class Resource { + private final FileObject resource; + private final Writer writer; + + Resource(FileObject resource, Writer writer) { + this.resource = resource; + this.writer = writer; + } + + FileObject getResource() { + return resource; + } + + Writer getWriter() { + return writer; + } + } + + @Nullable + private Resource openCmdLinePropertiesResource(Configuration configuration) { + @Nullable FileObject resource = createCommandLineDb(configuration); + return openResource(resource); + } + + @Nullable + private Resource openResource(String packageName, String name) { + @Nullable FileObject resource = createResource(packageName, name); + return openResource(resource); + } + + @Nullable + private Resource openResource(@Nullable FileObject resource) { + if (resource == null) { + return null; + } + try { + log(Kind.NOTE, "Writing %s", resource.toUri()); + return new Resource(resource, resource.openWriter()); + } catch (IOException e) { + if (!resource.delete()) { + log(Kind.WARNING, "Failed to clean up %s after a failing to open it for writing", + resource.toUri()); + } + error("Failed to open resource file to store %s: %s", resource.toUri(), + Throwables.getStackTraceAsString(e)); + return null; + } + } + + private TypeElement typeElement(Class<?> type) { + return elementUtils.getTypeElement(type.getName()); + } + + private String getBinaryName(TypeElement typeElement) { + return elementUtils.getBinaryName(typeElement).toString(); + } + + private boolean isAssignable(TypeElement subType, Class<?> baseType) { + return isAssignable(subType.asType(), baseType); + } + + private boolean isAssignable(TypeMirror subType, Class<?> baseType) { + return isAssignable(subType, typeElement(baseType)); + } + + private boolean isAssignable(TypeMirror subType, TypeElement baseType) { + return isAssignable(subType, baseType.asType()); + } + + private boolean isAssignable(TypeMirror subType, TypeMirror baseType) { + return typeUtils.isAssignable(typeUtils.erasure(subType), typeUtils.erasure(baseType)); + } + + private void error(String message, Object ... args) { + log(Kind.ERROR, message, args); + } + + private void log(Kind kind, String message, Object ... args) { + processingEnv.getMessager().printMessage(kind, String.format(message, args)); + } +}
http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons-args/src/main/java/com/twitter/common/args/apt/Configuration.java ---------------------------------------------------------------------- diff --git a/commons-args/src/main/java/com/twitter/common/args/apt/Configuration.java b/commons-args/src/main/java/com/twitter/common/args/apt/Configuration.java new file mode 100644 index 0000000..1254fc2 --- /dev/null +++ b/commons-args/src/main/java/com/twitter/common/args/apt/Configuration.java @@ -0,0 +1,530 @@ +// ================================================================================================= +// 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.apt; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.CharStreams; +import com.google.common.io.InputSupplier; +import com.google.common.io.LineProcessor; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * Loads and stores {@literal @CmdLine} configuration data. By default, that data + * is contained in text files called cmdline.arg.info.txt.0, cmdline.arg.info.txt.1 + * etc. Every time a new Configuration object is created, it consumes all existing + * files with the above names. Saving this Configuration results in creation of a + * file with index increased by one, e.g. cmdline.arg.info.txt.2 in the above + * example. + * + * @author John Sirois + */ +public final class Configuration { + + /** + * Indicates a problem reading stored {@literal @CmdLine} arg configuration data. + */ + public static class ConfigurationException extends RuntimeException { + public ConfigurationException(String message, Object... args) { + super(String.format(message, args)); + } + public ConfigurationException(Throwable cause) { + super(cause); + } + } + + static final String DEFAULT_RESOURCE_PACKAGE = Configuration.class.getPackage().getName(); + + private static final Logger LOG = Logger.getLogger(Configuration.class.getName()); + + private static final CharMatcher IDENTIFIER_START = + CharMatcher.forPredicate(new Predicate<Character>() { + @Override public boolean apply(Character c) { + return Character.isJavaIdentifierStart(c); + } + }); + + private static final CharMatcher IDENTIFIER_REST = + CharMatcher.forPredicate(new Predicate<Character>() { + @Override public boolean apply(Character c) { + return Character.isJavaIdentifierPart(c); + } + }); + + private static final Function<URL, InputSupplier<? extends InputStream>> URL_TO_INPUT = + new Function<URL, InputSupplier<? extends InputStream>>() { + @Override public InputSupplier<? extends InputStream> apply(final URL resource) { + return new InputSupplier<InputStream>() { + @Override public InputStream getInput() throws IOException { + return resource.openStream(); + } + }; + } + }; + + private static final Function<InputSupplier<? extends InputStream>, + InputSupplier<? extends Reader>> INPUT_TO_READER = + new Function<InputSupplier<? extends InputStream>, InputSupplier<? extends Reader>>() { + @Override public InputSupplier<? extends Reader> apply( + final InputSupplier<? extends InputStream> input) { + return CharStreams.newReaderSupplier(input, Charsets.UTF_8); + } + }; + + private static final Function<URL, InputSupplier<? extends Reader>> URL_TO_READER = + Functions.compose(INPUT_TO_READER, URL_TO_INPUT); + + private static final String DEFAULT_RESOURCE_NAME = "cmdline.arg.info.txt"; + + private int nextResourceIndex; + private final ImmutableSet<ArgInfo> positionalInfos; + private final ImmutableSet<ArgInfo> cmdLineInfos; + private final ImmutableSet<ParserInfo> parserInfos; + private final ImmutableSet<VerifierInfo> verifierInfos; + + private Configuration(int nextResourceIndex, + Iterable<ArgInfo> positionalInfos, Iterable<ArgInfo> cmdLineInfos, + Iterable<ParserInfo> parserInfos, Iterable<VerifierInfo> verifierInfos) { + this.nextResourceIndex = nextResourceIndex; + this.positionalInfos = ImmutableSet.copyOf(positionalInfos); + this.cmdLineInfos = ImmutableSet.copyOf(cmdLineInfos); + this.parserInfos = ImmutableSet.copyOf(parserInfos); + this.verifierInfos = ImmutableSet.copyOf(verifierInfos); + } + + private static String checkValidIdentifier(String identifier, boolean compound) { + Preconditions.checkNotNull(identifier); + + String trimmed = identifier.trim(); + Preconditions.checkArgument(!trimmed.isEmpty(), "Invalid identifier: '%s'", identifier); + + String[] parts = compound ? trimmed.split("\\.") : new String[] {trimmed}; + for (String part : parts) { + Preconditions.checkArgument( + IDENTIFIER_REST.matchesAllOf(IDENTIFIER_START.trimLeadingFrom(part)), + "Invalid identifier: '%s'", identifier); + } + + return trimmed; + } + + public static final class ArgInfo { + public final String className; + public final String fieldName; + + public ArgInfo(String className, String fieldName) { + this.className = checkValidIdentifier(className, true); + this.fieldName = checkValidIdentifier(fieldName, false); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof ArgInfo)) { + return false; + } + + ArgInfo other = (ArgInfo) obj; + + return new EqualsBuilder() + .append(className, other.className) + .append(fieldName, other.fieldName) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(className) + .append(fieldName) + .toHashCode(); + } + + @Override public String toString() { + return new ToStringBuilder(this) + .append("className", className) + .append("fieldName", fieldName) + .toString(); + } + } + + public static final class ParserInfo { + public final String parsedType; + public final String parserClass; + + public ParserInfo(String parsedType, String parserClass) { + this.parsedType = checkValidIdentifier(parsedType, true); + this.parserClass = checkValidIdentifier(parserClass, true); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof ParserInfo)) { + return false; + } + + ParserInfo other = (ParserInfo) obj; + + return new EqualsBuilder() + .append(parsedType, other.parsedType) + .append(parserClass, other.parserClass) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(parsedType) + .append(parserClass) + .toHashCode(); + } + + @Override public String toString() { + return new ToStringBuilder(this) + .append("parsedType", parsedType) + .append("parserClass", parserClass) + .toString(); + } + } + + public static final class VerifierInfo { + public final String verifiedType; + public final String verifyingAnnotation; + public final String verifierClass; + + public VerifierInfo(String verifiedType, String verifyingAnnotation, String verifierClass) { + this.verifiedType = checkValidIdentifier(verifiedType, true); + this.verifyingAnnotation = checkValidIdentifier(verifyingAnnotation, true); + this.verifierClass = checkValidIdentifier(verifierClass, true); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof VerifierInfo)) { + return false; + } + + VerifierInfo other = (VerifierInfo) obj; + + return new EqualsBuilder() + .append(verifiedType, other.verifiedType) + .append(verifyingAnnotation, other.verifyingAnnotation) + .append(verifierClass, other.verifierClass) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(verifiedType) + .append(verifyingAnnotation) + .append(verifierClass) + .toHashCode(); + } + + @Override public String toString() { + return new ToStringBuilder(this) + .append("verifiedType", verifiedType) + .append("verifyingAnnotation", verifyingAnnotation) + .append("verifierClass", verifierClass) + .toString(); + } + } + + static class Builder { + private final Set<ArgInfo> positionalInfos = Sets.newHashSet(); + private final Set<ArgInfo> argInfos = Sets.newHashSet(); + private final Set<ParserInfo> parserInfos = Sets.newHashSet(); + private final Set<VerifierInfo> verifierInfos = Sets.newHashSet(); + + public boolean isEmpty() { + return positionalInfos.isEmpty() + && argInfos.isEmpty() + && parserInfos.isEmpty() + && verifierInfos.isEmpty(); + } + + void addPositionalInfo(ArgInfo positionalInfo) { + positionalInfos.add(positionalInfo); + } + + void addCmdLineArg(ArgInfo argInfo) { + argInfos.add(argInfo); + } + + void addParser(ParserInfo parserInfo) { + parserInfos.add(parserInfo); + } + + public void addParser(String parserForType, String parserType) { + addParser(new ParserInfo(parserForType, parserType)); + } + + void addVerifier(VerifierInfo verifierInfo) { + verifierInfos.add(verifierInfo); + } + + public void addVerifier(String verifierForType, String annotationType, String verifierType) { + addVerifier(new VerifierInfo(verifierForType, annotationType, verifierType)); + } + + public Configuration build(Configuration configuration) { + return new Configuration(configuration.nextResourceIndex + 1, + positionalInfos, argInfos, parserInfos, verifierInfos); + } + } + + private static String getResourceName(int index) { + return String.format("%s.%s", DEFAULT_RESOURCE_NAME, index); + } + + private static String getResourcePath(int index) { + return String.format("%s/%s", DEFAULT_RESOURCE_PACKAGE.replace('.', '/'), + getResourceName(index)); + } + + static final class ConfigurationResources { + private final int nextResourceIndex; + private final Iterator<URL> resources; + + private ConfigurationResources(int nextResourceIndex, Iterator<URL> resources) { + this.nextResourceIndex = nextResourceIndex; + this.resources = resources; + } + } + + /** + * Loads the {@literal @CmdLine} argument configuration data stored in the classpath. + * + * @return The {@literal @CmdLine} argument configuration materialized from the classpath. + * @throws ConfigurationException if any configuration data is malformed. + * @throws IOException if the configuration data can not be read from the classpath. + */ + public static Configuration load() throws ConfigurationException, IOException { + ConfigurationResources allResources = getAllResources(); + List<URL> configs = ImmutableList.copyOf(allResources.resources); + if (configs.isEmpty()) { + LOG.info("No @CmdLine arg configs found on the classpath"); + } else { + LOG.info("Loading @CmdLine config from: " + configs); + } + return load(allResources.nextResourceIndex, configs); + } + + private static ConfigurationResources getAllResources() throws IOException { + int maxResourceIndex = 0; + Iterator<URL> allResources = getResources(0); // Try for a main + // Probe for resource files with index up to 10 (or more, while resources at the + // given index can be found) + for (int nextResourceIndex = 1; nextResourceIndex <= maxResourceIndex + 10; + nextResourceIndex++) { + Iterator<URL> resources = getResources(nextResourceIndex); + if (resources.hasNext()) { + allResources = Iterators.concat(allResources, resources); + maxResourceIndex = nextResourceIndex; + } + } + return new ConfigurationResources(maxResourceIndex + 1, allResources); + } + + private static Iterator<URL> getResources(int index) throws IOException { + return Iterators.forEnumeration( + Configuration.class.getClassLoader().getResources(getResourcePath(index))); + } + + private static final class ConfigurationParser implements LineProcessor<Configuration> { + private final int nextIndex; + private int lineNumber = 0; + + private final ImmutableList.Builder<ArgInfo> positionalInfo = ImmutableList.builder(); + private final ImmutableList.Builder<ArgInfo> fieldInfoBuilder = ImmutableList.builder(); + private final ImmutableList.Builder<ParserInfo> parserInfoBuilder = ImmutableList.builder(); + private final ImmutableList.Builder<VerifierInfo> verifierInfoBuilder = ImmutableList.builder(); + + private ConfigurationParser(int nextIndex) { + this.nextIndex = nextIndex; + } + + @Override + public boolean processLine(String line) throws IOException { + ++lineNumber; + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + List<String> parts = Lists.newArrayList(trimmed.split(" ")); + if (parts.size() < 1) { + throw new ConfigurationException("Invalid line: %s @%d", trimmed, lineNumber); + } + + String type = parts.remove(0); + if ("positional".equals(type)) { + if (parts.size() != 2) { + throw new ConfigurationException( + "Invalid positional line: %s @%d", trimmed, lineNumber); + } + positionalInfo.add(new ArgInfo(parts.get(0), parts.get(1))); + } else if ("field".equals(type)) { + if (parts.size() != 2) { + throw new ConfigurationException("Invalid field line: %s @%d", trimmed, lineNumber); + } + fieldInfoBuilder.add(new ArgInfo(parts.get(0), parts.get(1))); + } else if ("parser".equals(type)) { + if (parts.size() != 2) { + throw new ConfigurationException("Invalid parser line: %s @%d", trimmed, lineNumber); + } + parserInfoBuilder.add(new ParserInfo(parts.get(0), parts.get(1))); + } else if ("verifier".equals(type)) { + if (parts.size() != 3) { + throw new ConfigurationException("Invalid verifier line: %s @%d", trimmed, lineNumber); + } + verifierInfoBuilder.add(new VerifierInfo(parts.get(0), parts.get(1), parts.get(2))); + } else { + LOG.warning(String.format("Did not recognize entry type %s for line: %s @%d", + type, trimmed, lineNumber)); + } + } + return true; + } + + @Override + public Configuration getResult() { + return new Configuration(nextIndex, positionalInfo.build(), + fieldInfoBuilder.build(), parserInfoBuilder.build(), verifierInfoBuilder.build()); + } + } + + private static Configuration load(int nextIndex, List<URL> configs) + throws ConfigurationException, IOException { + InputSupplier<Reader> input = CharStreams.join(Iterables.transform(configs, URL_TO_READER)); + return CharStreams.readLines(input, new ConfigurationParser(nextIndex)); + } + + public boolean isEmpty() { + return positionalInfos.isEmpty() + && cmdLineInfos.isEmpty() + && parserInfos.isEmpty() + && verifierInfos.isEmpty(); + } + + /** + * Returns the field info for the sole {@literal @Positional} annotated field on the classpath, + * if any. + * + * @return The field info for the {@literal @Positional} annotated field if any. + */ + public Iterable<ArgInfo> positionalInfo() { + return positionalInfos; + } + + /** + * Returns the field info for all the {@literal @CmdLine} annotated fields on the classpath. + * + * @return The field info for all the {@literal @CmdLine} annotated fields. + */ + public Iterable<ArgInfo> optionInfo() { + return cmdLineInfos; + } + + /** + * Returns the parser info for all the {@literal @ArgParser} annotated parsers on the classpath. + * + * @return The parser info for all the {@literal @ArgParser} annotated parsers. + */ + public Iterable<ParserInfo> parserInfo() { + return parserInfos; + } + + /** + * Returns the verifier info for all the {@literal @VerifierFor} annotated verifiers on the + * classpath. + * + * @return The verifier info for all the {@literal @VerifierFor} annotated verifiers. + */ + public Iterable<VerifierInfo> verifierInfo() { + return verifierInfos; + } + + static String mainResourceName() { + return getResourceName(0); + } + + String nextResourceName() { + return getResourceName(nextResourceIndex); + } + + void store(Writer output, String message) { + PrintWriter writer = new PrintWriter(output); + writer.printf("# %s\n", new Date()); + writer.printf("# %s\n ", message); + + writer.println(); + for (ArgInfo info : positionalInfos) { + writer.printf("positional %s %s\n", info.className, info.fieldName); + } + + writer.println(); + for (ArgInfo info : cmdLineInfos) { + writer.printf("field %s %s\n", info.className, info.fieldName); + } + + writer.println(); + for (ParserInfo info : parserInfos) { + writer.printf("parser %s %s\n", info.parsedType, info.parserClass); + } + + writer.println(); + for (VerifierInfo info : verifierInfos) { + writer.printf("verifier %s %s %s\n", + info.verifiedType, info.verifyingAnnotation, info.verifierClass); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons-args/src/main/resources/META-INF/services/javax.annotation.processing.Processor ---------------------------------------------------------------------- diff --git a/commons-args/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/commons-args/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..bc5a756 --- /dev/null +++ b/commons-args/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.twitter.common.args.apt.CmdLineProcessor http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/AbstractApplication.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/AbstractApplication.java b/commons/src/main/java/com/twitter/common/application/AbstractApplication.java new file mode 100644 index 0000000..239a9ef --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/AbstractApplication.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// 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; + +import java.util.Collections; + +import com.google.inject.Module; + +/** + * A base application class that provides empty implementations of all but the {@link #run()} + * method. + */ +public abstract class AbstractApplication implements Application { + @Override + public Iterable<? extends Module> getModules() { + return Collections.emptyList(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/AppLauncher.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/AppLauncher.java b/commons/src/main/java/com/twitter/common/application/AppLauncher.java new file mode 100644 index 0000000..6b4ccc3 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/AppLauncher.java @@ -0,0 +1,205 @@ +// ================================================================================================= +// 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; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Stage; +import com.google.inject.util.Modules; + +import com.twitter.common.application.modules.AppLauncherModule; +import com.twitter.common.application.modules.LifecycleModule; +import com.twitter.common.args.Arg; +import com.twitter.common.args.ArgFilters; +import com.twitter.common.args.ArgScanner; +import com.twitter.common.args.ArgScanner.ArgScanException; +import com.twitter.common.args.CmdLine; +import com.twitter.common.args.constraints.NotNull; +import com.twitter.common.base.ExceptionalCommand; + +/** + * An application launcher that sets up a framework for pluggable binding modules. This class + * should be called directly as the main class, with a command line argument {@code -app_class} + * which is the canonical class name of the application to execute. + * + * If your application uses command line arguments all {@link Arg} fields annotated with + * {@link CmdLine} will be discovered and command line arguments will be validated against this set, + * parsed and applied. + * + * A bootstrap module will be automatically applied ({@link AppLauncherModule}), which provides + * overridable default bindings for things like quit/abort hooks and a health check function. + * A {@link LifecycleModule} is also automatically applied to perform startup and shutdown + * actions. + */ +public final class AppLauncher { + + private static final Logger LOG = Logger.getLogger(AppLauncher.class.getName()); + + private static final String APP_CLASS_NAME = "app_class"; + @NotNull + @CmdLine(name = APP_CLASS_NAME, + help = "Fully-qualified name of the application class, which must implement Runnable.") + private static final Arg<Class<? extends Application>> APP_CLASS = Arg.create(); + + @CmdLine(name = "guice_stage", + help = "Guice development stage to create injector with.") + private static final Arg<Stage> GUICE_STAGE = Arg.create(Stage.DEVELOPMENT); + + private static final Predicate<Field> SELECT_APP_CLASS = + ArgFilters.selectCmdLineArg(AppLauncher.class, APP_CLASS_NAME); + + @Inject @StartupStage private ExceptionalCommand startupCommand; + @Inject private Lifecycle lifecycle; + + private AppLauncher() { + // This should not be invoked directly. + } + + private void run(Application application) { + try { + configureInjection(application); + + LOG.info("Executing startup actions."); + // We're an app framework and this is the outer shell - it makes sense to handle all errors + // before exiting. + // SUPPRESS CHECKSTYLE:OFF IllegalCatch + try { + startupCommand.execute(); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Startup action failed, quitting.", e); + throw Throwables.propagate(e); + } + // SUPPRESS CHECKSTYLE:ON IllegalCatch + + try { + application.run(); + } finally { + LOG.info("Application run() exited."); + } + } finally { + if (lifecycle != null) { + lifecycle.shutdown(); + } + } + } + + private void configureInjection(Application application) { + Iterable<Module> modules = ImmutableList.<Module>builder() + .add(new LifecycleModule()) + .add(new AppLauncherModule()) + .addAll(application.getModules()) + .build(); + + Injector injector = Guice.createInjector(GUICE_STAGE.get(), Modules.combine(modules)); + injector.injectMembers(this); + injector.injectMembers(application); + } + + public static void main(String... args) throws IllegalAccessException, InstantiationException { + // TODO(John Sirois): Support a META-INF/MANIFEST.MF App-Class attribute to allow java -jar + parseArgs(ArgFilters.SELECT_ALL, Arrays.asList(args)); + new AppLauncher().run(APP_CLASS.get().newInstance()); + } + + /** + * A convenience for main wrappers. Equivalent to: + * <pre> + * AppLauncher.launch(appClass, ArgFilters.SELECT_ALL, Arrays.asList(args)); + * </pre> + * + * @param appClass The application class to instantiate and launch. + * @param args The command line arguments to parse. + * @see ArgFilters + */ + public static void launch(Class<? extends Application> appClass, String... args) { + launch(appClass, ArgFilters.SELECT_ALL, Arrays.asList(args)); + } + + /** + * A convenience for main wrappers. Equivalent to: + * <pre> + * AppLauncher.launch(appClass, argFilter, Arrays.asList(args)); + * </pre> + * + * @param appClass The application class to instantiate and launch. + * @param argFilter A filter that selects the {@literal @CmdLine} {@link Arg}s to enable for + * parsing. + * @param args The command line arguments to parse. + * @see ArgFilters + */ + public static void launch(Class<? extends Application> appClass, Predicate<Field> argFilter, + String... args) { + launch(appClass, argFilter, Arrays.asList(args)); + } + + /** + * Used to launch an application with a restricted set of {@literal @CmdLine} {@link Arg}s + * considered for parsing. This is useful if the classpath includes annotated fields you do not + * wish arguments to be parsed for. + * + * @param appClass The application class to instantiate and launch. + * @param argFilter A filter that selects the {@literal @CmdLine} {@link Arg}s to enable for + * parsing. + * @param args The command line arguments to parse. + * @see ArgFilters + */ + public static void launch(Class<? extends Application> appClass, Predicate<Field> argFilter, + List<String> args) { + Preconditions.checkNotNull(appClass); + Preconditions.checkNotNull(argFilter); + Preconditions.checkNotNull(args); + + parseArgs(Predicates.<Field>and(Predicates.not(SELECT_APP_CLASS), argFilter), args); + try { + new AppLauncher().run(appClass.newInstance()); + } catch (InstantiationException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + private static void parseArgs(Predicate<Field> filter, List<String> args) { + try { + if (!new ArgScanner().parse(filter, args)) { + System.exit(0); + } + } catch (ArgScanException e) { + exit("Failed to scan arguments", e); + } catch (IllegalArgumentException e) { + exit("Failed to apply arguments", e); + } + } + + private static void exit(String message, Exception error) { + LOG.log(Level.SEVERE, message + "\n" + error, error); + System.exit(1); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/Application.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/Application.java b/commons/src/main/java/com/twitter/common/application/Application.java new file mode 100644 index 0000000..c3203c0 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/Application.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// 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; + +import com.google.inject.Module; + +/** + * An application that supports a limited lifecycle and optional binding of guice modules. + */ +public interface Application extends Runnable { + + /** + * Returns binding modules for the application. + * + * @return Application binding modules. + */ + Iterable<? extends Module> getModules(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/Lifecycle.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/Lifecycle.java b/commons/src/main/java/com/twitter/common/application/Lifecycle.java new file mode 100644 index 0000000..28a667d --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/Lifecycle.java @@ -0,0 +1,97 @@ +// ================================================================================================= +// 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; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.logging.Logger; + +import com.google.inject.Inject; + +import com.twitter.common.base.Command; + +/** + * Application lifecycle manager, which coordinates orderly shutdown of an application. This class + * is responsible for executing shutdown commands, and can also be used to allow threads to await + * application shutdown. + * + * @author William Farner + */ +public class Lifecycle { + + private static final Logger LOG = Logger.getLogger(Lifecycle.class.getName()); + + // Monitor and state for suspending and terminating execution. + private final Object waitMonitor = new Object(); + private boolean destroyed = false; + + private final Command shutdownRegistry; + + @Inject + public Lifecycle(@ShutdownStage Command shutdownRegistry, + UncaughtExceptionHandler exceptionHandler) { + + this.shutdownRegistry = shutdownRegistry; + Thread.setDefaultUncaughtExceptionHandler(exceptionHandler); + } + + /** + * Checks whether this lifecycle is still considered alive. The lifecycle is still alive until + * {@link #shutdown()} has been called and all of the actions registered with the shutdown + * controller have completed. + * + * @return {@code true} if the lifecycle is alive, {@code false} otherwise. + * + */ + public final boolean isAlive() { + synchronized (waitMonitor) { + return !destroyed; + } + } + + /** + * Allows a caller to wait forever; typically used when all work is done in daemon threads. + * Will exit on interrupts. + */ + public final void awaitShutdown() { + LOG.info("Awaiting shutdown"); + synchronized (waitMonitor) { + while (!destroyed) { + try { + waitMonitor.wait(); + } catch (InterruptedException e) { + LOG.info("Exiting on interrupt"); + shutdown(); + return; + } + } + } + } + + /** + * Initiates an orderly shutdown of the lifecycle's registered shutdown hooks. + */ + public final void shutdown() { + synchronized (waitMonitor) { + if (!destroyed) { + destroyed = true; + LOG.info("Shutting down application"); + shutdownRegistry.execute(); + waitMonitor.notifyAll(); + } + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/ShutdownRegistry.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/ShutdownRegistry.java b/commons/src/main/java/com/twitter/common/application/ShutdownRegistry.java new file mode 100644 index 0000000..993d273 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/ShutdownRegistry.java @@ -0,0 +1,102 @@ +// ================================================================================================= +// 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; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; + +import com.twitter.common.base.Command; +import com.twitter.common.base.ExceptionalCommand; + +/** + * A shutdown action controller. It executes actions in the reverse order they were registered, and + * logs a warning for every shutdown action that fails, but doesn't prevent completion of subsequent + * actions or the normal completion of the {@code execute()} method. + * + * @author Attila Szegedi + */ +public interface ShutdownRegistry { + + /** + * Adds an action to the shutdown registry. + * + * @param action Action to register. + * @param <E> Exception type thrown by the action. + * @param <T> Type of command. + */ + <E extends Exception, T extends ExceptionalCommand<E>> void addAction(T action); + + /** + * Implementation of a shutdown registry. + */ + public static class ShutdownRegistryImpl implements ShutdownRegistry, Command { + private static final Logger LOG = Logger.getLogger(ShutdownRegistry.class.getName()); + + private final List<ExceptionalCommand<? extends Exception>> actions = Lists.newLinkedList(); + + private boolean completed = false; + + /** + * Registers an action to execute during {@link #execute()}. It is an error to call this method + * after calling {@link #execute()}. + * + * @param action the action to add to the list of actions to execute during execution + */ + @Override + public synchronized <E extends Exception, T extends ExceptionalCommand<E>> void addAction( + T action) { + Preconditions.checkState(!completed); + actions.add(action); + } + + /** + * Executes an application shutdown stage by executing all registered actions. This method can + * be called multiple times but will only execute the registered actions the first time. + * + * This sends output to System.out because logging is unreliable during JVM shutdown, which + * this class may be used for. + */ + @Override + public synchronized void execute() { + if (!completed) { + LOG.info(String.format("Executing %d shutdown commands.", actions.size())); + completed = true; + try { + for (ExceptionalCommand<? extends Exception> action : Lists.reverse(actions)) { + // Part of our contract is ensuring each shutdown action executes so we must catch all + // exceptions. + // SUPPRESS CHECKSTYLE:OFF IllegalCatch + try { + action.execute(); + } catch (Exception e) { + LOG.log(Level.WARNING, "Shutdown action failed.", e); + } + // SUPPRESS CHECKSTYLE:ON IllegalCatch + } + } finally { + actions.clear(); + } + } else { + LOG.info("Action controller has already completed, subsequent calls ignored."); + } + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/ShutdownStage.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/ShutdownStage.java b/commons/src/main/java/com/twitter/common/application/ShutdownStage.java new file mode 100644 index 0000000..8c8b2bd --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/ShutdownStage.java @@ -0,0 +1,34 @@ +// ================================================================================================= +// 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; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +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; + +/** + * Binding annotation used for the shutdown registry. + */ +@BindingAnnotation +@Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) +public @interface ShutdownStage { } http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/StartupRegistry.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/StartupRegistry.java b/commons/src/main/java/com/twitter/common/application/StartupRegistry.java new file mode 100644 index 0000000..0643aff --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/StartupRegistry.java @@ -0,0 +1,55 @@ +// ================================================================================================= +// 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; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; + +import com.twitter.common.base.ExceptionalCommand; + +/** + * A registry that executes a set of commands. The registry will synchronously execute commands + * when {@link #execute()} is invoked, returning early if any action throws an exception. + * Only one call to {@link #execute()} will have an effect, all subsequent calls will be ignored. + */ +public class StartupRegistry implements ExceptionalCommand<Exception> { + + private static final Logger LOG = Logger.getLogger(StartupRegistry.class.getName()); + + private final Set<ExceptionalCommand> startupActions; + private final AtomicBoolean started = new AtomicBoolean(false); + + @Inject + public StartupRegistry(@StartupStage Set<ExceptionalCommand> startupActions) { + this.startupActions = Preconditions.checkNotNull(startupActions); + } + + @Override + public void execute() throws Exception { + if (!started.compareAndSet(false, true)) { + LOG.warning("Startup actions cannot be executed more than once, ignoring."); + } + + for (ExceptionalCommand<?> startupAction : startupActions) { + startupAction.execute(); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/StartupStage.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/StartupStage.java b/commons/src/main/java/com/twitter/common/application/StartupStage.java new file mode 100644 index 0000000..b8e6a52 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/StartupStage.java @@ -0,0 +1,34 @@ +// ================================================================================================= +// 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; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +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; + +/** + * Binding annotation used for the startup registry. + */ +@BindingAnnotation +@Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) +public @interface StartupStage { } http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/DefaultQuitHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/http/DefaultQuitHandler.java b/commons/src/main/java/com/twitter/common/application/http/DefaultQuitHandler.java new file mode 100644 index 0000000..2b5d0d5 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/http/DefaultQuitHandler.java @@ -0,0 +1,46 @@ +// ================================================================================================= +// 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.http; + +import java.util.logging.Logger; + +import com.google.inject.Inject; + +import com.twitter.common.application.Lifecycle; + +/** + * The default quit handler to use, which invokes {@link Lifecycle#shutdown()}. + * + * @author William Farner + */ +public class DefaultQuitHandler implements Runnable { + + private static final Logger LOG = Logger.getLogger(DefaultQuitHandler.class.getName()); + + private final Lifecycle lifecycle; + + @Inject + public DefaultQuitHandler(Lifecycle lifecycle) { + this.lifecycle = lifecycle; + } + + @Override + public void run() { + LOG.info("Instructing lifecycle to destroy."); + lifecycle.shutdown(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/GraphViewer.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/http/GraphViewer.java b/commons/src/main/java/com/twitter/common/application/http/GraphViewer.java new file mode 100644 index 0000000..5313c7e --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/http/GraphViewer.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.http; + +import com.google.inject.Binder; + +/** + * A utility class to register the file resources for the graph viewer. + */ +public final class GraphViewer { + + private GraphViewer() { + // Utility class. + } + + private static void registerJs(Binder binder, String assetName) { + Registration.registerHttpAsset( + binder, + "/graphview/" + assetName, + GraphViewer.class, + "graphview/" + assetName, + "application/javascript", + true); + } + + /** + * Registers required resources with the binder. + * + * @param binder Binder to register with. + */ + public static void registerResources(Binder binder) { + registerJs(binder, "dygraph-combined.js"); + registerJs(binder, "dygraph-extra.js"); + registerJs(binder, "grapher.js"); + registerJs(binder, "parser.js"); + Registration.registerHttpAsset(binder, + "/graphview", GraphViewer.class, "graphview/graphview.html", "text/html", false); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/HttpAssetConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/http/HttpAssetConfig.java b/commons/src/main/java/com/twitter/common/application/http/HttpAssetConfig.java new file mode 100644 index 0000000..4940751 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/http/HttpAssetConfig.java @@ -0,0 +1,54 @@ +// ================================================================================================= +// 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.http; + +import java.net.URL; + +import com.google.common.io.Resources; + +import com.twitter.common.net.http.handlers.AssetHandler; +import com.twitter.common.net.http.handlers.AssetHandler.StaticAsset; + +import static com.twitter.common.base.MorePreconditions.checkNotBlank; + +/** + * Configuration for a static HTTP-served asset. + * + * TODO(William Farner): Move this to a more appropriate package after initial AppLauncher check-in. + * + * @author William Farner + */ +public class HttpAssetConfig { + public final String path; + public final AssetHandler handler; + public final boolean silent; + + /** + * Creates a new asset configuration. + * + * @param path HTTP path the asset should be accessible from. + * @param asset Asset resource URL. + * @param contentType HTTP content-type to report for the asset. + * @param silent Whether the asset should be visible on the default index page. + */ + public HttpAssetConfig(String path, URL asset, String contentType, boolean silent) { + this.path = checkNotBlank(path); + this.handler = new AssetHandler( + new StaticAsset(Resources.newInputStreamSupplier(asset), contentType, true)); + this.silent = silent; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/HttpFilterConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/http/HttpFilterConfig.java b/commons/src/main/java/com/twitter/common/application/http/HttpFilterConfig.java new file mode 100644 index 0000000..864c621 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/http/HttpFilterConfig.java @@ -0,0 +1,26 @@ +package com.twitter.common.application.http; + +import javax.servlet.Filter; + +import com.twitter.common.base.MorePreconditions; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Configuration tuple for an HTTP filter. + */ +public class HttpFilterConfig { + public final Class<? extends Filter> filterClass; + public final String pathSpec; + + /** + * Creates a new filter configuration. + * + * @param filterClass Filter class. + * @param pathSpec Path spec that the filter should match. + */ + public HttpFilterConfig(Class<? extends Filter> filterClass, String pathSpec) { + this.pathSpec = MorePreconditions.checkNotBlank(pathSpec); + this.filterClass = checkNotNull(filterClass); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/application/http/HttpServletConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/application/http/HttpServletConfig.java b/commons/src/main/java/com/twitter/common/application/http/HttpServletConfig.java new file mode 100644 index 0000000..00479f0 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/application/http/HttpServletConfig.java @@ -0,0 +1,68 @@ +// ================================================================================================= +// 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.http; + +import javax.servlet.http.HttpServlet; + +import com.google.common.collect.ImmutableMap; + +import static com.google.common.base.Preconditions.checkNotNull; + +import static com.twitter.common.base.MorePreconditions.checkNotBlank; + +/** + * An {@link javax.servlet.http.HttpServlet} configuration used to mount HTTP handlers via + * {@link Registration#registerServlet(com.google.inject.Binder, HttpServletConfig)}. + * + * TODO(William Farner): Move this to a more appropriate package after initial AppLauncher check-in. + * + */ +public class HttpServletConfig { + public final String path; + public final Class<? extends HttpServlet> handlerClass; + public final ImmutableMap<String, String> params; + public final boolean silent; + + /** + * Creates a new servlet config. + * + * @param path the absolute path to mount the handler on + * @param servletClass the type of servlet that will render pages at {@code path} + * @param silent whether or not to display a link for this handler on the landing page + */ + public HttpServletConfig(String path, Class<? extends HttpServlet> servletClass, + boolean silent) { + this(path, servletClass, ImmutableMap.<String, String>of(), silent); + } + + /** + * Registers a new servlet config with servlet initialization parameters. + * + * @param path the absolute path to mount the handler on + * @param servletClass the type of servlet that will render pages at {@code path} + * @param params a map of servlet init parameters to initialize the servlet with + * @param silent whether or not to display a link for this handler on the landing page + */ + public HttpServletConfig(String path, Class<? extends HttpServlet> servletClass, + ImmutableMap<String, String> params, boolean silent) { + + this.path = checkNotBlank(path); + this.handlerClass = checkNotNull(servletClass); + this.params = checkNotNull(params); + this.silent = silent; + } +}
