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 diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml index 1c1f771c5d2..56f7fd885f3 100644 --- a/log4j-core-test/pom.xml +++ b/log4j-core-test/pom.xml @@ -63,60 +63,73 @@ 4.0.0 + 2.40.1 - + 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 +142,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,36 +220,42 @@ 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 + org.jspecify jspecify @@ -235,110 +266,138 @@ 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 + ${json-unit.version} + test + + net.javacrumbs.json-unit json-unit + ${json-unit.version} 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..2c0133835db --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java @@ -0,0 +1,144 @@ +/* + * 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 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; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.commons.io.IOUtils; +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 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; + + @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; + } + } + assertThat(reachabilityMetadataUrl).isNotNull(); + reachabilityMetadata = IOUtils.toString(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, Object 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..18b29beec01 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessor.java @@ -0,0 +1,312 @@ +/* + * 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.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.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 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.getSimpleName(); + + 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, + 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 processPlugin(Element element) { + TypeElement typeElement = safeCast(element, TypeElement.class); + 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: + 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: + 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) { + 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. + // + String reachabilityMetadataPath = getReachabilityMetadataPath(); + 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); + messager.printMessage(Diagnostic.Kind.ERROR, message); + throw new IllegalArgumentException(message, e); + } + } + + 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) { + 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 safeCast(Element element, Class 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() { + @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..43596361f5d --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/internal/ReachabilityMetadata.java @@ -0,0 +1,290 @@ +/* + * 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.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.core.util.JsonUtils; +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 final 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(); + JsonUtils.quoteAsString(input, sb); + 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(':'); + } + + 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 final 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 final 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 final 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 final class Reflection { + + private final Collection types = new TreeSet<>(Comparator.comparing(Type::getType)); + + public Reflection(Collection 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; + type.toJson(jsonWriter); + } + 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 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); + } + } + + 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 a7e0230406f..daaf5109cec 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,12 +575,6 @@ ${jna.version} - - net.javacrumbs.json-unit - json-unit - ${json-unit.version} - - junit junit @@ -1189,6 +1182,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 +1193,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