// =================================================================================================
// 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.
*
* @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 {
Iterator<URL> allResources = getResources(0); // Try for a main
for (int nextResourceIndex = 1; /* until no resources */; nextResourceIndex++) {
Iterator<URL> resources = getResources(nextResourceIndex);
if (resources.hasNext()) {
allResources = Iterators.concat(allResources, resources);
} else {
return new ConfigurationResources(nextResourceIndex, 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);
}
}
}