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 com.sun.source.util.TaskListener;
022import java.lang.annotation.Annotation;
023import java.lang.reflect.InvocationTargetException;
024import java.util.Arrays;
025import java.util.EnumSet;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Objects;
029import java.util.Set;
030import java.util.TreeSet;
031import java.util.stream.Stream;
032import javax.annotation.processing.ProcessingEnvironment;
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.ElementKind;
038import javax.lang.model.element.ExecutableElement;
039import javax.lang.model.element.Modifier;
040import javax.lang.model.element.PackageElement;
041import javax.lang.model.element.TypeElement;
042import javax.lang.model.element.VariableElement;
043import javax.lang.model.type.TypeMirror;
044import lombok.AllArgsConstructor;
045import lombok.NoArgsConstructor;
046import lombok.ToString;
047
048import static java.util.stream.Collectors.toCollection;
049import static java.util.stream.Collectors.toList;
050import static java.util.stream.Collectors.toSet;
051import static javax.tools.Diagnostic.Kind.ERROR;
052import static lombok.AccessLevel.PROTECTED;
053
054/**
055 * Abstract {@link javax.annotation.processing.Processor} base class for
056 * processing {@link Annotation}s specified by @{@link For}.  Provides
057 * built-in support for a number of {@link Annotation} types.
058 *
059 * {@bean.info}
060 *
061 * @see AnnotationValueMustConvertTo
062 * @see TargetMustBe
063 * @see TargetMustExtend
064 * @see TargetMustHaveConstructor
065 * @see TargetMustHaveModifiers
066 * @see TargetMustNotHaveModifiers
067 *
068 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
069 */
070@NoArgsConstructor(access = PROTECTED) @ToString
071public abstract class AnnotatedProcessor extends AbstractProcessor {
072    private final Set<String> processed = new TreeSet<>();
073
074    /**
075     * Method to get the {@link List} of supported {@link Annotation}
076     * {@link Class}es.
077     *
078     * @return  The {@link List} of supported {@link Annotation}
079     *          {@link Class}es.
080     */
081    protected List<Class<? extends Annotation>> getSupportedAnnotationTypeList() {
082        return Arrays.asList(getClass().getAnnotation(For.class).value());
083    }
084
085    @Override
086    public Set<String> getSupportedAnnotationTypes() {
087        Set<String> set =
088            getSupportedAnnotationTypeList().stream()
089            .map(Class::getCanonicalName)
090            .collect(toSet());
091
092        return set;
093    }
094
095    @Override
096    public void init(ProcessingEnvironment processingEnv) {
097        super.init(processingEnv);
098
099        try {
100            if (this instanceof ClassFileProcessor) {
101                TaskListener listener =
102                    new COMPILATIONFinishedTaskListener(javac, elements, processed, () -> onCOMPILATIONFinished());
103
104                javac.addTaskListener(listener);
105            }
106        } catch (Exception exception) {
107            print(ERROR, exception);
108        }
109    }
110
111    @Override
112    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
113        annotations.stream().forEach(t -> process(roundEnv, t));
114
115        return true;
116    }
117
118    private void process(RoundEnvironment roundEnv, TypeElement annotation) {
119        try {
120            roundEnv.getElementsAnnotatedWith(annotation).stream()
121                .peek(t -> processed.add(getEnclosingTypeBinaryName(t)))
122                .peek(new AnnotationValueMustConvertToCheck(annotation))
123                .peek(new TargetMustBeCheck(annotation))
124                .peek(new TargetMustHaveModifiersCheck(annotation))
125                .peek(new TargetMustNotHaveModifiersCheck(annotation))
126                .peek(new TargetMustExtendCheck(annotation))
127                .peek(new TargetMustHaveConstructorCheck(annotation))
128                .forEach(t -> process(roundEnv, annotation, t));
129        } catch (Throwable throwable) {
130            print(ERROR, throwable);
131        }
132    }
133
134    /**
135     * Callback method to process an annotated {@link Element}.  Default
136     * implementation does nothing.
137     *
138     * @param   roundEnv        The {@link RoundEnvironment}.
139     * @param   annotation      The annotation {@link TypeElement}.
140     * @param   element         The annotated {@link Element}.
141     */
142    protected void process(RoundEnvironment roundEnv, TypeElement annotation, Element element) {
143    }
144
145    private String getEnclosingTypeBinaryName(Element element) {
146        String name = null;
147
148        switch (element.getKind()) {
149        case ANNOTATION_TYPE:
150        case CLASS:
151        case ENUM:
152        case INTERFACE:
153            name = elements.getBinaryName((TypeElement) element).toString();
154            break;
155
156        case PACKAGE:
157            name = ((PackageElement) element).getQualifiedName().toString() + ".package-info";
158            break;
159
160        default:
161            name = getEnclosingTypeBinaryName(element.getEnclosingElement());
162            break;
163        }
164
165        return name;
166    }
167
168    private void onCOMPILATIONFinished() {
169        HashSet<Class<?>> set = new HashSet<>();
170
171        try {
172            ClassLoader loader = getClassPathClassLoader(fm);
173
174            for (String name : ClassFileProcessor.list(fm)) {
175                try {
176                    set.add(Class.forName(name, true, loader));
177                } catch (Throwable throwable) {
178                }
179            }
180
181            ((ClassFileProcessor) this).process(set, fm);
182        } catch (Exception exception) {
183            print(ERROR, exception);
184        }
185    }
186
187    @AllArgsConstructor @ToString
188    private class AnnotationValueMustConvertToCheck extends Check<Element> {
189        private final TypeElement annotation;
190
191        @Override
192        public void accept(Element element) {
193            AnnotationMirror meta = getAnnotationMirror(annotation, AnnotationValueMustConvertTo.class);
194
195            if (meta != null) {
196                AnnotationValue value = getAnnotationValue(meta, "value");
197                TypeElement to = (TypeElement) types.asElement((TypeMirror) value.getValue());
198                String method = (String) getAnnotationValue(meta, "method").getValue();
199                String name = (String) getAnnotationValue(meta, "name").getValue();
200                AnnotationMirror mirror = getAnnotationMirror(element, annotation);
201                AnnotationValue from = null;
202
203                try {
204                    from = getAnnotationValue(mirror, name);
205
206                    Class<?> type = Class.forName(to.getQualifiedName().toString());
207
208                    if (! method.isEmpty()) {
209                        type.getMethod(method, from.getValue().getClass())
210                            .invoke(null, from.getValue());
211                    } else {
212                        type.getConstructor(from.getValue().getClass())
213                            .newInstance(from.getValue());
214                    }
215                } catch (Exception exception) {
216                    Throwable throwable = exception;
217
218                    while (throwable instanceof InvocationTargetException) {
219                        throwable = throwable.getCause();
220                    }
221
222                    print(ERROR, element, mirror,
223                          "Cannot convert %s to %s\n%s",
224                          from, to.getQualifiedName(), throwable.getMessage());
225                }
226            }
227        }
228    }
229
230    @AllArgsConstructor @ToString
231    private class TargetMustBeCheck extends Check<Element> {
232        private final TypeElement annotation;
233
234        @Override
235        public void accept(Element element) {
236            AnnotationMirror meta = getAnnotationMirror(annotation, TargetMustBe.class);
237
238            if (meta != null) {
239                AnnotationValue value = getAnnotationValue(meta, "value");
240                String name = ((VariableElement) value.getValue()).getSimpleName().toString();
241                ElementKind kind = ElementKind.valueOf(name);
242
243                if (! kind.equals(element.getKind())) {
244                    print(ERROR, element,
245                          "@%s: %s is not a %s",
246                          annotation.getSimpleName(), element.getKind(), kind);
247                }
248            }
249        }
250    }
251
252    @AllArgsConstructor @ToString
253    private class TargetMustHaveModifiersCheck extends Check<Element> {
254        private final TypeElement annotation;
255
256        @Override
257        public void accept(Element element) {
258            AnnotationMirror meta = getAnnotationMirror(annotation, TargetMustHaveModifiers.class);
259
260            if (meta != null) {
261                EnumSet<Modifier> modifiers =
262                    Stream.of(getAnnotationValue(meta, "value"))
263                    .filter(Objects::nonNull)
264                    .map(t -> (List<?>) t.getValue())
265                    .flatMap(List::stream)
266                    .map(t -> ((AnnotationValue) t).getValue())
267                    .map(Objects::toString)
268                    .map(Modifier::valueOf)
269                    .collect(toCollection(() -> EnumSet.noneOf(Modifier.class)));
270
271                if (! withModifiers(modifiers).test(element)) {
272                    print(ERROR, element, "%s must be %s", element.getKind(), modifiers);
273                }
274            }
275        }
276    }
277
278    @AllArgsConstructor @ToString
279    private class TargetMustNotHaveModifiersCheck extends Check<Element> {
280        private final TypeElement annotation;
281
282        @Override
283        public void accept(Element element) {
284            AnnotationMirror meta = getAnnotationMirror(annotation, TargetMustNotHaveModifiers.class);
285
286            if (meta != null) {
287                EnumSet<Modifier> modifiers =
288                    Stream.of(getAnnotationValue(meta, "value"))
289                    .filter(Objects::nonNull)
290                    .map(t -> (List<?>) t.getValue())
291                    .flatMap(List::stream)
292                    .map(t -> ((AnnotationValue) t).getValue())
293                    .map(Objects::toString)
294                    .map(Modifier::valueOf)
295                    .collect(toCollection(() -> EnumSet.noneOf(Modifier.class)));
296
297                if (! withoutModifiers(modifiers).test(element)) {
298                    print(ERROR, element, "%s must not be %s", element.getKind(), modifiers);
299                }
300            }
301        }
302    }
303
304    @AllArgsConstructor @ToString
305    private class TargetMustExtendCheck extends Check<Element> {
306        private final TypeElement annotation;
307
308        @Override
309        public void accept(Element element) {
310            AnnotationMirror meta = getAnnotationMirror(annotation, TargetMustExtend.class);
311
312            if (meta != null) {
313                AnnotationValue value = getAnnotationValue(meta, "value");
314                TypeElement type = (TypeElement) types.asElement((TypeMirror) value.getValue());
315
316                if (! types.isAssignable(element.asType(), type.asType())) {
317                    print(ERROR, element,
318                          "@%s: %s does not extend %s",
319                          annotation.getSimpleName(), element, type.getQualifiedName());
320                }
321            }
322        }
323    }
324
325    @AllArgsConstructor @ToString
326    private class TargetMustHaveConstructorCheck extends Check<Element> {
327        private final TypeElement annotation;
328
329        @Override
330        public void accept(Element element) {
331            AnnotationMirror meta = getAnnotationMirror(annotation, TargetMustHaveConstructor.class);
332
333            if (meta != null) {
334                AnnotationValue value = getAnnotationValue(meta, "value");
335                String name = ((VariableElement) value.getValue()).getSimpleName().toString();
336                Modifier modifier = Modifier.valueOf(name);
337                List<TypeMirror> parameters =
338                    Stream.of(getAnnotationValue(meta, "parameters"))
339                    .filter(Objects::nonNull)
340                    .map(t -> (List<?>) t.getValue())
341                    .flatMap(List::stream)
342                    .map(t -> (AnnotationValue) t)
343                    .map(t -> (TypeMirror) t.getValue())
344                    .collect(toList());
345                ExecutableElement constructor = getConstructor((TypeElement) element, parameters);
346                boolean found = (constructor != null && constructor.getModifiers().contains(modifier));
347
348                if (! found) {
349                    print(ERROR, element, "@%s: No %s matching constructor", annotation.getSimpleName(), modifier);
350                }
351            }
352        }
353    }
354}