001package ball.annotation.processing;
002/*-
003 * ##########################################################################
004 * Utilities
005 * $Id: ServiceProviderForProcessor.java 6099 2020-06-03 17:01:31Z 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 - 2020 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: 6099 $
068 */
069@ServiceProviderFor({ Processor.class })
070@For({ ServiceProviderFor.class })
071@NoArgsConstructor @ToString
072public class ServiceProviderForProcessor extends AnnotatedProcessor
073                                         implements ClassFileProcessor {
074    private static final String PATH = "META-INF/services/%s";
075
076    @Override
077    protected void process(RoundEnvironment roundEnv,
078                           TypeElement annotation, Element element) {
079        super.process(roundEnv, annotation, element);
080
081        AnnotationMirror mirror = getAnnotationMirror(element, annotation);
082        AnnotationValue value = getAnnotationValue(mirror, "value");
083
084        if (! isEmptyArray(value)) {
085            String provider =
086                elements.getBinaryName((TypeElement) element).toString();
087            List<TypeElement> services =
088                Stream.of(value)
089                .filter(Objects::nonNull)
090                .map(t -> (List<?>) t.getValue())
091                .flatMap(List::stream)
092                .map(t -> (AnnotationValue) t)
093                .map(t -> (TypeMirror) t.getValue())
094                .map(t -> (TypeElement) types.asElement(t))
095                .collect(toList());
096
097            for (TypeElement service : services) {
098                if (! types.isAssignable(element.asType(), service.asType())) {
099                    print(ERROR, element,
100                          "@%s: %s does not implement %s",
101                          annotation.getSimpleName(),
102                          element.getKind(), service.getQualifiedName());
103                }
104            }
105        } else {
106            print(ERROR, element, mirror, value, "value() is empty");
107        }
108    }
109
110    @Override
111    public void process(Set<Class<?>> set,
112                        JavaFileManager fm) throws Throwable {
113        Map<String,Set<String>> map = new TreeMap<>();
114
115        for (Class<?> provider : set) {
116            if (! isAbstract(provider.getModifiers())) {
117                ServiceProviderFor annotation =
118                    provider.getAnnotation(ServiceProviderFor.class);
119
120                if (annotation != null) {
121                    for (Class<?> service : annotation.value()) {
122                        if (service.isAssignableFrom(provider)) {
123                            map.computeIfAbsent(service.getName(),
124                                                k -> new TreeSet<>())
125                                .add(provider.getName());
126                        }
127                    }
128                }
129            }
130        }
131
132        for (Map.Entry<String,Set<String>> entry : map.entrySet()) {
133            String service = entry.getKey();
134            FileObject file =
135                fm.getFileForOutput(CLASS_OUTPUT,
136                                    EMPTY, String.format(PATH, service), null);
137
138            try (PrintWriter writer = new PrintWriter(file.openWriter())) {
139                writer.println("# " + service);
140
141                entry.getValue().stream().forEach(t -> writer.println(t));
142            }
143        }
144    }
145}