// ================================================================================================= // 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.PrintWriter; import java.io.Writer; import java.net.URL; import java.util.Date; import java.util.List; import java.util.Map; 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.ImmutableMap; 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.google.common.io.ByteSource; import com.google.common.io.CharSource; import com.google.common.io.LineProcessor; import com.google.common.io.Resources; import com.google.common.reflect.ClassPath; 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 String DEFAULT_RESOURCE_PREFIX; static { DEFAULT_RESOURCE_PREFIX = DEFAULT_RESOURCE_PACKAGE.replace('.', '/') + "/"; }; static final String DEFAULT_RESOURCE_SUFFIX = ".txt"; 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, ByteSource> URL_TO_INPUT = new Function<URL, ByteSource>() { @Override public ByteSource apply(final URL resource) { return Resources.asByteSource(resource); } }; private static final Function<ByteSource, CharSource> INPUT_TO_READER = new Function<ByteSource, CharSource>() { @Override public CharSource apply( final ByteSource input) { return input.asCharSource(Charsets.UTF_8); } }; private static final Function<URL, CharSource> URL_TO_READER = Functions.compose(INPUT_TO_READER, URL_TO_INPUT); private final ImmutableSet<ArgInfo> positionalInfos; private final ImmutableSet<ArgInfo> cmdLineInfos; private final ImmutableSet<ParserInfo> parserInfos; private final ImmutableSet<VerifierInfo> verifierInfos; private Configuration(Iterable<ArgInfo> positionalInfos, Iterable<ArgInfo> cmdLineInfos, Iterable<ParserInfo> parserInfos, Iterable<VerifierInfo> verifierInfos) { 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 { public final String className; 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 Builder(String className) { this.className = className; } 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() { return new Configuration(positionalInfos, argInfos, parserInfos, verifierInfos); } } /** * 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 { Map<String, URL> resources = getLiveResources(); if (resources.isEmpty()) { LOG.fine("No @CmdLine arg resources found on the classpath"); } else { LOG.fine("Loading @CmdLine config for: " + resources.keySet()); } CharSource input = CharSource.concat(Iterables.transform(resources.values(), URL_TO_READER)); return input.readLines(new ConfigurationParser()); } /** * Gets all relevant resources from our package. * * This filters classnames that actually exist, to avoid including Configuration * for classes that were removed. */ private static ImmutableMap<String, URL> getLiveResources() throws IOException { ClassLoader classLoader = Configuration.class.getClassLoader(); ClassPath classPath = ClassPath.from(classLoader); ImmutableMap.Builder<String, URL> resources = new ImmutableMap.Builder<String, URL>(); for (ClassPath.ResourceInfo resourceInfo : classPath.getResources()) { String name = resourceInfo.getResourceName(); // Find relevant resource files. if (name.startsWith(DEFAULT_RESOURCE_PREFIX) && name.endsWith(DEFAULT_RESOURCE_SUFFIX)) { String className = name.substring( DEFAULT_RESOURCE_PREFIX.length(), name.length() - DEFAULT_RESOURCE_SUFFIX.length()); // Include only those resources for live classes. if (classExists(classLoader, className)) { resources.put(className, resourceInfo.url()); } } } return resources.build(); } private static boolean classExists(ClassLoader cl, String className) { try { cl.loadClass(className); return true; } catch (ClassNotFoundException e) { // Expected for classes removed during incremental compilation. return false; } } private static final class ConfigurationParser implements LineProcessor<Configuration> { 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(); @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(positionalInfo.build(), fieldInfoBuilder.build(), parserInfoBuilder.build(), verifierInfoBuilder.build()); } } 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; } 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); } } }