001package ball.annotation.processing;
002/*-
003 * ##########################################################################
004 * Utilities
005 * $Id: ServiceProviderForProcessor.java 7790 2021-04-05 03:43: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.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Set;
029import java.util.TreeMap;
030import java.util.TreeSet;
031import java.util.stream.Stream;
032import javax.annotation.processing.Processor;
033import javax.annotation.processing.RoundEnvironment;
034import javax.lang.model.element.AnnotationMirror;
035import javax.lang.model.element.AnnotationValue;
036import javax.lang.model.element.Element;
037import javax.lang.model.element.TypeElement;
038import javax.lang.model.type.TypeMirror;
039import javax.tools.FileObject;
040import javax.tools.JavaFileManager;
041import lombok.NoArgsConstructor;
042import lombok.ToString;
043
044import static java.lang.reflect.Modifier.isAbstract;
045import static java.util.stream.Collectors.toList;
046import static javax.tools.Diagnostic.Kind.ERROR;
047import static javax.tools.StandardLocation.CLASS_OUTPUT;
048import static org.apache.commons.lang3.StringUtils.EMPTY;
049
050/**
051 * {@link Processor} implementation to check {@link Class}es annotated with
052 * {@link ServiceProviderFor} to verify the annotated {@link Class}:
053 * <ol>
054 *   <li value="1">Is concrete</li>
055 *   <li value="2">Has a public no-argument constructor</li>
056 *   <li value="3">
057 *     Implements the {@link Class}es specified by
058 *     {@link ServiceProviderFor#value()}
059 *   </li>
060 * </ol>
061 *
062 * Note: Google offers a similar
063 * {@link.uri https://github.com/google/auto/tree/master/service target=newtab AutoService}
064 * library.
065 *
066 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
067 * @version $Revision: 7790 $
068 */
069@ServiceProviderFor({ Processor.class })
070@For({ ServiceProviderFor.class })
071@NoArgsConstructor @ToString
072public class ServiceProviderForProcessor extends AnnotatedProcessor
073                                         implements ClassFileProcessor {
074/*
075    private static abstract class PROTOTYPE {
076        public static Object provider() { }
077    }
078
079    private static final Method PROTOTYPE =
080        PROTOTYPE.class.getDeclaredMethods()[0];
081
082    static { PROTOTYPE.setAccessible(true); }
083*/
084    private static final String PATH = "META-INF/services/%s";
085
086    @Override
087    protected void process(RoundEnvironment roundEnv,
088                           TypeElement annotation, Element element) {
089        super.process(roundEnv, annotation, element);
090
091        AnnotationMirror mirror = getAnnotationMirror(element, annotation);
092        AnnotationValue value = getAnnotationValue(mirror, "value");
093
094        if (! isEmptyArray(value)) {
095            String provider =
096                elements.getBinaryName((TypeElement) element).toString();
097            List<TypeElement> services =
098                Stream.of(value)
099                .filter(Objects::nonNull)
100                .map(t -> (List<?>) t.getValue())
101                .flatMap(List::stream)
102                .map(t -> (AnnotationValue) t)
103                .map(t -> (TypeMirror) t.getValue())
104                .map(t -> (TypeElement) types.asElement(t))
105                .collect(toList());
106
107            for (TypeElement service : services) {
108                if (! types.isAssignable(types.erasure(element.asType()),
109                                         types.erasure(service.asType()))) {
110                    print(ERROR, element,
111                          "@%s: %s does not implement %s",
112                          annotation.getSimpleName(),
113                          element.getKind(), service.getQualifiedName());
114                }
115            }
116        } else {
117            print(ERROR, element, mirror, value, "value() is empty");
118        }
119    }
120
121    @Override
122    public void process(Set<Class<?>> set, JavaFileManager fm) throws Exception {
123        Map<String,Set<String>> map = new TreeMap<>();
124
125        for (Class<?> provider : set) {
126            if (! isAbstract(provider.getModifiers())) {
127                ServiceProviderFor annotation =
128                    provider.getAnnotation(ServiceProviderFor.class);
129
130                if (annotation != null) {
131                    for (Class<?> service : annotation.value()) {
132                        if (service.isAssignableFrom(provider)) {
133                            map.computeIfAbsent(service.getName(),
134                                                k -> new TreeSet<>())
135                                .add(provider.getName());
136                        }
137                    }
138                }
139            }
140        }
141
142        for (Map.Entry<String,Set<String>> entry : map.entrySet()) {
143            String service = entry.getKey();
144            FileObject file =
145                fm.getFileForOutput(CLASS_OUTPUT,
146                                    EMPTY, String.format(PATH, service), null);
147
148            try (PrintWriter writer = new PrintWriter(file.openWriter())) {
149                writer.println("# " + service);
150
151                entry.getValue().stream().forEach(t -> writer.println(t));
152            }
153        }
154    }
155}