From 2048b2bcf465659c9750af44f8b9a33608851b4c Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 12:40:59 +0200 Subject: [PATCH 1/6] Add `reachability-metadata.json` to all Log4j modules This PR adds a plugin processor that generates a GraalVM reachability metadata file and bundles it with each Log4j module. After this change, GraalVM will be able to handle Log4j Core without any user intervention. **Note**: I also added a static `resource-config.json` file that specifies that all resources starting with `log4j2` and the plugin descriptor must be included in the GraalVM image. --- log4j-core-test/pom.xml | 60 +++- .../plugins/processor/FakeAnnotations.java | 83 +++++ .../config/plugins/processor/FakePlugin.java | 54 +++ .../processor/GraalVmProcessorTest.java | 163 +++++++++ .../plugins/processor/GraalVmProcessor.java | 327 ++++++++++++++++++ .../plugins/processor/PluginProcessor.java | 51 +-- .../processor/internal/Annotations.java | 147 ++++++++ .../internal/ReachabilityMetadata.java | 314 +++++++++++++++++ .../plugins/processor/package-info.java | 2 +- .../log4j-core/resource-config.json | 12 + log4j-parent/pom.xml | 11 + 11 files changed, 1201 insertions(+), 23 deletions(-) create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakeAnnotations.java create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/Annotations.java create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java create mode 100644 log4j-core/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-core/resource-config.json diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml index f770ea88703..30ed7854e97 100644 --- a/log4j-core-test/pom.xml +++ b/log4j-core-test/pom.xml @@ -66,57 +66,69 @@ - + org.apache.logging.log4j log4j-api-test + org.apache.logging.log4j log4j-core + org.assertj assertj-core + org.awaitility awaitility + commons-io commons-io + org.apache.commons commons-lang3 + org.hamcrest hamcrest + com.google.code.java-allocation-instrumenter java-allocation-instrumenter + junit junit + org.junit.jupiter junit-jupiter-api + org.junit.platform junit-platform-commons + org.springframework spring-test + org.apache.activemq @@ -129,65 +141,77 @@ + org.apache-extras.beanshell bsh test + commons-codec commons-codec test + org.apache.commons commons-compress test + org.apache.commons commons-csv test + commons-logging commons-logging test + com.conversantmedia disruptor test + com.lmax disruptor test + org.zapodot embedded-ldap-junit test + org.apache.groovy groovy-dateutil test + org.apache.groovy groovy-jsr223 test + com.h2database h2 test + org.hsqldb @@ -195,145 +219,177 @@ jdk8 test + com.fasterxml.jackson.core jackson-databind test + com.fasterxml.jackson.dataformat jackson-dataformat-xml test + com.fasterxml.jackson.dataformat jackson-dataformat-yaml test + org.fusesource.jansi jansi test + javax.jms javax.jms-api test + com.sun.mail javax.mail test + javax.mail javax.mail-api test + org.jctools jctools-core test + org.zeromq jeromq test + org.jmdns jmdns test - + + + + net.javacrumbs.json-unit + json-unit-assertj + test + + net.javacrumbs.json-unit json-unit test + org.junit.jupiter junit-jupiter-engine test + org.junit.jupiter junit-jupiter-params test + org.junit-pioneer junit-pioneer test + org.junit.vintage junit-vintage-engine test + org.apache.kafka kafka-clients test + org.apache.maven maven-core test + org.mockito mockito-core test + org.mockito mockito-junit-jupiter test + org.codehaus.plexus plexus-utils test + com.github.tomakehurst wiremock-jre8 test + org.xmlunit xmlunit-core test + org.xmlunit xmlunit-matchers test + org.tukaani xz test + com.github.luben zstd-jni test + diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakeAnnotations.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakeAnnotations.java new file mode 100644 index 00000000000..f51243c748d --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakeAnnotations.java @@ -0,0 +1,83 @@ +/* + * 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.core.config.plugins.processor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.PluginVisitorStrategy; +import org.apache.logging.log4j.core.config.plugins.validation.Constraint; +import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator; +import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; + +/** + * Fake constraint and plugin visitor that are accessed through reflection. + */ +public class FakeAnnotations { + + @Constraint(FakeConstraintValidator.class) + public @interface FakeConstraint {} + + public static class FakeConstraintValidator implements ConstraintValidator { + @Override + public void initialize(FakeConstraint annotation) {} + + @Override + public boolean isValid(String name, Object value) { + return false; + } + } + + @PluginVisitorStrategy(FakePluginVisitor.class) + public @interface FakeAnnotation {} + + public static class FakePluginVisitor implements PluginVisitor { + + @Override + public PluginVisitor setAnnotation(Annotation annotation) { + return null; + } + + @Override + public PluginVisitor setAliases(String... aliases) { + return null; + } + + @Override + public PluginVisitor setStrSubstitutor(StrSubstitutor substitutor) { + return null; + } + + @Override + public PluginVisitor setMember(Member member) { + return null; + } + + @Override + public Object visit(Configuration configuration, Node node, LogEvent event, StringBuilder log) { + return null; + } + + @Override + public PluginVisitor setConversionType(Class conversionType) { + return null; + } + } +} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakePlugin.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakePlugin.java index 6da59e9afea..babf3a16ae8 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakePlugin.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/FakePlugin.java @@ -16,8 +16,21 @@ */ package org.apache.logging.log4j.core.config.plugins.processor; +import java.io.Serializable; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAliases; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext; +import org.apache.logging.log4j.core.config.plugins.PluginNode; +import org.apache.logging.log4j.core.config.plugins.PluginValue; /** * Test plugin class for unit tests. @@ -28,4 +41,45 @@ public class FakePlugin { @Plugin(name = "Nested", category = "Test") public static class Nested {} + + @PluginFactory + public static FakePlugin newPlugin( + @PluginAttribute("attribute") int attribute, + @PluginElement("layout") Layout layout, + @PluginConfiguration Configuration config, + @PluginNode Node node, + @PluginLoggerContext LoggerContext loggerContext, + @PluginValue("value") String value) { + return null; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute + private int attribute; + + @PluginElement("layout") + private Layout layout; + + @PluginConfiguration + private Configuration config; + + @PluginNode + private Node node; + + @PluginLoggerContext + private LoggerContext loggerContext; + + @PluginValue("value") + private String value; + + @Override + public FakePlugin build() { + return null; + } + } } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java new file mode 100644 index 00000000000..976e7a18e0a --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java @@ -0,0 +1,163 @@ +/* + * 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.core.config.plugins.processor; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Stream; +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class GraalVmProcessorTest { + + private static final String FAKE_PLUGIN = String.join( + "", + " {", + " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin\",", + " \"methods\": [", + " {", + " \"name\": \"\",", + " \"parameterTypes\": []", + " },", + " {", + " \"name\": \"newPlugin\",", + " \"parameterTypes\": [", + " \"int\",", + " \"org.apache.logging.log4j.core.Layout\",", + " \"org.apache.logging.log4j.core.config.Configuration\",", + " \"org.apache.logging.log4j.core.config.Node\",", + " \"org.apache.logging.log4j.core.LoggerContext\",", + " \"java.lang.String\"", + " ]", + " }", + " ],", + " \"fields\": []", + " }"); + private static final String FAKE_PLUGIN_BUILDER = String.join( + "\n", + "{", + " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin$Builder\",", + " \"methods\": [],", + " \"fields\": [", + " {", + " \"name\": \"attribute\"", + " },", + " {", + " \"name\": \"config\"", + " },", + " {", + " \"name\": \"layout\"", + " },", + " {", + " \"name\": \"loggerContext\"", + " },", + " {", + " \"name\": \"node\"", + " },", + " {", + " \"name\": \"value\"", + " }", + " ]", + " }"); + private static final String FAKE_PLUGIN_NESTED = String.join( + "\n", + " {", + " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin$Nested\",", + " \"methods\": [", + " {", + " \"name\": \"\",", + " \"parameterTypes\": []", + " }", + " ],", + " \"fields\": []", + " }"); + private static final String FAKE_CONSTRAINT_VALIDATOR = String.join( + "\n", + " {", + " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakeAnnotations$FakeConstraintValidator\",", + " \"methods\": [", + " {", + " \"name\": \"\",", + " \"parameterTypes\": []", + " }", + " ],", + " \"fields\": []", + " }"); + private static final String FAKE_PLUGIN_VISITOR = String.join( + "\n", + " {", + " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakeAnnotations$FakePluginVisitor\",", + " \"methods\": [", + " {", + " \"name\": \"\",", + " \"parameterTypes\": []", + " }", + " ],", + " \"fields\": []", + " }"); + + private static String reachabilityMetadata; + + @BeforeAll + static void setup() throws IOException { + // There are two descriptors, choose the one in `test-classes` + URL reachabilityMetadataUrl = null; + for (URL url : Collections.list(GraalVmProcessor.class + .getClassLoader() + .getResources("META-INF/native-image/org.apache.logging.log4j/log4j-core-test/reflect-config.json"))) { + if (url.getPath().contains("test-classes")) { + reachabilityMetadataUrl = url; + break; + } + } + Assertions.assertThat(reachabilityMetadataUrl).isNotNull(); + reachabilityMetadata = + IOUtils.toString(Objects.requireNonNull(reachabilityMetadataUrl), StandardCharsets.UTF_8); + } + + static Stream containsSpecificEntries() { + return Stream.of( + Arguments.of(FakePlugin.class, FAKE_PLUGIN), + Arguments.of(FakePlugin.Builder.class, FAKE_PLUGIN_BUILDER), + Arguments.of(FakePlugin.Nested.class, FAKE_PLUGIN_NESTED), + Arguments.of(FakeAnnotations.FakeConstraintValidator.class, FAKE_CONSTRAINT_VALIDATOR), + Arguments.of(FakeAnnotations.FakePluginVisitor.class, FAKE_PLUGIN_VISITOR)); + } + + @ParameterizedTest + @MethodSource + void containsSpecificEntries(Class clazz, String expectedJson) { + assertThatJson(reachabilityMetadata) + .inPath(filterByName(clazz)) + .isArray() + .contains(json(expectedJson)); + } + + private String filterByName(Class clazz) { + return String.format("$[?(@.name == '%s')]", clazz.getName()); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java new file mode 100644 index 00000000000..078d917aa7c --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java @@ -0,0 +1,327 @@ +/* + * 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.core.config.plugins.processor; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +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.FileObject; +import javax.tools.StandardLocation; +import org.apache.logging.log4j.core.config.plugins.processor.internal.Annotations; +import org.apache.logging.log4j.core.config.plugins.processor.internal.ReachabilityMetadata; +import org.apache.logging.log4j.util.Strings; +import org.jspecify.annotations.Nullable; + +/** + * Java annotation processor that generates GraalVM metadata. + *

+ * Note: The annotations listed here must also be classified by the {@link Annotations} helper. + *

+ */ +@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL) +@SupportedAnnotationTypes({ + "org.apache.logging.log4j.core.config.plugins.validation.Constraint", + "org.apache.logging.log4j.core.config.plugins.Plugin", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute", + "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory", + "org.apache.logging.log4j.core.config.plugins.PluginConfiguration", + "org.apache.logging.log4j.core.config.plugins.PluginElement", + "org.apache.logging.log4j.core.config.plugins.PluginFactory", + "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.core.config.plugins.PluginVisitorStrategy" +}) +@SupportedOptions({"log4j.graalvm.groupId", "log4j.graalvm.artifactId"}) +public class GraalVmProcessor extends AbstractProcessor { + + private final Map reachableTypes = new HashMap<>(); + private final List 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 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 PLUGIN: + processPlugin(element); + break; + case CONSTRAINT_OR_VISITOR: + processConstraintOrVisitor(element, annotation); + break; + case PARAMETER: + processParameter(element); + break; + case FACTORY: + processFactory(element); + break; + case UNKNOWN: + messager.printMessage( + Diagnostic.Kind.WARNING, + "The annotation type `" + annotation + "` is not handled by " + + GraalVmProcessor.class.getSimpleName(), + annotation); + } + processedElements.add(element); + } + } + // Write the result file + if (roundEnv.processingOver() && !reachableTypes.isEmpty()) { + // + // Many users will have `log4j-core` on the annotation processor path, but do not have Log4j Plugins. + // Therefore, we check for the annotation processor required options only if some elements were processed. + // + String groupId = processingEnv.getOptions().get("log4j.graalvm.groupId"); + String artifactId = processingEnv.getOptions().get("log4j.graalvm.artifactId"); + if (groupId == null || artifactId == null) { + messager.printMessage( + Diagnostic.Kind.ERROR, + "The `" + GraalVmProcessor.class.getName() + + "` annotation processor is missing the required `maven.groupId` and `maven.artifactId` options.\n" + + "The generation of GraalVM reflection metadata for your Log4j Plugins will be disabled."); + return false; + } + String reachabilityMetadataPath = + String.format("META-INF/native-image/%s/%s/reflect-config.json", groupId, artifactId); + try { + messager.printMessage( + Diagnostic.Kind.NOTE, + String.format( + "%s: writing GraalVM metadata for %d Java classes to `%s`.", + GraalVmProcessor.class.getSimpleName(), + reachableTypes.size(), + reachabilityMetadataPath)); + writeReachabilityMetadata(reachabilityMetadataPath, processedElements.toArray(new Element[0])); + } catch (IOException e) { + StringWriter sw = new StringWriter(); + sw.append(GraalVmProcessor.class.getSimpleName()) + .append(": unable to write reachability metadata to file ") + .append(reachabilityMetadataPath) + .append("\n"); + e.printStackTrace(new PrintWriter(sw)); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, sw.toString()); + } + } + // Do not claim the annotations to allow other annotation processors to run + return false; + } + + private void processPlugin(Element element) { + TypeElement typeElement = safeCast(element, TypeElement.class); + if (typeElement != null) { + for (Element child : typeElement.getEnclosedElements()) { + if (child instanceof ExecutableElement) { + ExecutableElement executableChild = (ExecutableElement) child; + if (executableChild.getModifiers().contains(Modifier.PUBLIC)) { + switch (executableChild.getSimpleName().toString()) { + // 1. All public constructors. + case "": + 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 processConstraintOrVisitor(Element element, TypeElement annotation) { + // Add the metadata for the public constructors + processPlugin(annotationUtil.getAnnotationClassValue(element, annotation)); + } + + private void processParameter(Element element) { + switch (element.getKind()) { + case FIELD: + { + VariableElement field = safeCast(element, VariableElement.class); + TypeElement typeElement = safeCast(element.getEnclosingElement(), TypeElement.class); + if (typeElement != null && field != null) { + addField(typeElement, field); + } + } + break; + case PARAMETER: + // Do nothing, the containing method must be annotated with a factory annotation. + break; + default: + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Invalid Log4j Attribute element.", element); + } + } + + private void processFactory(Element element) { + ExecutableElement method = safeCast(element, ExecutableElement.class); + TypeElement typeElement = safeCast(element.getEnclosingElement(), TypeElement.class); + if (typeElement != null && method != null) { + addMethod(typeElement, method); + } + } + + private void writeReachabilityMetadata(String reachabilityMetadataPath, Element... elements) throws IOException { + FileObject resource = processingEnv + .getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, Strings.EMPTY, reachabilityMetadataPath, elements); + try (OutputStream os = resource.openOutputStream(); + Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { + ReachabilityMetadata.Reflection reflection = new ReachabilityMetadata.Reflection(); + reachableTypes.values().forEach(reflection::addType); + ReachabilityMetadata.writeReflectConfig(reflection, writer); + } + } + + 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 @Nullable T safeCast(Element element, Class type) { + if (type.isInstance(element)) { + return type.cast(element); + } + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "Unexpected type of element `" + element + "`: expecting `" + type.getName() + "` but found `" + + element.getClass().getName() + "`", + element); + return null; + } + + /** + * Returns the fully qualified name of a type. + * + * @param type A Java type. + */ + private String toString(TypeMirror type) { + return type.accept( + new SimpleTypeVisitor8() { + @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 processingEnv.getTypeUtils().erasure(t).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() { + @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-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java index ecafc4eaa10..cb4b9444e98 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java @@ -22,9 +22,12 @@ import aQute.bnd.annotation.spi.ServiceProvider; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -39,7 +42,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.lang.model.util.SimpleElementVisitor7; -import javax.tools.Diagnostic.Kind; +import javax.tools.Diagnostic; import javax.tools.FileObject; import javax.tools.StandardLocation; import org.apache.logging.log4j.core.config.plugins.Plugin; @@ -50,7 +53,7 @@ * Annotation processor for pre-scanning Log4j 2 plugins. */ @ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL) -@SupportedAnnotationTypes("org.apache.logging.log4j.core.config.plugins.*") +@SupportedAnnotationTypes("org.apache.logging.log4j.core.config.plugins.Plugin") public class PluginProcessor extends AbstractProcessor { // TODO: this could be made more abstract to allow for compile-time and run-time plugin processing @@ -64,6 +67,7 @@ public class PluginProcessor extends AbstractProcessor { public static final String PLUGIN_CACHE_FILE = "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat"; + private final List processedElements = new ArrayList<>(); private final PluginCache pluginCache = new PluginCache(); @Override @@ -74,26 +78,33 @@ public SourceVersion getSupportedSourceVersion() { @Override public boolean process(final Set annotations, final RoundEnvironment roundEnv) { final Messager messager = processingEnv.getMessager(); - messager.printMessage(Kind.NOTE, "Processing Log4j annotations"); - try { + // Process the elements for this round + if (!annotations.isEmpty()) { final Set elements = roundEnv.getElementsAnnotatedWith(Plugin.class); - if (elements.isEmpty()) { - messager.printMessage(Kind.NOTE, "No elements to process"); - return false; - } collectPlugins(elements); - writeCacheFile(elements.toArray(EMPTY_ELEMENT_ARRAY)); - messager.printMessage(Kind.NOTE, "Annotations processed"); - return true; - } catch (final Exception ex) { - ex.printStackTrace(); - error(ex.getMessage()); - return false; + processedElements.addAll(elements); } - } - - private void error(final CharSequence message) { - processingEnv.getMessager().printMessage(Kind.ERROR, message); + // Write the cache file + if (roundEnv.processingOver() && !processedElements.isEmpty()) { + try { + messager.printMessage( + Diagnostic.Kind.NOTE, + String.format( + "%s: writing plugin descriptor for %d Log4j Plugins to `%s`.", + PluginProcessor.class.getSimpleName(), processedElements.size(), PLUGIN_CACHE_FILE)); + writeCacheFile(processedElements.toArray(EMPTY_ELEMENT_ARRAY)); + } catch (final Exception e) { + StringWriter sw = new StringWriter(); + sw.append(PluginProcessor.class.getSimpleName()) + .append(": unable to write plugin descriptor to file ") + .append(PLUGIN_CACHE_FILE) + .append("\n"); + e.printStackTrace(new PrintWriter(sw)); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, sw.toString()); + } + } + // Do not claim the annotations to allow other annotation processors to run + return false; } private void collectPlugins(final Iterable elements) { @@ -159,7 +170,7 @@ private static final class PluginAliasesElementVisitor private final Elements elements; private PluginAliasesElementVisitor(final Elements elements) { - super(Collections.emptyList()); + super(Collections.emptyList()); this.elements = elements; } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/Annotations.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/Annotations.java new file mode 100644 index 00000000000..e809ba3d4df --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/Annotations.java @@ -0,0 +1,147 @@ +/* + * 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.core.config.plugins.processor.internal; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +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; + +public final class Annotations { + + /** + * These are fields, methods or parameters that correspond to Log4j configuration attributes, elements, and other + * injected elements. + *

+ * Note: The annotations listed here must also be declared in + * {@link org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor}. + *

+ */ + private static final Collection PARAMETER_ANNOTATION_NAMES = Arrays.asList( + "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 are static methods that must be reachable through reflection. + *

+ * Note: The annotations listed here must also be declared in + * {@link org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor}. + *

+ */ + private static final Collection FACTORY_ANNOTATION_NAMES = Arrays.asList( + "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory", + "org.apache.logging.log4j.core.config.plugins.PluginFactory"); + /** + * These must be public types with either: + *
    + *
  • A factory method.
  • + *
  • A static method called {@code newInstance}.
  • + *
  • A public no-argument constructor.
  • + *
+ *

+ * Note: The annotations listed here must also be declared in + * {@link org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor}. + *

+ */ + private static final Collection PLUGIN_ANNOTATION_NAMES = + Collections.singletonList("org.apache.logging.log4j.core.config.plugins.Plugin"); + + /** + * Reflection is also used to create constraint validators and plugin visitors. + *

+ * Note: The annotations listed here must also be declared in + * {@link org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor}. + *

+ */ + private static final Collection CONSTRAINT_OR_VISITOR_ANNOTATION_NAMES = Arrays.asList( + "org.apache.logging.log4j.core.config.plugins.validation.Constraint", + "org.apache.logging.log4j.core.config.plugins.PluginVisitorStrategy"); + + public enum Type { + /** + * Annotation used to mark a configuration attribute, element or other injected parameters. + */ + PARAMETER, + /** + * 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.core.config.plugins.validation.ConstraintValidator} + * or + * {@link org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor}. + */ + CONSTRAINT_OR_VISITOR, + /** + * Unknown + */ + UNKNOWN + } + + private final Map typeElementToTypeMap = new HashMap<>(); + + public Annotations(final Elements elements) { + PARAMETER_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.PARAMETER)); + FACTORY_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.FACTORY)); + PLUGIN_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.PLUGIN)); + CONSTRAINT_OR_VISITOR_ANNOTATION_NAMES.forEach( + className -> addTypeElementIfExists(elements, className, Type.CONSTRAINT_OR_VISITOR)); + } + + 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-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java new file mode 100644 index 00000000000..a9cec9287d2 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java @@ -0,0 +1,314 @@ +/* + * 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.core.config.plugins.processor.internal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.IntStream; +import org.jspecify.annotations.NullMarked; + +/** + * Provides support for the + * {@code reachability-metadata.json} + * 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. + *

+ * Since GraalVM for JDK 23 it will be called "type". + *

+ */ + 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 static final int ESCAPED_CHARS_LENGHT = 128; + + /** + * BitSet of 7-bit ASCII characters that would require quoting in JSON. + */ + private static final BitSet ESCAPED_CHARS; + + static { + BitSet requiresLookup = new BitSet(ESCAPED_CHARS_LENGHT); + // Control chars need generic escape sequence + for (int i = 0; i < 32; ++i) { + requiresLookup.set(i); + } + // Others (and some within that range too) have explicit shorter sequences + requiresLookup.set('"'); + requiresLookup.set('\\'); + requiresLookup.set(0x08); + requiresLookup.set(0x09); + requiresLookup.set(0x0C); + requiresLookup.set(0x0A); + requiresLookup.set(0x0D); + ESCAPED_CHARS = requiresLookup; + } + + private final Appendable output; + + public MinimalJsonWriter(Appendable output) { + this.output = output; + } + + /** + * Writes a JSON String ignoring characters that need quoting. + */ + public void writeString(CharSequence input) throws IOException { + output.append('"'); + int limit = input.length(); + for (int i = 0; i < limit; ++i) { + char c = input.charAt(i); + if (c >= ESCAPED_CHARS_LENGHT || !ESCAPED_CHARS.get(c)) { + output.append(c); + } + } + 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(':'); + } + + public void writeArrayStart() throws IOException { + output.append('['); + } + + public void writeSeparator() throws IOException { + output.append(','); + } + + public void writeArrayEnd() throws IOException { + output.append(']'); + } + } + + /** + * Specifies a field that needs to be accessed through reflection. + */ + public static class Field implements Comparable { + + 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 class Method implements Comparable { + + private final String name; + private final List 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 class Type { + + private final String type; + private final Collection methods = new TreeSet<>(); + private final Collection 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 class Reflection { + + private final Collection types = new TreeSet<>(Comparator.comparing(Type::getType)); + + public void addType(Type type) { + types.add(type); + } + + void toJson(MinimalJsonWriter jsonWriter) throws IOException { + boolean first = true; + jsonWriter.writeArrayStart(); + for (Type type : types) { + if (!first) { + jsonWriter.writeSeparator(); + } + first = false; + type.toJson(jsonWriter); + } + jsonWriter.writeArrayEnd(); + } + } + + /** + * Writes the contents of a {@code reflect-config.json} file. + * + * @param reflection The reflection metadata. + * @param output The object to use as output. + */ + public static void writeReflectConfig(Reflection reflection, Appendable output) throws IOException { + MinimalJsonWriter jsonWriter = new MinimalJsonWriter(output); + reflection.toJson(jsonWriter); + } + + private ReachabilityMetadata() {} +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/package-info.java index 1322b47dcdf..bc28acee278 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/package-info.java @@ -20,7 +20,7 @@ * executable {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager} class in your build process. */ @Export -@Version("2.20.1") +@Version("2.25.0") package org.apache.logging.log4j.core.config.plugins.processor; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-core/resource-config.json b/log4j-core/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-core/resource-config.json new file mode 100644 index 00000000000..d5b51667ed9 --- /dev/null +++ b/log4j-core/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-core/resource-config.json @@ -0,0 +1,12 @@ +{ + "resources": { + "includes": [ + { + "pattern": "log4j2.*" + }, + { + "pattern": "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat" + } + ] + } +} \ No newline at end of file diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index 10e8c89a5b1..3c7ab46c958 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -576,6 +576,12 @@ ${jna.version} + + net.javacrumbs.json-unit + json-unit-assertj + ${json-unit.version} + + net.javacrumbs.json-unit json-unit @@ -1189,6 +1195,8 @@ org.apache.logging.log4j.docgen.processor.DescriptorGenerator org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor + + org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor @@ -1198,6 +1206,9 @@ -Alog4j.docgen.version=${project.version} -Alog4j.docgen.description=${project.description} -Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern} + + -Alog4j.graalvm.groupId=${project.groupId} + -Alog4j.graalvm.artifactId=${project.artifactId} only From d58894f84f4b00e491a9b77f4b30deb935fdcc98 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 16:00:33 +0200 Subject: [PATCH 2/6] Rewrite expected values as Maps --- .../processor/GraalVmProcessorTest.java | 155 ++++++++---------- 1 file changed, 69 insertions(+), 86 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java index 976e7a18e0a..7f1ee1fb69b 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java @@ -16,6 +16,9 @@ */ package org.apache.logging.log4j.core.config.plugins.processor; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; @@ -23,6 +26,8 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; import java.util.stream.Stream; import org.apache.commons.io.IOUtils; @@ -34,91 +39,69 @@ class GraalVmProcessorTest { - private static final String FAKE_PLUGIN = String.join( - "", - " {", - " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin\",", - " \"methods\": [", - " {", - " \"name\": \"\",", - " \"parameterTypes\": []", - " },", - " {", - " \"name\": \"newPlugin\",", - " \"parameterTypes\": [", - " \"int\",", - " \"org.apache.logging.log4j.core.Layout\",", - " \"org.apache.logging.log4j.core.config.Configuration\",", - " \"org.apache.logging.log4j.core.config.Node\",", - " \"org.apache.logging.log4j.core.LoggerContext\",", - " \"java.lang.String\"", - " ]", - " }", - " ],", - " \"fields\": []", - " }"); - private static final String FAKE_PLUGIN_BUILDER = String.join( - "\n", - "{", - " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin$Builder\",", - " \"methods\": [],", - " \"fields\": [", - " {", - " \"name\": \"attribute\"", - " },", - " {", - " \"name\": \"config\"", - " },", - " {", - " \"name\": \"layout\"", - " },", - " {", - " \"name\": \"loggerContext\"", - " },", - " {", - " \"name\": \"node\"", - " },", - " {", - " \"name\": \"value\"", - " }", - " ]", - " }"); - private static final String FAKE_PLUGIN_NESTED = String.join( - "\n", - " {", - " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakePlugin$Nested\",", - " \"methods\": [", - " {", - " \"name\": \"\",", - " \"parameterTypes\": []", - " }", - " ],", - " \"fields\": []", - " }"); - private static final String FAKE_CONSTRAINT_VALIDATOR = String.join( - "\n", - " {", - " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakeAnnotations$FakeConstraintValidator\",", - " \"methods\": [", - " {", - " \"name\": \"\",", - " \"parameterTypes\": []", - " }", - " ],", - " \"fields\": []", - " }"); - private static final String FAKE_PLUGIN_VISITOR = String.join( - "\n", - " {", - " \"name\": \"org.apache.logging.log4j.core.config.plugins.processor.FakeAnnotations$FakePluginVisitor\",", - " \"methods\": [", - " {", - " \"name\": \"\",", - " \"parameterTypes\": []", - " }", - " ],", - " \"fields\": []", - " }"); + private static final Object FAKE_PLUGIN = asMap( + "name", + FakePlugin.class.getName(), + "methods", + asList( + asMap("name", "", "parameterTypes", emptyList()), + asMap( + "name", + "newPlugin", + "parameterTypes", + asList( + "int", + "org.apache.logging.log4j.core.Layout", + "org.apache.logging.log4j.core.config.Configuration", + "org.apache.logging.log4j.core.config.Node", + "org.apache.logging.log4j.core.LoggerContext", + "java.lang.String"))), + "fields", + emptyList()); + private static final Object FAKE_PLUGIN_BUILDER = asMap( + "name", + FakePlugin.Builder.class.getName(), + "methods", + emptyList(), + "fields", + asList( + asMap("name", "attribute"), + asMap("name", "config"), + asMap("name", "layout"), + asMap("name", "loggerContext"), + asMap("name", "node"), + asMap("name", "value"))); + private static final Object FAKE_PLUGIN_NESTED = onlyNoArgsConstructor(FakePlugin.Nested.class); + private static final Object FAKE_CONSTRAINT_VALIDATOR = + onlyNoArgsConstructor(FakeAnnotations.FakeConstraintValidator.class); + private static final Object FAKE_PLUGIN_VISITOR = onlyNoArgsConstructor(FakeAnnotations.FakePluginVisitor.class); + + /** + * Generates a metadata element with just a single no-arg constructor. + * + * @param clazz The name of the metadata element. + * @return A GraalVM metadata element. + */ + private static Object onlyNoArgsConstructor(Class clazz) { + return asMap( + "name", + clazz.getName(), + "methods", + singletonList(asMap("name", "", "parameterTypes", emptyList())), + "fields", + emptyList()); + } + + private static Map asMap(Object... pairs) { + final Map map = new LinkedHashMap<>(); + if (pairs.length % 2 != 0) { + throw new IllegalArgumentException("odd number of arguments: " + pairs.length); + } + for (int i = 0; i < pairs.length; i += 2) { + map.put((String) pairs[i], pairs[i + 1]); + } + return map; + } private static String reachabilityMetadata; @@ -150,7 +133,7 @@ static Stream containsSpecificEntries() { @ParameterizedTest @MethodSource - void containsSpecificEntries(Class clazz, String expectedJson) { + void containsSpecificEntries(Class clazz, Object expectedJson) { assertThatJson(reachabilityMetadata) .inPath(filterByName(clazz)) .isArray() From 0717df689258756b6e4a9da1225e6c1cd3bb3470 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 16:28:31 +0200 Subject: [PATCH 3/6] Apply review suggestions --- .../processor/GraalVmProcessorTest.java | 8 +- .../plugins/processor/GraalVmProcessor.java | 171 ++++++++---------- .../internal/ReachabilityMetadata.java | 14 +- 3 files changed, 91 insertions(+), 102 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java index 7f1ee1fb69b..2c0133835db 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java @@ -21,6 +21,7 @@ import static java.util.Collections.singletonList; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; +import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.net.URL; @@ -28,10 +29,8 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; import java.util.stream.Stream; import org.apache.commons.io.IOUtils; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -117,9 +116,8 @@ static void setup() throws IOException { break; } } - Assertions.assertThat(reachabilityMetadataUrl).isNotNull(); - reachabilityMetadata = - IOUtils.toString(Objects.requireNonNull(reachabilityMetadataUrl), StandardCharsets.UTF_8); + assertThat(reachabilityMetadataUrl).isNotNull(); + reachabilityMetadata = IOUtils.toString(reachabilityMetadataUrl, StandardCharsets.UTF_8); } static Stream containsSpecificEntries() { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java index 078d917aa7c..5a02af2d5e5 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java @@ -20,11 +20,6 @@ import aQute.bnd.annotation.spi.ServiceProvider; import java.io.IOException; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.Writer; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -51,7 +46,6 @@ import javax.lang.model.util.SimpleElementVisitor8; import javax.lang.model.util.SimpleTypeVisitor8; import javax.tools.Diagnostic; -import javax.tools.FileObject; import javax.tools.StandardLocation; import org.apache.logging.log4j.core.config.plugins.processor.internal.Annotations; import org.apache.logging.log4j.core.config.plugins.processor.internal.ReachabilityMetadata; @@ -81,6 +75,10 @@ @SupportedOptions({"log4j.graalvm.groupId", "log4j.graalvm.artifactId"}) public class GraalVmProcessor extends AbstractProcessor { + private static final String GROUP_ID = "log4j.graalvm.groupId"; + private static final String ARTIFACT_ID = "log4j.graalvm.artifactId"; + private static final String PROCESSOR_NAME = GraalVmProcessor.class.getName(); + private final Map reachableTypes = new HashMap<>(); private final List processedElements = new ArrayList<>(); private Annotations annotationUtil; @@ -118,8 +116,8 @@ public boolean process(Set annotations, RoundEnvironment case UNKNOWN: messager.printMessage( Diagnostic.Kind.WARNING, - "The annotation type `" + annotation + "` is not handled by " - + GraalVmProcessor.class.getSimpleName(), + String.format( + "The annotation type `%s` is not handled by %s", annotation, PROCESSOR_NAME), annotation); } processedElements.add(element); @@ -127,40 +125,7 @@ public boolean process(Set annotations, RoundEnvironment } // Write the result file if (roundEnv.processingOver() && !reachableTypes.isEmpty()) { - // - // Many users will have `log4j-core` on the annotation processor path, but do not have Log4j Plugins. - // Therefore, we check for the annotation processor required options only if some elements were processed. - // - String groupId = processingEnv.getOptions().get("log4j.graalvm.groupId"); - String artifactId = processingEnv.getOptions().get("log4j.graalvm.artifactId"); - if (groupId == null || artifactId == null) { - messager.printMessage( - Diagnostic.Kind.ERROR, - "The `" + GraalVmProcessor.class.getName() - + "` annotation processor is missing the required `maven.groupId` and `maven.artifactId` options.\n" - + "The generation of GraalVM reflection metadata for your Log4j Plugins will be disabled."); - return false; - } - String reachabilityMetadataPath = - String.format("META-INF/native-image/%s/%s/reflect-config.json", groupId, artifactId); - try { - messager.printMessage( - Diagnostic.Kind.NOTE, - String.format( - "%s: writing GraalVM metadata for %d Java classes to `%s`.", - GraalVmProcessor.class.getSimpleName(), - reachableTypes.size(), - reachabilityMetadataPath)); - writeReachabilityMetadata(reachabilityMetadataPath, processedElements.toArray(new Element[0])); - } catch (IOException e) { - StringWriter sw = new StringWriter(); - sw.append(GraalVmProcessor.class.getSimpleName()) - .append(": unable to write reachability metadata to file ") - .append(reachabilityMetadataPath) - .append("\n"); - e.printStackTrace(new PrintWriter(sw)); - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, sw.toString()); - } + writeReachabilityMetadata(); } // Do not claim the annotations to allow other annotation processors to run return false; @@ -168,25 +133,23 @@ public boolean process(Set annotations, RoundEnvironment private void processPlugin(Element element) { TypeElement typeElement = safeCast(element, TypeElement.class); - if (typeElement != null) { - for (Element child : typeElement.getEnclosedElements()) { - if (child instanceof ExecutableElement) { - ExecutableElement executableChild = (ExecutableElement) child; - if (executableChild.getModifiers().contains(Modifier.PUBLIC)) { - switch (executableChild.getSimpleName().toString()) { - // 1. All public constructors. - case "": + for (Element child : typeElement.getEnclosedElements()) { + if (child instanceof ExecutableElement) { + ExecutableElement executableChild = (ExecutableElement) child; + if (executableChild.getModifiers().contains(Modifier.PUBLIC)) { + switch (executableChild.getSimpleName().toString()) { + // 1. All public constructors. + case "": + 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; - // 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: - } + } + break; + // 3. Other factory methods are annotated, so we don't deal with them here. + default: } } } @@ -201,42 +164,68 @@ private void processConstraintOrVisitor(Element element, TypeElement annotation) private void processParameter(Element element) { switch (element.getKind()) { case FIELD: - { - VariableElement field = safeCast(element, VariableElement.class); - TypeElement typeElement = safeCast(element.getEnclosingElement(), TypeElement.class); - if (typeElement != null && field != null) { - addField(typeElement, field); - } - } + addField( + safeCast(element.getEnclosingElement(), TypeElement.class), + safeCast(element, VariableElement.class)); break; case PARAMETER: // Do nothing, the containing method must be annotated with a factory annotation. break; default: - processingEnv - .getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Invalid Log4j Attribute element.", element); + String msg = String.format("Invalid Log4j parameter element `%s`.", element); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element); + throw new IllegalStateException(msg); } } private void processFactory(Element element) { - ExecutableElement method = safeCast(element, ExecutableElement.class); - TypeElement typeElement = safeCast(element.getEnclosingElement(), TypeElement.class); - if (typeElement != null && method != null) { - addMethod(typeElement, method); + addMethod( + safeCast(element.getEnclosingElement(), TypeElement.class), safeCast(element, ExecutableElement.class)); + } + + private void writeReachabilityMetadata() { + // + // Many users will have `log4j-core` on the annotation processor path, but do not have Log4j Plugins. + // Therefore, we check for the annotation processor required options only if some elements were processed. + // + Messager messager = processingEnv.getMessager(); + String reachabilityMetadataPath = getReachabilityMetadataPath(); + try { + 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(new Element[0])) + .openOutputStream()) { + ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), output); + } + } 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); } } - private void writeReachabilityMetadata(String reachabilityMetadataPath, Element... elements) throws IOException { - FileObject resource = processingEnv - .getFiler() - .createResource(StandardLocation.CLASS_OUTPUT, Strings.EMPTY, reachabilityMetadataPath, elements); - try (OutputStream os = resource.openOutputStream(); - Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { - ReachabilityMetadata.Reflection reflection = new ReachabilityMetadata.Reflection(); - reachableTypes.values().forEach(reflection::addType); - ReachabilityMetadata.writeReflectConfig(reflection, writer); + private String getReachabilityMetadataPath() { + String groupId = processingEnv.getOptions().get(GROUP_ID); + String artifactId = processingEnv.getOptions().get(ARTIFACT_ID); + if (groupId == null || artifactId == null) { + String message = String.format( + "The `%s` annotation processor is missing the required `%s` and `%s` options.%n" + + "The generation of GraalVM reflection metadata for your Log4j Plugins will be disabled.", + PROCESSOR_NAME, GROUP_ID, ARTIFACT_ID); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message); + throw new IllegalArgumentException(message); } + return String.format("META-INF/native-image/%s/%s/reflect-config.json", groupId, artifactId); } private void addField(TypeElement parent, VariableElement element) { @@ -255,18 +244,16 @@ private void addMethod(TypeElement parent, ExecutableElement element) { reachableType.addMethod(method); } - private @Nullable T safeCast(Element element, Class type) { + private T safeCast(Element element, Class type) { if (type.isInstance(element)) { return type.cast(element); } - processingEnv - .getMessager() - .printMessage( - Diagnostic.Kind.ERROR, - "Unexpected type of element `" + element + "`: expecting `" + type.getName() + "` but found `" - + element.getClass().getName() + "`", - element); - return null; + // 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); } /** diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java index a9cec9287d2..1efc60191d2 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java @@ -17,6 +17,9 @@ package org.apache.logging.log4j.core.config.plugins.processor.internal; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; @@ -281,8 +284,8 @@ public static class Reflection { private final Collection types = new TreeSet<>(Comparator.comparing(Type::getType)); - public void addType(Type type) { - types.add(type); + public Reflection(Collection types) { + this.types.addAll(types); } void toJson(MinimalJsonWriter jsonWriter) throws IOException { @@ -302,11 +305,12 @@ void toJson(MinimalJsonWriter jsonWriter) throws IOException { /** * Writes the contents of a {@code reflect-config.json} file. * - * @param reflection The reflection metadata. + * @param types The reflection metadata for types. * @param output The object to use as output. */ - public static void writeReflectConfig(Reflection reflection, Appendable output) throws IOException { - MinimalJsonWriter jsonWriter = new MinimalJsonWriter(output); + public static void writeReflectConfig(Collection types, OutputStream output) throws IOException { + Reflection reflection = new Reflection(types); + MinimalJsonWriter jsonWriter = new MinimalJsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)); reflection.toJson(jsonWriter); } From 0537fae39ec73722f79381ab4a680ac3b2e5a62b Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 16:58:07 +0200 Subject: [PATCH 4/6] Simplify `ReachabilityMetadata` Simplifies `ReachabilityMetadata` and fixes unclosed resource. --- .../plugins/processor/GraalVmProcessor.java | 34 +++++------ .../internal/ReachabilityMetadata.java | 58 +++++-------------- 2 files changed, 31 insertions(+), 61 deletions(-) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java index 5a02af2d5e5..18b29beec01 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java @@ -77,7 +77,7 @@ public class GraalVmProcessor extends AbstractProcessor { private static final String GROUP_ID = "log4j.graalvm.groupId"; private static final String ARTIFACT_ID = "log4j.graalvm.artifactId"; - private static final String PROCESSOR_NAME = GraalVmProcessor.class.getName(); + private static final String PROCESSOR_NAME = GraalVmProcessor.class.getSimpleName(); private final Map reachableTypes = new HashMap<>(); private final List processedElements = new ArrayList<>(); @@ -188,24 +188,22 @@ private void writeReachabilityMetadata() { // Many users will have `log4j-core` on the annotation processor path, but do not have Log4j Plugins. // Therefore, we check for the annotation processor required options only if some elements were processed. // - Messager messager = processingEnv.getMessager(); String reachabilityMetadataPath = getReachabilityMetadataPath(); - try { - 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(new Element[0])) - .openOutputStream()) { - ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), output); - } + 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(new Element[0])) + .openOutputStream()) { + ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), output); } catch (IOException e) { String message = String.format( "%s: unable to write reachability metadata to file `%s`", PROCESSOR_NAME, reachabilityMetadataPath); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java index 1efc60191d2..43596361f5d 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java @@ -19,14 +19,15 @@ 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.BitSet; 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.core.util.JsonUtils; import org.jspecify.annotations.NullMarked; /** @@ -61,31 +62,7 @@ public final class ReachabilityMetadata { */ public static final String METHODS = "methods"; - private static class MinimalJsonWriter { - - private static final int ESCAPED_CHARS_LENGHT = 128; - - /** - * BitSet of 7-bit ASCII characters that would require quoting in JSON. - */ - private static final BitSet ESCAPED_CHARS; - - static { - BitSet requiresLookup = new BitSet(ESCAPED_CHARS_LENGHT); - // Control chars need generic escape sequence - for (int i = 0; i < 32; ++i) { - requiresLookup.set(i); - } - // Others (and some within that range too) have explicit shorter sequences - requiresLookup.set('"'); - requiresLookup.set('\\'); - requiresLookup.set(0x08); - requiresLookup.set(0x09); - requiresLookup.set(0x0C); - requiresLookup.set(0x0A); - requiresLookup.set(0x0D); - ESCAPED_CHARS = requiresLookup; - } + private static final class MinimalJsonWriter { private final Appendable output; @@ -93,18 +70,11 @@ public MinimalJsonWriter(Appendable output) { this.output = output; } - /** - * Writes a JSON String ignoring characters that need quoting. - */ public void writeString(CharSequence input) throws IOException { output.append('"'); - int limit = input.length(); - for (int i = 0; i < limit; ++i) { - char c = input.charAt(i); - if (c >= ESCAPED_CHARS_LENGHT || !ESCAPED_CHARS.get(c)) { - output.append(c); - } - } + StringBuilder sb = new StringBuilder(); + JsonUtils.quoteAsString(input, sb); + output.append(sb); output.append('"'); } @@ -137,7 +107,7 @@ public void writeArrayEnd() throws IOException { /** * Specifies a field that needs to be accessed through reflection. */ - public static class Field implements Comparable { + public static final class Field implements Comparable { private final String name; @@ -165,7 +135,7 @@ public int compareTo(Field other) { /** * Specifies a method that needs to be accessed through reflection. */ - public static class Method implements Comparable { + public static final class Method implements Comparable { private final String name; private final List parameterTypes = new ArrayList<>(); @@ -221,7 +191,7 @@ public int compareTo(Method other) { /** * Specifies a Java type that needs to be accessed through reflection. */ - public static class Type { + public static final class Type { private final String type; private final Collection methods = new TreeSet<>(); @@ -280,7 +250,7 @@ void toJson(MinimalJsonWriter jsonWriter) throws IOException { /** * Collection of reflection metadata. */ - public static class Reflection { + public static final class Reflection { private final Collection types = new TreeSet<>(Comparator.comparing(Type::getType)); @@ -309,9 +279,11 @@ void toJson(MinimalJsonWriter jsonWriter) throws IOException { * @param output The object to use as output. */ public static void writeReflectConfig(Collection types, OutputStream output) throws IOException { - Reflection reflection = new Reflection(types); - MinimalJsonWriter jsonWriter = new MinimalJsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)); - reflection.toJson(jsonWriter); + try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) { + Reflection reflection = new Reflection(types); + MinimalJsonWriter jsonWriter = new MinimalJsonWriter(writer); + reflection.toJson(jsonWriter); + } } private ReachabilityMetadata() {} From 79909eab2699b9f5032c2465a1341f2b175e01aa Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 17:05:24 +0200 Subject: [PATCH 5/6] Add `resource-config.json` to `log4j-api` Add a GraalVM metadata file to include the following 3 files into an executable: * `log4j2.component.properties`, * `log4j2.simplelog.properties`, * `log4j2.StatusLogger.properties` --- .../log4j-api/resource-config.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 log4j-api/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-api/resource-config.json diff --git a/log4j-api/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-api/resource-config.json b/log4j-api/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-api/resource-config.json new file mode 100644 index 00000000000..1649e6be1ef --- /dev/null +++ b/log4j-api/src/main/resources/META-INF/native-image/org.apache.logging.log4j/log4j-api/resource-config.json @@ -0,0 +1,9 @@ +{ + "resources": { + "includes": [ + { + "pattern": "log4j2\\.(component|simplelog|StatusLogger)\\.properties" + } + ] + } +} \ No newline at end of file From d25e0e6f632fa47f36afaadb7e2af0e4452560bc Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 3 Oct 2024 17:17:47 +0200 Subject: [PATCH 6/6] Move `json-unit` to `log4j-core-test` --- log4j-core-test/pom.xml | 3 +++ log4j-parent/pom.xml | 13 ------------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml index 30ed7854e97..1895bc4bd7f 100644 --- a/log4j-core-test/pom.xml +++ b/log4j-core-test/pom.xml @@ -63,6 +63,7 @@ 4.0.0 + 2.40.1 @@ -292,12 +293,14 @@ net.javacrumbs.json-unit json-unit-assertj + ${json-unit.version} test net.javacrumbs.json-unit json-unit + ${json-unit.version} test diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index 3c7ab46c958..5e7e249ee54 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -108,7 +108,6 @@ 0.6.0 3.5.12 1.37 - 2.40.1 4.13.2 5.10.3 1.9.1 @@ -576,18 +575,6 @@ ${jna.version} - - net.javacrumbs.json-unit - json-unit-assertj - ${json-unit.version} - - - - net.javacrumbs.json-unit - json-unit - ${json-unit.version} - - junit junit