blob: 461b21827f7154b7f4b9c797f663659f91b29662 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.openide.util.lookup.implspi;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;
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.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
/**
* Infrastructure for generating {@code META-INF/services/*} and
* {@code META-INF/namedservices/*} registrations from annotations. From version
* 8.40, it is not necessary (and is not recommended) to declare
* @{@link SupportedSourceVersion} on subclasses: the default implementation
* declares support for {@link SourceVersion#latest()}. Declare specific
* {@link SourceVersion} limits only when necessary.
*
* @since 8.1
*/
public abstract class AbstractServiceProviderProcessor extends AbstractProcessor {
private final Map<Filer,Map<String,SortedSet<ServiceLoaderLine>>> outputFilesByProcessor =
new WeakHashMap<Filer,Map<String,SortedSet<ServiceLoaderLine>>>();
private final Map<Filer,Map<String,List<Element>>> originatingElementsByProcessor =
new WeakHashMap<Filer,Map<String,List<Element>>>();
private final Map<TypeElement,Boolean> verifiedClasses = new WeakHashMap<TypeElement,Boolean>();
/** Throws IllegalStateException. For access by selected subclasses. */
protected AbstractServiceProviderProcessor() {
if (getClass().getName().equals("org.netbeans.modules.openide.util.ServiceProviderProcessor")) { // NOI18N
// OK subclass
return;
}
if (getClass().getName().equals("org.netbeans.modules.openide.util.URLStreamHandlerRegistrationProcessor")) { // NOI18N
// OK subclass
return;
}
if (getClass().getName().equals("org.netbeans.modules.openide.util.NamedServiceProcessor")) { // NOI18N
// OK subclass
return;
}
throw new IllegalStateException();
}
public @Override final boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.errorRaised()) {
return false;
}
if (roundEnv.processingOver()) {
writeServices();
outputFilesByProcessor.clear();
originatingElementsByProcessor.clear();
return true;
} else {
return handleProcess(annotations, roundEnv);
}
}
/**
* The regular body of {@link #process}.
* Called during regular rounds if there are no outstanding errors.
* In the last round, one of the processors will write out generated registrations.
* @param annotations as in {@link #process}
* @param roundEnv as in {@link #process}
* @return as in {@link #process}
*/
protected abstract boolean handleProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
/**
* Register a service.
* If the class does not have an appropriate signature, an error will be printed and the registration skipped.
* @param clazz the service implementation type (an error will be reported if not a {@link TypeElement})
* @param annotation the (top-level) annotation registering the service, for diagnostic purposes
* @param type the type to which the implementation must be assignable
* @param path a path under which to register, or "" if inapplicable
* @param position a position at which to register, or {@link Integer#MAX_VALUE} to skip
* @param supersedes possibly empty list of implementation to supersede
* @since 8.8
*/
protected final void register(
Element el, Class<? extends Annotation> annotation,
TypeMirror type, String path, int position, String... supersedes
) {
if (el.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Kind.ERROR, annotation.getName() + " is not applicable to a " + el.getKind(), el);
return;
}
if (el.getEnclosingElement().getKind() == ElementKind.CLASS && !el.getModifiers().contains(Modifier.STATIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Inner class needs to be static to be annotated with @ServiceProvider", el);
return;
}
TypeElement clazz = (TypeElement) el;
String impl = processingEnv.getElementUtils().getBinaryName(clazz).toString();
String xface = processingEnv.getElementUtils().getBinaryName((TypeElement) processingEnv.getTypeUtils().asElement(type)).toString();
if (!processingEnv.getTypeUtils().isAssignable(clazz.asType(), type)) {
AnnotationMirror ann = findAnnotationMirror(clazz, annotation);
processingEnv.getMessager().printMessage(Kind.ERROR, impl + " is not assignable to " + xface,
clazz, ann, findAnnotationValue(ann, "service"));
return;
}
String rsrc = (path.length() > 0 ? "META-INF/namedservices/" + path + "/" : "META-INF/services/") + xface;
Boolean verify = verifiedClasses.get(clazz);
if (verify == null) {
verify = verifyServiceProviderSignature(clazz, annotation);
verifiedClasses.put(clazz, verify);
}
if (!verify) {
return;
}
registerImpl(clazz, impl, rsrc, position, supersedes);
}
protected final void register(Element el, String path) {
TypeElement clazz = (TypeElement)el;
String impl = processingEnv.getElementUtils().getBinaryName(clazz).toString();
registerImpl(clazz, impl, path, Integer.MAX_VALUE);
}
private void registerImpl(
TypeElement clazz, String impl, String rsrc, int position, String... supersedes
) {
/*
processingEnv.getMessager().printMessage(Kind.NOTE,
impl + " to be registered as a " + xface + (path.length() > 0 ? " under " + path : ""));
*/
Filer filer = processingEnv.getFiler();
{
Map<String,List<Element>> originatingElements = originatingElementsByProcessor.get(filer);
if (originatingElements == null) {
originatingElements = new HashMap<String,List<Element>>();
originatingElementsByProcessor.put(filer, originatingElements);
}
List<Element> origEls = originatingElements.get(rsrc);
if (origEls == null) {
origEls = new ArrayList<Element>();
originatingElements.put(rsrc, origEls);
}
origEls.add(clazz);
}
Map<String,SortedSet<ServiceLoaderLine>> outputFiles = outputFilesByProcessor.get(filer);
if (outputFiles == null) {
outputFiles = new HashMap<String,SortedSet<ServiceLoaderLine>>();
outputFilesByProcessor.put(filer, outputFiles);
}
SortedSet<ServiceLoaderLine> lines = outputFiles.get(rsrc);
if (lines == null) {
lines = new TreeSet<ServiceLoaderLine>();
try {
try {
FileObject in = filer.getResource(StandardLocation.SOURCE_PATH, "", rsrc);
in.openInputStream().close();
processingEnv.getMessager().printMessage(Kind.ERROR,
"Cannot generate " + rsrc + " because it already exists in sources: " + in.toUri());
return;
} catch (NullPointerException ex) {
// trying to prevent java.lang.NullPointerException
// at com.sun.tools.javac.util.DefaultFileManager.getFileForOutput(DefaultFileManager.java:1078)
// at com.sun.tools.javac.util.DefaultFileManager.getFileForOutput(DefaultFileManager.java:1054)
// at com.sun.tools.javac.processing.JavacFiler.getResource(JavacFiler.java:434)
// at org.netbeans.modules.openide.util.AbstractServiceProviderProcessor.register(AbstractServiceProviderProcessor.java:163)
// at org.netbeans.modules.openide.util.ServiceProviderProcessor.register(ServiceProviderProcessor.java:99)
} catch (FileNotFoundException | NoSuchFileException x) {
// Good.
}
try {
FileObject in = filer.getResource(StandardLocation.CLASS_OUTPUT, "", rsrc);
InputStream is = in.openInputStream();
try {
ServiceLoaderLine.parse(new InputStreamReader(is, StandardCharsets.UTF_8), lines);
} finally {
is.close();
}
} catch (FileNotFoundException | NoSuchFileException x) {
// OK, created for the first time
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Kind.ERROR, x.toString());
return;
}
outputFiles.put(rsrc, lines);
}
lines.add(new ServiceLoaderLine(impl, position, supersedes));
}
/**
* @param element a source element
* @param annotation a type of annotation
* @return the instance of that annotation on the element, or null if not found
*/
private AnnotationMirror findAnnotationMirror(Element element, Class<? extends Annotation> annotation) {
for (AnnotationMirror ann : element.getAnnotationMirrors()) {
if (processingEnv.getElementUtils().getBinaryName((TypeElement) ann.getAnnotationType().asElement()).
contentEquals(annotation.getName())) {
return ann;
}
}
return null;
}
/**
* @param annotation an annotation instance (null permitted)
* @param name the name of an attribute of that annotation
* @return the corresponding value if found
*/
private AnnotationValue findAnnotationValue(AnnotationMirror annotation, String name) {
if (annotation != null) {
for (Map.Entry<? extends ExecutableElement,? extends AnnotationValue> entry : annotation.getElementValues().entrySet()) {
if (entry.getKey().getSimpleName().contentEquals(name)) {
return entry.getValue();
}
}
}
return null;
}
private boolean verifyServiceProviderSignature(TypeElement clazz, Class<? extends Annotation> annotation) {
AnnotationMirror ann = findAnnotationMirror(clazz, annotation);
if (!clazz.getModifiers().contains(Modifier.PUBLIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, clazz + " must be public", clazz, ann);
return false;
}
if (clazz.getModifiers().contains(Modifier.ABSTRACT)) {
processingEnv.getMessager().printMessage(Kind.ERROR, clazz + " must not be abstract", clazz, ann);
return false;
}
if (clazz.getEnclosingElement().getKind() != ElementKind.PACKAGE && !clazz.getModifiers().contains(Modifier.STATIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, clazz + " must be static", clazz, ann);
return false;
}
{
boolean hasDefaultCtor = false;
for (ExecutableElement constructor : ElementFilter.constructorsIn(clazz.getEnclosedElements())) {
if (constructor.getModifiers().contains(Modifier.PUBLIC) && constructor.getParameters().isEmpty()) {
hasDefaultCtor = true;
break;
}
}
if (!hasDefaultCtor) {
processingEnv.getMessager().printMessage(Kind.ERROR, clazz + " must have a public no-argument constructor", clazz, ann);
return false;
}
}
return true;
}
private void writeServices() {
for (Map.Entry<Filer,Map<String,SortedSet<ServiceLoaderLine>>> outputFiles : outputFilesByProcessor.entrySet()) {
Filer filer = outputFiles.getKey();
for (Map.Entry<String,SortedSet<ServiceLoaderLine>> entry : outputFiles.getValue().entrySet()) {
try {
FileObject out = filer.createResource(StandardLocation.CLASS_OUTPUT, "", entry.getKey(),
originatingElementsByProcessor.get(filer).get(entry.getKey()).toArray(new Element[0]));
OutputStream os = out.openOutputStream();
try {
PrintWriter w = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
for (ServiceLoaderLine line : entry.getValue()) {
line.write(w);
}
w.flush();
w.close();
} finally {
os.close();
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write to " + entry.getKey() + ": " + x.toString());
}
}
}
}
@Deprecated
protected final void register(TypeElement el, Class<? extends Annotation> annotation,
TypeMirror type, String path, int position, String[] supersedes) {
register((Element) el, annotation, type, path, position, supersedes);
}
/**
* If the subclass itself does not define SupportedSourceVersion, assume latest(). If it does
* (was recommended prior to 8.40), returns the subclass' value.
* @return max supported source version.
* @since 8.40
*/
@Override
public SourceVersion getSupportedSourceVersion() {
SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
SourceVersion sv;
if (ssv == null) {
sv = SourceVersion.latest();
} else
sv = ssv.value();
return sv;
}
}