blob: f12299a51046466e40bb6ef288556fa67329f666 [file] [log] [blame]
/*
* Copyright 2008 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License 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.google.auto.service.processor;
import static com.google.auto.common.AnnotationMirrors.getAnnotationValue;
import static com.google.auto.common.MoreElements.getAnnotationMirror;
import static com.google.auto.common.MoreStreams.toImmutableSet;
import static com.google.common.base.Throwables.getStackTraceAsString;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
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.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleAnnotationValueVisitor8;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
/**
* Processes {@link AutoService} annotations and generates the service provider
* configuration files described in {@link java.util.ServiceLoader}.
* <p>
* Processor Options:<ul>
* <li>{@code -Adebug} - turns on debug statements</li>
* <li>{@code -Averify=true} - turns on extra verification</li>
* </ul>
*/
@SupportedOptions({"debug", "verify"})
public class AutoServiceProcessor extends AbstractProcessor {
@VisibleForTesting
static final String MISSING_SERVICES_ERROR = "No service interfaces provided for element!";
/**
* Maps the class names of service provider interfaces to the
* class names of the concrete classes which implement them.
* <p>
* For example,
* {@code "com.google.apphosting.LocalRpcService" ->
* "com.google.apphosting.datastore.LocalDatastoreService"}
*/
private final Multimap<String, String> providers = HashMultimap.create();
@Override
public ImmutableSet<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* <ol>
* <li> For each class annotated with {@link AutoService}<ul>
* <li> Verify the {@link AutoService} interface value is correct
* <li> Categorize the class by its service interface
* </ul>
*
* <li> For each {@link AutoService} interface <ul>
* <li> Create a file named {@code META-INF/services/<interface>}
* <li> For each {@link AutoService} annotated class for this interface <ul>
* <li> Create an entry in the file
* </ul>
* </ul>
* </ol>
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
processImpl(annotations, roundEnv);
} catch (RuntimeException e) {
// We don't allow exceptions of any kind to propagate to the compiler
fatalError(getStackTraceAsString(e));
}
return false;
}
private void processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
generateConfigFiles();
} else {
processAnnotations(annotations, roundEnv);
}
}
private void processAnnotations(
Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
log(annotations.toString());
log(elements.toString());
for (Element e : elements) {
// TODO(gak): check for error trees?
TypeElement providerImplementer = MoreElements.asType(e);
AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
if (providerInterfaces.isEmpty()) {
error(MISSING_SERVICES_ERROR, e, annotationMirror);
continue;
}
for (DeclaredType providerInterface : providerInterfaces) {
TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
log("provider interface: " + providerType.getQualifiedName());
log("provider implementer: " + providerImplementer.getQualifiedName());
if (checkImplementer(providerImplementer, providerType, annotationMirror)) {
providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
} else {
String message =
"ServiceProviders must implement their service provider interface. "
+ providerImplementer.getQualifiedName()
+ " does not implement "
+ providerType.getQualifiedName();
error(message, e, annotationMirror);
}
}
}
}
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
// would like to be able to print the full path
// before we attempt to get the resource in case the behavior
// of filer.getResource does change to match the spec, but there's
// no good way to resolve CLASS_OUTPUT without first getting a resource.
FileObject existingFile =
filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
allServices.addAll(oldServices);
} catch (IOException e) {
// According to the javadoc, Filer.getResource throws an exception
// if the file doesn't already exist. In practice this doesn't
// appear to be the case. Filer.getResource will happily return a
// FileObject that refers to a non-existent file but will throw
// IOException if you try to open an input stream for it.
log("Resource file did not already exist.");
}
Set<String> newServices = new HashSet<>(providers.get(providerInterface));
if (!allServices.addAll(newServices)) {
log("No new service entries being added.");
continue;
}
log("New service file contents: " + allServices);
FileObject fileObject =
filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
try (OutputStream out = fileObject.openOutputStream()) {
ServicesFiles.writeServiceFile(allServices, out);
}
log("Wrote to: " + fileObject.toUri());
} catch (IOException e) {
fatalError("Unable to create " + resourceFile + ", " + e);
return;
}
}
}
/**
* Verifies {@link ServiceProvider} constraints on the concrete provider class. Note that these
* constraints are enforced at runtime via the ServiceLoader, we're just checking them at compile
* time to be extra nice to our users.
*/
private boolean checkImplementer(
TypeElement providerImplementer,
TypeElement providerType,
AnnotationMirror annotationMirror) {
String verify = processingEnv.getOptions().get("verify");
if (verify == null || !Boolean.parseBoolean(verify)) {
return true;
}
// TODO: We're currently only enforcing the subtype relationship
// constraint. It would be nice to enforce them all.
Types types = processingEnv.getTypeUtils();
if (types.isSubtype(providerImplementer.asType(), providerType.asType())) {
return true;
}
// Maybe the provider has generic type, but the argument to @AutoService can't be generic.
// So we allow that with a warning, which can be suppressed with @SuppressWarnings("rawtypes").
// See https://github.com/google/auto/issues/870.
if (types.isSubtype(providerImplementer.asType(), types.erasure(providerType.asType()))) {
if (!rawTypesSuppressed(providerImplementer)) {
warning(
"Service provider "
+ providerType
+ " is generic, so it can't be named exactly by @AutoService."
+ " If this is OK, add @SuppressWarnings(\"rawtypes\").",
providerImplementer,
annotationMirror);
}
return true;
}
return false;
}
private static boolean rawTypesSuppressed(Element element) {
for (; element != null; element = element.getEnclosingElement()) {
SuppressWarnings suppress = element.getAnnotation(SuppressWarnings.class);
if (suppress != null && Arrays.asList(suppress.value()).contains("rawtypes")) {
return true;
}
}
return false;
}
/**
* Returns the binary name of a reference type. For example,
* {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
*
*/
private String getBinaryName(TypeElement element) {
return getBinaryNameImpl(element, element.getSimpleName().toString());
}
private String getBinaryNameImpl(TypeElement element, String className) {
Element enclosingElement = element.getEnclosingElement();
if (enclosingElement instanceof PackageElement) {
PackageElement pkg = MoreElements.asPackage(enclosingElement);
if (pkg.isUnnamed()) {
return className;
}
return pkg.getQualifiedName() + "." + className;
}
TypeElement typeElement = MoreElements.asType(enclosingElement);
return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className);
}
/**
* Returns the contents of a {@code Class[]}-typed "value" field in a given {@code
* annotationMirror}.
*/
private ImmutableSet<DeclaredType> getValueFieldOfClasses(AnnotationMirror annotationMirror) {
return getAnnotationValue(annotationMirror, "value")
.accept(
new SimpleAnnotationValueVisitor8<ImmutableSet<DeclaredType>, Void>() {
@Override
public ImmutableSet<DeclaredType> visitType(TypeMirror typeMirror, Void v) {
// TODO(ronshapiro): class literals may not always be declared types, i.e.
// int.class, int[].class
return ImmutableSet.of(MoreTypes.asDeclared(typeMirror));
}
@Override
public ImmutableSet<DeclaredType> visitArray(
List<? extends AnnotationValue> values, Void v) {
return values.stream()
.flatMap(value -> value.accept(this, null).stream())
.collect(toImmutableSet());
}
},
null);
}
private void log(String msg) {
if (processingEnv.getOptions().containsKey("debug")) {
processingEnv.getMessager().printMessage(Kind.NOTE, msg);
}
}
private void warning(String msg, Element element, AnnotationMirror annotation) {
processingEnv.getMessager().printMessage(Kind.WARNING, msg, element, annotation);
}
private void error(String msg, Element element, AnnotationMirror annotation) {
processingEnv.getMessager().printMessage(Kind.ERROR, msg, element, annotation);
}
private void fatalError(String msg) {
processingEnv.getMessager().printMessage(Kind.ERROR, "FATAL ERROR: " + msg);
}
}