This is an automated email from the ASF dual-hosted git repository. mattsicker pushed a commit to branch feature/3.x/graalvm-reachability in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 0d6fbafc30ab5d4ae882dd7a52e2c396e0b07035 Author: Matt Sicker <msic...@apple.com> AuthorDate: Fri Aug 15 12:35:45 2025 -0500 Port GraalVM reachability to 3.x This ports the GraalVmProcessor introduced in a14f0ad57ed380b19ddd715df6f3651acea6f8eb (released in 2.25.0) to the plugin model of 3.x. --- log4j-parent/pom.xml | 3 + .../log4j/plugin/processor/GraalVmProcessor.java | 355 +++++++++++++++++++++ .../plugin/processor/internal/Annotations.java | 149 +++++++++ .../processor/internal/ReachabilityMetadata.java | 296 +++++++++++++++++ .../plugin/processor/GraalVmProcessorTest.java | 70 ++++ .../example/AbstractPluginWithGenericBuilder.java | 58 ++++ .../test/resources/example/ConfigurablePlugin.java | 63 ++++ .../test/resources/example/ConfigurableRecord.java | 33 ++ .../PluginWithGenericSubclassFoo1Builder.java | 66 ++++ .../test/resources/example/ValidatingPlugin.java | 64 ++++ .../ValidatingPluginWithGenericBuilder.java | 70 ++++ .../example/ValidatingPluginWithTypedBuilder.java | 65 ++++ .../test/resources/expected-reflect-config.json | 15 + 13 files changed, 1307 insertions(+) diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index 1f1b6a7575..e2f940675c 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -866,6 +866,9 @@ <arg>-Alog4j.docgen.version=${project.version}</arg> <arg>-Alog4j.docgen.description=${project.description}</arg> <arg>-Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern}</arg> + <!-- Provide arguments for the GraalVM processor --> + <arg>-Alog4j.graalvm.groupId=${project.groupId}</arg> + <arg>-Alog4j.graalvm.artifactId=${project.artifactId}</arg> </compilerArgs> </configuration> diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java new file mode 100644 index 0000000000..0f67a29f89 --- /dev/null +++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.plugin.processor; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; +import javax.tools.Diagnostic; +import javax.tools.StandardLocation; +import org.apache.logging.log4j.plugin.processor.internal.Annotations; +import org.apache.logging.log4j.plugin.processor.internal.ReachabilityMetadata; +import org.apache.logging.log4j.util.Strings; +import org.jspecify.annotations.Nullable; + +/** + * Java annotation processor that generates GraalVM metadata. + * <p> + * <strong>Note:</strong> The annotations listed here must also be classified by the {@link Annotations} helper. + * </p> + */ +@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL) +@SupportedAnnotationTypes({ + "org.apache.logging.log4j.plugins.Factory", + "org.apache.logging.log4j.plugins.PluginFactory", + "org.apache.logging.log4j.plugins.SingletonFactory", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory", + "org.apache.logging.log4j.core.config.plugins.PluginFactory", + "org.apache.logging.log4j.plugins.Inject", + "org.apache.logging.log4j.plugins.Named", + "org.apache.logging.log4j.plugins.PluginAttribute", + "org.apache.logging.log4j.plugins.PluginBuilderAttribute", + "org.apache.logging.log4j.plugins.PluginElement", + "org.apache.logging.log4j.plugins.PluginNode", + "org.apache.logging.log4j.plugins.PluginValue", + "org.apache.logging.log4j.core.config.plugins.PluginAttribute", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute", + "org.apache.logging.log4j.core.config.plugins.PluginConfiguration", + "org.apache.logging.log4j.core.config.plugins.PluginElement", + "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext", + "org.apache.logging.log4j.core.config.plugins.PluginNode", + "org.apache.logging.log4j.core.config.plugins.PluginValue", + "org.apache.logging.log4j.plugins.Plugin", + "org.apache.logging.log4j.core.config.plugins.Plugin", + "org.apache.logging.log4j.plugins.condition.Conditional", + "org.apache.logging.log4j.plugins.validation.Constraint" +}) +@SupportedOptions({"log4j.graalvm.groupId", "log4j.graalvm.artifactId"}) +public class GraalVmProcessor extends AbstractProcessor { + + static final String GROUP_ID = "log4j.graalvm.groupId"; + static final String ARTIFACT_ID = "log4j.graalvm.artifactId"; + private static final String LOCATION_PREFIX = "META-INF/native-image/log4j-generated/"; + private static final String LOCATION_SUFFIX = "/reflect-config.json"; + private static final String PROCESSOR_NAME = GraalVmProcessor.class.getSimpleName(); + + private final Map<String, ReachabilityMetadata.Type> reachableTypes = new HashMap<>(); + private final List<Element> processedElements = new ArrayList<>(); + private Annotations annotationUtil; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.annotationUtil = new Annotations(processingEnv.getElementUtils()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + Messager messager = processingEnv.getMessager(); + for (TypeElement annotation : annotations) { + Annotations.Type annotationType = annotationUtil.classifyAnnotation(annotation); + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + switch (annotationType) { + case INJECT: + processInject(element); + break; + case PLUGIN: + processPlugin(element); + break; + case META_ANNOTATION_STRATEGY: + processMetaAnnotationStrategy(element, annotation); + break; + case QUALIFIER: + processQualifier(element); + break; + case FACTORY: + processFactory(element); + break; + case UNKNOWN: + messager.printMessage( + Diagnostic.Kind.WARNING, + String.format( + "The annotation type `%s` is not handled by %s", annotation, PROCESSOR_NAME), + annotation); + } + processedElements.add(element); + } + } + // Write the result file + if (roundEnv.processingOver() && !reachableTypes.isEmpty()) { + writeReachabilityMetadata(); + } + // Do not claim the annotations to allow other annotation processors to run + return false; + } + + private void processInject(Element element) { + if (element instanceof ExecutableElement executableElement) { + var parent = safeCast(executableElement.getEnclosingElement(), TypeElement.class); + addMethod(parent, executableElement); + } else if (element instanceof VariableElement variableElement) { + var parent = safeCast(variableElement.getEnclosingElement(), TypeElement.class); + addField(parent, variableElement); + } + } + + private void processPlugin(Element element) { + TypeElement typeElement = safeCast(element, TypeElement.class); + for (Element child : typeElement.getEnclosedElements()) { + if (child instanceof ExecutableElement executableChild) { + if (executableChild.getModifiers().contains(Modifier.PUBLIC)) { + switch (executableChild.getSimpleName().toString()) { + // 1. All public constructors. + case "<init>": + addMethod(typeElement, executableChild); + break; + // 2. Static `newInstance` method used in, e.g. `PatternConverter` classes. + case "newInstance": + if (executableChild.getModifiers().contains(Modifier.STATIC)) { + addMethod(typeElement, executableChild); + } + break; + // 3. Other factory methods are annotated, so we don't deal with them here. + default: + } + } + } + } + } + + private void processMetaAnnotationStrategy(Element element, TypeElement annotation) { + // Add the metadata for the public constructors + processPlugin(annotationUtil.getAnnotationClassValue(element, annotation)); + } + + private void processQualifier(Element element) { + if (element.getKind() == ElementKind.FIELD) { + addField( + safeCast(element.getEnclosingElement(), TypeElement.class), + safeCast(element, VariableElement.class)); + } + } + + private void processFactory(Element element) { + addMethod( + safeCast(element.getEnclosingElement(), TypeElement.class), safeCast(element, ExecutableElement.class)); + } + + private void writeReachabilityMetadata() { + // Compute the reachability metadata + ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(); + try { + ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), arrayOutputStream); + } catch (IOException e) { + String message = String.format( + "%s: an error occurred while generating reachability metadata: %s", PROCESSOR_NAME, e.getMessage()); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message); + return; + } + byte[] data = arrayOutputStream.toByteArray(); + + Map<String, String> options = processingEnv.getOptions(); + String reachabilityMetadataPath = getReachabilityMetadataPath( + options.get(GROUP_ID), options.get(ARTIFACT_ID), Integer.toHexString(Arrays.hashCode(data))); + Messager messager = processingEnv.getMessager(); + messager.printMessage( + Diagnostic.Kind.NOTE, + String.format( + "%s: writing GraalVM metadata for %d Java classes to `%s`.", + PROCESSOR_NAME, reachableTypes.size(), reachabilityMetadataPath)); + try (OutputStream output = processingEnv + .getFiler() + .createResource( + StandardLocation.CLASS_OUTPUT, + Strings.EMPTY, + reachabilityMetadataPath, + processedElements.toArray(Element[]::new)) + .openOutputStream()) { + output.write(data); + } catch (IOException e) { + String message = String.format( + "%s: unable to write reachability metadata to file `%s`", PROCESSOR_NAME, reachabilityMetadataPath); + messager.printMessage(Diagnostic.Kind.ERROR, message); + throw new IllegalArgumentException(message, e); + } + } + + /** + * Returns the path to the reachability metadata file. + * <p> + * If the groupId or artifactId is not specified, a warning is printed and a fallback folder name is used. + * The fallback folder name should be reproducible, but unique enough to avoid conflicts. + * </p> + * + * @param groupId The group ID of the plugin. + * @param artifactId The artifact ID of the plugin. + * @param fallbackFolderName The fallback folder name to use if groupId or artifactId is not specified. + */ + String getReachabilityMetadataPath( + @Nullable String groupId, @Nullable String artifactId, String fallbackFolderName) { + if (groupId == null || artifactId == null) { + String message = String.format( + "The `%1$s` annotation processor is missing the recommended `%2$s` and `%3$s` options.%n" + + "To follow the GraalVM recommendations, please add the following options to your build tool:%n" + + " -A%2$s=<groupId>%n" + + " -A%3$s=<artifactId>%n", + PROCESSOR_NAME, GROUP_ID, ARTIFACT_ID); + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message); + return LOCATION_PREFIX + fallbackFolderName + LOCATION_SUFFIX; + } + return LOCATION_PREFIX + groupId + '/' + artifactId + LOCATION_SUFFIX; + } + + private void addField(TypeElement parent, VariableElement element) { + ReachabilityMetadata.Type reachableType = + reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new); + reachableType.addField( + new ReachabilityMetadata.Field(element.getSimpleName().toString())); + } + + private void addMethod(TypeElement parent, ExecutableElement element) { + ReachabilityMetadata.Type reachableType = + reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new); + ReachabilityMetadata.Method method = + new ReachabilityMetadata.Method(element.getSimpleName().toString()); + element.getParameters().stream().map(v -> toString(v.asType())).forEach(method::addParameterType); + reachableType.addMethod(method); + } + + private <T extends Element> T safeCast(Element element, Class<T> type) { + if (type.isInstance(element)) { + return type.cast(element); + } + // This should never happen, unless annotations start appearing on unexpected elements. + String msg = String.format( + "Unexpected type of element `%s`: expecting `%s` but found `%s`", + element, type.getName(), element.getClass().getName()); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element); + throw new IllegalStateException(msg); + } + + /** + * Returns the fully qualified name of a type. + * + * @param type A Java type. + */ + private String toString(TypeMirror type) { + return type.accept( + new SimpleTypeVisitor8<String, @Nullable Void>() { + @Override + protected String defaultAction(final TypeMirror e, @Nullable Void unused) { + return e.toString(); + } + + @Override + public String visitArray(final ArrayType t, @Nullable Void unused) { + return visit(t.getComponentType(), unused) + "[]"; + } + + @Override + public @Nullable String visitDeclared(final DeclaredType t, final Void unused) { + return safeCast(t.asElement(), TypeElement.class) + .getQualifiedName() + .toString(); + } + }, + null); + } + + /** + * Returns the fully qualified name of the element corresponding to a {@link DeclaredType}. + * + * @param element A Java language element. + */ + private String toString(Element element) { + return element.accept( + new SimpleElementVisitor8<String, @Nullable Void>() { + @Override + public String visitPackage(PackageElement e, @Nullable Void unused) { + return e.getQualifiedName().toString(); + } + + @Override + public String visitType(TypeElement e, @Nullable Void unused) { + Element parent = e.getEnclosingElement(); + String separator = parent.getKind() == ElementKind.PACKAGE ? "." : "$"; + return visit(parent, unused) + + separator + + e.getSimpleName().toString(); + } + + @Override + protected String defaultAction(Element e, @Nullable Void unused) { + return ""; + } + }, + null); + } +} diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java new file mode 100644 index 0000000000..50e8a65105 --- /dev/null +++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.plugin.processor.internal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.util.Elements; +import org.apache.logging.log4j.plugin.processor.GraalVmProcessor; + +public final class Annotations { + + private static final Collection<String> FACTORY_TYPE_NAMES = List.of( + "org.apache.logging.log4j.plugins.Factory", + "org.apache.logging.log4j.plugins.PluginFactory", + "org.apache.logging.log4j.plugins.SingletonFactory", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory", + "org.apache.logging.log4j.core.config.plugins.PluginFactory"); + + private static final Collection<String> INJECT_NAMES = List.of("org.apache.logging.log4j.plugins.Inject"); + + private static final Collection<String> QUALIFIER_TYPE_NAMES = List.of( + "org.apache.logging.log4j.plugins.Named", + "org.apache.logging.log4j.plugins.PluginAttribute", + "org.apache.logging.log4j.plugins.PluginBuilderAttribute", + "org.apache.logging.log4j.plugins.PluginElement", + "org.apache.logging.log4j.plugins.PluginNode", + "org.apache.logging.log4j.plugins.PluginValue", + "org.apache.logging.log4j.core.config.plugins.PluginAttribute", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute", + "org.apache.logging.log4j.core.config.plugins.PluginConfiguration", + "org.apache.logging.log4j.core.config.plugins.PluginElement", + "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext", + "org.apache.logging.log4j.core.config.plugins.PluginNode", + "org.apache.logging.log4j.core.config.plugins.PluginValue"); + + /** + * These must be public types with either: + * <ul> + * <li>A factory method.</li> + * <li>A static method called {@code newInstance}.</li> + * <li>A public no-argument constructor.</li> + * </ul> + * <p> + * <strong>Note:</strong> The annotations listed here must also be declared in + * {@link GraalVmProcessor}. + * </p> + */ + private static final Collection<String> PLUGIN_ANNOTATION_NAMES = + List.of("org.apache.logging.log4j.plugins.Plugin", "org.apache.logging.log4j.core.config.plugins.Plugin"); + + /** + * Reflection is also used to create meta annotation strategies. + * . + * <p> + * <strong>Note:</strong> The annotations listed here must also be declared in + * {@link GraalVmProcessor}. + * </p> + */ + private static final Collection<String> META_ANNOTATION_STRATEGY_NAMES = List.of( + "org.apache.logging.log4j.plugins.condition.Conditional", + "org.apache.logging.log4j.plugins.validation.Constraint"); + + public enum Type { + INJECT, + /** + * Annotation used to mark a configuration attribute, element or other injected parameters. + */ + QUALIFIER, + /** + * Annotation used to mark a Log4j Plugin factory method. + */ + FACTORY, + /** + * Annotation used to mark a Log4j Plugin class. + */ + PLUGIN, + /** + * Annotation containing the name of a + * {@link org.apache.logging.log4j.plugins.validation.ConstraintValidator} + * or + * {@link org.apache.logging.log4j.plugins.condition.Condition}. + */ + META_ANNOTATION_STRATEGY, + /** + * Unknown + */ + UNKNOWN + } + + private final Map<TypeElement, Type> typeElementToTypeMap = new HashMap<>(); + + public Annotations(final Elements elements) { + FACTORY_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.FACTORY)); + INJECT_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.INJECT)); + QUALIFIER_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.QUALIFIER)); + PLUGIN_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.PLUGIN)); + META_ANNOTATION_STRATEGY_NAMES.forEach( + className -> addTypeElementIfExists(elements, className, Type.META_ANNOTATION_STRATEGY)); + } + + private void addTypeElementIfExists(Elements elements, CharSequence className, Type type) { + final TypeElement element = elements.getTypeElement(className); + if (element != null) { + typeElementToTypeMap.put(element, type); + } + } + + public Annotations.Type classifyAnnotation(TypeElement element) { + return typeElementToTypeMap.getOrDefault(element, Type.UNKNOWN); + } + + public Element getAnnotationClassValue(Element element, TypeElement annotation) { + // This prevents getting an "Attempt to access Class object for TypeMirror" exception + AnnotationMirror annotationMirror = element.getAnnotationMirrors().stream() + .filter(am -> am.getAnnotationType().asElement().equals(annotation)) + .findFirst() + .orElseThrow( + () -> new IllegalStateException("No `@" + annotation + "` annotation found on " + element)); + AnnotationValue annotationValue = annotationMirror.getElementValues().entrySet().stream() + .filter(e -> "value".equals(e.getKey().getSimpleName().toString())) + .map(Map.Entry::getValue) + .findFirst() + .orElseThrow(() -> + new IllegalStateException("No `value` found `@" + annotation + "` annotation on " + element)); + DeclaredType value = (DeclaredType) annotationValue.getValue(); + return value.asElement(); + } +} diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java new file mode 100644 index 0000000000..c65fcff721 --- /dev/null +++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.plugin.processor.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.IntStream; +import org.apache.logging.log4j.util.StringBuilders; +import org.jspecify.annotations.NullMarked; + +/** + * Provides support for the + * <a href="https://www.graalvm.org/latest/reference-manual/native-image/metadata/#specifying-metadata-with-json">{@code reachability-metadata.json}</a> + * file format. + */ +@NullMarked +public final class ReachabilityMetadata { + + /** + * Key used to specify the name of a field or method + */ + public static final String FIELD_OR_METHOD_NAME = "name"; + /** + * Key used to list the method parameter types. + */ + public static final String PARAMETER_TYPES = "parameterTypes"; + /** + * Key used to specify the name of a type. + * <p> + * Since GraalVM for JDK 23 it will be called "type". + * </p> + */ + public static final String TYPE_NAME = "name"; + /** + * Key used to specify the list of fields available for reflection. + */ + public static final String FIELDS = "fields"; + /** + * Key used to specify the list of methods available for reflection. + */ + public static final String METHODS = "methods"; + + private static class MinimalJsonWriter { + private final Appendable output; + + public MinimalJsonWriter(Appendable output) { + this.output = output; + } + + public void writeString(CharSequence input) throws IOException { + output.append('"'); + StringBuilder sb = new StringBuilder(input); + StringBuilders.escapeJson(sb, 0); + output.append(sb); + output.append('"'); + } + + public void writeObjectStart() throws IOException { + output.append('{'); + } + + public void writeObjectEnd() throws IOException { + output.append('}'); + } + + public void writeObjectKey(CharSequence key) throws IOException { + writeString(key); + output.append(':').append(' '); + } + + public void writeArrayStart() throws IOException { + output.append('['); + } + + public void writeSeparator() throws IOException { + output.append(',').append(' '); + } + + public void writeArrayEnd() throws IOException { + output.append(']'); + } + + public void writeLineSeparator() throws IOException { + output.append('\n'); + } + } + + /** + * Specifies a field that needs to be accessed through reflection. + */ + public static final class Field implements Comparable<Field> { + + private final String name; + + public Field(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + void toJson(MinimalJsonWriter jsonWriter) throws IOException { + jsonWriter.writeObjectStart(); + jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME); + jsonWriter.writeString(name); + jsonWriter.writeObjectEnd(); + } + + @Override + public int compareTo(Field other) { + return name.compareTo(other.name); + } + } + + /** + * Specifies a method that needs to be accessed through reflection. + */ + public static final class Method implements Comparable<Method> { + + private final String name; + private final List<String> parameterTypes = new ArrayList<>(); + + public Method(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void addParameterType(final String parameterType) { + parameterTypes.add(parameterType); + } + + void toJson(MinimalJsonWriter jsonWriter) throws IOException { + jsonWriter.writeObjectStart(); + jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME); + jsonWriter.writeString(name); + jsonWriter.writeSeparator(); + jsonWriter.writeObjectKey(PARAMETER_TYPES); + jsonWriter.writeArrayStart(); + boolean first = true; + for (String parameterType : parameterTypes) { + if (!first) { + jsonWriter.writeSeparator(); + } + first = false; + jsonWriter.writeString(parameterType); + } + jsonWriter.writeArrayEnd(); + jsonWriter.writeObjectEnd(); + } + + @Override + public int compareTo(Method other) { + int result = name.compareTo(other.name); + if (result == 0) { + result = parameterTypes.size() - other.parameterTypes.size(); + } + if (result == 0) { + result = IntStream.range(0, parameterTypes.size()) + .map(idx -> parameterTypes.get(idx).compareTo(other.parameterTypes.get(idx))) + .filter(r -> r != 0) + .findFirst() + .orElse(0); + } + return result; + } + } + + /** + * Specifies a Java type that needs to be accessed through reflection. + */ + public static final class Type { + + private final String type; + private final Collection<Method> methods = new TreeSet<>(); + private final Collection<Field> fields = new TreeSet<>(); + + public Type(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void addMethod(Method method) { + methods.add(method); + } + + public void addField(Field field) { + fields.add(field); + } + + void toJson(MinimalJsonWriter jsonWriter) throws IOException { + jsonWriter.writeObjectStart(); + jsonWriter.writeObjectKey(TYPE_NAME); + jsonWriter.writeString(type); + jsonWriter.writeSeparator(); + + boolean first = true; + jsonWriter.writeObjectKey(METHODS); + jsonWriter.writeArrayStart(); + for (Method method : methods) { + if (!first) { + jsonWriter.writeSeparator(); + } + first = false; + method.toJson(jsonWriter); + } + jsonWriter.writeArrayEnd(); + jsonWriter.writeSeparator(); + + first = true; + jsonWriter.writeObjectKey(FIELDS); + jsonWriter.writeArrayStart(); + for (Field field : fields) { + if (!first) { + jsonWriter.writeSeparator(); + } + first = false; + field.toJson(jsonWriter); + } + jsonWriter.writeArrayEnd(); + jsonWriter.writeObjectEnd(); + } + } + + /** + * Collection of reflection metadata. + */ + public static final class Reflection { + + private final Collection<Type> types = new TreeSet<>(Comparator.comparing(Type::getType)); + + public Reflection(Collection<Type> types) { + this.types.addAll(types); + } + + void toJson(MinimalJsonWriter jsonWriter) throws IOException { + boolean first = true; + jsonWriter.writeArrayStart(); + for (Type type : types) { + if (!first) { + jsonWriter.writeSeparator(); + } + first = false; + jsonWriter.writeLineSeparator(); + type.toJson(jsonWriter); + } + jsonWriter.writeLineSeparator(); + jsonWriter.writeArrayEnd(); + } + } + + /** + * Writes the contents of a {@code reflect-config.json} file. + * + * @param types The reflection metadata for types. + * @param output The object to use as output. + */ + public static void writeReflectConfig(Collection<Type> types, OutputStream output) throws IOException { + try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) { + Reflection reflection = new Reflection(types); + MinimalJsonWriter jsonWriter = new MinimalJsonWriter(writer); + reflection.toJson(jsonWriter); + jsonWriter.writeLineSeparator(); + } + } + + private ReachabilityMetadata() {} +} diff --git a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java new file mode 100644 index 0000000000..9e11f0be93 --- /dev/null +++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.plugin.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class GraalVmProcessorTest { + static final String GROUP_ID = "org.apache.logging.log4j"; + static final String ARTIFACT_ID = "log4j-plugin-processor-test"; + static final Path REFLECT_CONFIG_PATH = + Path.of("META-INF", "native-image", "log4j-generated", GROUP_ID, ARTIFACT_ID, "reflect-config.json"); + + static String readExpectedReflectConfig() throws IOException { + var url = Objects.requireNonNull(GraalVmProcessorTest.class.getResource("/expected-reflect-config.json")); + try (var inputStream = url.openStream()) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + static String readActualReflectConfig(Path baseDirectory) throws IOException { + return Files.readString(baseDirectory.resolve(REFLECT_CONFIG_PATH)); + } + + static List<Path> findInputSourceFiles() throws IOException { + try (var stream = Files.list(Path.of("src", "test", "resources", "example"))) { + return stream.filter(Files::isRegularFile).toList(); + } + } + + @Test + void verifyAnnotationProcessorGeneratesExpectedReachability(@TempDir Path outputDir) throws Exception { + var compiler = ToolProvider.getSystemJavaCompiler(); + var fileManager = compiler.getStandardFileManager(null, null, null); + fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, List.of(outputDir)); + fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, List.of(outputDir)); + var sourceFiles = fileManager.getJavaFileObjectsFromPaths(findInputSourceFiles()); + var options = List.of("-Alog4j.graalvm.groupId=" + GROUP_ID, "-Alog4j.graalvm.artifactId=" + ARTIFACT_ID); + var task = compiler.getTask(null, fileManager, null, options, null, sourceFiles); + task.setProcessors(List.of(new GraalVmProcessor())); + assertEquals(true, task.call()); + String expected = readExpectedReflectConfig(); + String actual = readActualReflectConfig(outputDir); + assertEquals(expected, actual); + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java new file mode 100644 index 0000000000..8350441b24 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import org.apache.logging.log4j.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.plugins.validation.constraints.Required; + +/** + * + */ +public class AbstractPluginWithGenericBuilder { + + public abstract static class Builder<B extends Builder<B>> { + + @PluginBuilderAttribute + @Required(message = "The thing given by the builder is null") + private String thing; + + @SuppressWarnings("unchecked") + public B asBuilder() { + return (B) this; + } + + public String getThing() { + return thing; + } + + public B setThing(final String name) { + this.thing = name; + return asBuilder(); + } + } + + private final String thing; + + public AbstractPluginWithGenericBuilder(final String thing) { + super(); + this.thing = thing; + } + + public String getThing() { + return thing; + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java new file mode 100644 index 0000000000..512a5eb4f8 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Inject; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginElement; + +@Configurable +@Plugin("configurable") +public class ConfigurablePlugin { + private final ValidatingPlugin alpha; + private final ValidatingPluginWithGenericBuilder beta; + private final ValidatingPluginWithTypedBuilder gamma; + private final PluginWithGenericSubclassFoo1Builder delta; + + @Inject + public ConfigurablePlugin( + @PluginElement final ValidatingPlugin alpha, + @PluginElement final ValidatingPluginWithGenericBuilder beta, + @PluginElement final ValidatingPluginWithTypedBuilder gamma, + @PluginElement final PluginWithGenericSubclassFoo1Builder delta) { + this.alpha = alpha; + this.beta = beta; + this.gamma = gamma; + this.delta = delta; + } + + public String getAlphaName() { + return alpha.getName(); + } + + public String getBetaName() { + return beta.getName(); + } + + public String getGammaName() { + return gamma.getName(); + } + + public String getDeltaThing() { + return delta.getThing(); + } + + public String getDeltaName() { + return delta.getFoo1(); + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java new file mode 100644 index 0000000000..ce69006fdf --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Inject; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginElement; + +@Configurable +@Plugin +public record ConfigurableRecord( + @PluginElement ValidatingPlugin alpha, + @PluginElement ValidatingPluginWithGenericBuilder beta, + @PluginElement ValidatingPluginWithTypedBuilder gamma, + @PluginElement PluginWithGenericSubclassFoo1Builder delta) { + @Inject + public ConfigurableRecord {} +} diff --git a/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java new file mode 100644 index 0000000000..64a28433f0 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginAttribute; +import org.apache.logging.log4j.plugins.PluginFactory; +import org.apache.logging.log4j.plugins.validation.constraints.Required; + +@Configurable +@Plugin("PluginWithGenericSubclassFoo1Builder") +public class PluginWithGenericSubclassFoo1Builder extends AbstractPluginWithGenericBuilder { + + public static class Builder<B extends Builder<B>> extends AbstractPluginWithGenericBuilder.Builder<B> + implements org.apache.logging.log4j.plugins.util.Builder<PluginWithGenericSubclassFoo1Builder> { + + @PluginAttribute + @Required(message = "The foo1 given by the builder is null") + private String foo1; + + @Override + public PluginWithGenericSubclassFoo1Builder build() { + return new PluginWithGenericSubclassFoo1Builder(getThing(), getFoo1()); + } + + public String getFoo1() { + return foo1; + } + + public B setFoo1(final String foo1) { + this.foo1 = foo1; + return asBuilder(); + } + } + + @PluginFactory + public static <B extends Builder<B>> B newBuilder() { + return new Builder<B>().asBuilder(); + } + + private final String foo1; + + public PluginWithGenericSubclassFoo1Builder(final String thing, final String foo1) { + super(thing); + this.foo1 = foo1; + } + + public String getFoo1() { + return foo1; + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java new file mode 100644 index 0000000000..3b48000c93 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import java.util.Objects; +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.plugins.PluginFactory; +import org.apache.logging.log4j.plugins.validation.constraints.Required; + +/** + * + */ +@Configurable +@Plugin("Validator") +public class ValidatingPlugin { + + private final String name; + + public ValidatingPlugin(final String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + public String getName() { + return name; + } + + @PluginFactory + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPlugin> { + + @PluginBuilderAttribute + @Required(message = "The name given by the builder is null") + private String name; + + public Builder setName(final String name) { + this.name = name; + return this; + } + + @Override + public ValidatingPlugin build() { + return new ValidatingPlugin(name); + } + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java new file mode 100644 index 0000000000..211a003fe3 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import java.util.Objects; +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginAttribute; +import org.apache.logging.log4j.plugins.PluginFactory; +import org.apache.logging.log4j.plugins.validation.constraints.Required; + +/** + * + */ +@Configurable +@Plugin("ValidatingPluginWithGenericBuilder") +public class ValidatingPluginWithGenericBuilder { + + private final String name; + + public ValidatingPluginWithGenericBuilder(final String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + public String getName() { + return name; + } + + @PluginFactory + public static <B extends Builder<B>> B newBuilder() { + return new Builder<B>().asBuilder(); + } + + public static class Builder<B extends Builder<B>> + implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithGenericBuilder> { + + @PluginAttribute + @Required(message = "The name given by the builder is null") + private String name; + + public B setName(final String name) { + this.name = name; + return asBuilder(); + } + + @SuppressWarnings("unchecked") + public B asBuilder() { + return (B) this; + } + + @Override + public ValidatingPluginWithGenericBuilder build() { + return new ValidatingPluginWithGenericBuilder(name); + } + } +} diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java new file mode 100644 index 0000000000..2977b1041a --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import java.util.Objects; +import org.apache.logging.log4j.plugins.Configurable; +import org.apache.logging.log4j.plugins.Plugin; +import org.apache.logging.log4j.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.plugins.PluginFactory; +import org.apache.logging.log4j.plugins.validation.constraints.Required; + +/** + * + */ +@Configurable +@Plugin("ValidatingPluginWithTypedBuilder") +public class ValidatingPluginWithTypedBuilder { + + private final String name; + + public ValidatingPluginWithTypedBuilder(final String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + public String getName() { + return name; + } + + @PluginFactory + public static Builder<Integer> newBuilder() { + return new Builder<>(); + } + + public static class Builder<T> + implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithTypedBuilder> { + + @PluginBuilderAttribute + @Required(message = "The name given by the builder is null") + private String name; + + public Builder<T> setName(final String name) { + this.name = name; + return this; + } + + @Override + public ValidatingPluginWithTypedBuilder build() { + return new ValidatingPluginWithTypedBuilder(name); + } + } +} diff --git a/log4j-plugin-processor/src/test/resources/expected-reflect-config.json b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json new file mode 100644 index 0000000000..f5af8c6511 --- /dev/null +++ b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json @@ -0,0 +1,15 @@ +[ +{"name": "example.AbstractPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "thing"}]}, +{"name": "example.ConfigurablePlugin", "methods": [{"name": "<init>", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": []}, +{"name": "example.ConfigurableRecord", "methods": [{"name": "<init>", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": [{"name": "alpha"}, {"name": "beta"}, {"name": "delta"}, {"name": "gamma"}]}, +{"name": "example.FakePlugin", "methods": [{"name": "<init>", "parameterTypes": []}], "fields": []}, +{"name": "example.FakePlugin$Nested", "methods": [{"name": "<init>", "parameterTypes": []}], "fields": []}, +{"name": "example.PluginWithGenericSubclassFoo1Builder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String", "java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, +{"name": "example.PluginWithGenericSubclassFoo1Builder$Builder", "methods": [], "fields": [{"name": "foo1"}]}, +{"name": "example.ValidatingPlugin", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, +{"name": "example.ValidatingPlugin$Builder", "methods": [], "fields": [{"name": "name"}]}, +{"name": "example.ValidatingPluginWithGenericBuilder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, +{"name": "example.ValidatingPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "name"}]}, +{"name": "example.ValidatingPluginWithTypedBuilder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, +{"name": "example.ValidatingPluginWithTypedBuilder$Builder", "methods": [], "fields": [{"name": "name"}]} +]