001package ball.annotation.processing;
002/*-
003 * ##########################################################################
004 * Utilities
005 * $Id: ServiceProviderForProcessor.java 8257 2021-07-22 17:23:41Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/annotation/processing/ServiceProviderForProcessor.java $
007 * %%
008 * Copyright (C) 2008 - 2021 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import ball.annotation.ServiceProviderFor;
024import java.io.PrintWriter;
025import java.lang.reflect.Method;
026import java.util.Collections;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Set;
031import java.util.TreeMap;
032import java.util.TreeSet;
033import java.util.stream.Stream;
034import javax.annotation.processing.Processor;
035import javax.annotation.processing.RoundEnvironment;
036import javax.lang.model.element.AnnotationMirror;
037import javax.lang.model.element.AnnotationValue;
038import javax.lang.model.element.Element;
039import javax.lang.model.element.ExecutableElement;
040import javax.lang.model.element.TypeElement;
041import javax.lang.model.type.TypeMirror;
042import javax.tools.FileObject;
043import javax.tools.JavaFileManager;
044import lombok.NoArgsConstructor;
045import lombok.ToString;
046
047import static java.lang.reflect.Modifier.isAbstract;
048import static java.util.stream.Collectors.toList;
049import static javax.lang.model.element.Modifier.ABSTRACT;
050import static javax.lang.model.element.Modifier.PUBLIC;
051import static javax.tools.Diagnostic.Kind.ERROR;
052import static javax.tools.StandardLocation.CLASS_OUTPUT;
053import static org.apache.commons.lang3.StringUtils.EMPTY;
054
055/**
056 * {@link Processor} implementation to check {@link Class}es annotated with
057 * {@link ServiceProviderFor} to verify the annotated {@link Class}:
058 * <ol>
059 *   <li value="1">Is concrete</li>
060 *   <li value="2">Has a public no-argument constructor</li>
061 *   <li value="3">
062 *     Implements the {@link Class}es specified by
063 *     {@link ServiceProviderFor#value()}
064 *   </li>
065 * </ol>
066 * or implements Java 9's {@code java.util.ServiceLoader.Provider}
067 * {@code public static T provider()} method.
068 * <p>
069 * Note: Google offers a similar
070 * {@link.uri https://github.com/google/auto/tree/master/service target=newtab AutoService}
071 * library.
072 * </p>
073 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
074 * @version $Revision: 8257 $
075 */
076@ServiceProviderFor({ Processor.class })
077@For({ ServiceProviderFor.class })
078@NoArgsConstructor @ToString
079public class ServiceProviderForProcessor extends AnnotatedProcessor
080                                         implements ClassFileProcessor {
081    private static abstract class PROTOTYPE {
082        public static Object provider() { return null; }
083    }
084
085    private static final Method PROTOTYPE =
086        PROTOTYPE.class.getDeclaredMethods()[0];
087
088    static { PROTOTYPE.setAccessible(true); }
089
090    private static final String PATH = "META-INF/services/%s";
091
092    @Override
093    protected void process(RoundEnvironment roundEnv,
094                           TypeElement annotation, Element element) {
095        super.process(roundEnv, annotation, element);
096
097        TypeElement type = (TypeElement) element;
098        AnnotationMirror mirror = getAnnotationMirror(type, annotation);
099        AnnotationValue value = getAnnotationValue(mirror, "value");
100
101        if (! isEmptyArray(value)) {
102            ExecutableElement method = getMethod(type, PROTOTYPE);
103
104            if (method != null) {
105                if (! method.getModifiers().containsAll(getModifiers(PROTOTYPE))) {
106                    print(ERROR, method,
107                          "@%s: %s is not %s",
108                          annotation.getSimpleName(),
109                          method.getKind(), modifiers(PROTOTYPE.getModifiers()));
110                }
111            } else {
112                if (! withoutModifiers(ABSTRACT).test(element)) {
113                    print(ERROR, element,
114                          "%s: %s must not be %s",
115                          annotation.getSimpleName(),
116                          element.getKind(), ABSTRACT);
117                }
118
119                ExecutableElement constructor =
120                    getConstructor((TypeElement) element, Collections.emptyList());
121                boolean found =
122                    (constructor != null && constructor.getModifiers().contains(PUBLIC));
123
124                if (! found) {
125                    print(ERROR, element,
126                          "@%s: No %s NO-ARG constructor",
127                          annotation.getSimpleName(), PUBLIC);
128                }
129            }
130
131            String provider = elements.getBinaryName(type).toString();
132            List<TypeElement> services =
133                Stream.of(value)
134                .filter(Objects::nonNull)
135                .map(t -> (List<?>) t.getValue())
136                .flatMap(List::stream)
137                .map(t -> (AnnotationValue) t)
138                .map(t -> (TypeMirror) t.getValue())
139                .map(t -> (TypeElement) types.asElement(t))
140                .collect(toList());
141
142            for (TypeElement service : services) {
143                if (! isAssignable(type, service)) {
144                    print(ERROR, type,
145                          "@%s: %s does not implement %s",
146                          annotation.getSimpleName(),
147                          type.getKind(), service.getQualifiedName());
148                }
149
150                if (method != null) {
151                    if (! isAssignable(method.getReturnType(), service.asType())) {
152                        print(ERROR, method,
153                              "@%s: %s does not return %s",
154                              annotation.getSimpleName(),
155                              method.getKind(), service.getQualifiedName());
156                    }
157                }
158            }
159        } else {
160            print(ERROR, type, mirror, value, "value() is empty");
161        }
162    }
163
164    private boolean isAssignable(Element from, Element to) {
165        return isAssignable(from.asType(), to.asType());
166    }
167
168    private boolean isAssignable(TypeMirror from, TypeMirror to) {
169        return types.isAssignable(types.erasure(from), types.erasure(to));
170    }
171
172    @Override
173    public void process(Set<Class<?>> set, JavaFileManager fm) throws Exception {
174        Map<String,Set<String>> map = new TreeMap<>();
175
176        for (Class<?> provider : set) {
177            ServiceProviderFor annotation =
178                provider.getAnnotation(ServiceProviderFor.class);
179
180            if (annotation != null) {
181                for (Class<?> service : annotation.value()) {
182                    if (service.isAssignableFrom(provider)) {
183                        map.computeIfAbsent(service.getName(), k -> new TreeSet<>())
184                            .add(provider.getName());
185                    }
186                }
187            }
188        }
189
190        for (Map.Entry<String,Set<String>> entry : map.entrySet()) {
191            String service = entry.getKey();
192            FileObject file =
193                fm.getFileForOutput(CLASS_OUTPUT,
194                                    EMPTY, String.format(PATH, service), null);
195
196            try (PrintWriter writer = new PrintWriter(file.openWriter())) {
197                writer.println("# " + service);
198
199                entry.getValue().stream().forEach(t -> writer.println(t));
200            }
201        }
202    }
203}