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