Skip to content

Commit f93b6b1

Browse files
ppkarwaszCopilot
andauthored
fix: Make GraalVmProcessor Arguments Optional (#3772)
Fixes #3771 This PR makes the `-Alog4j.graalvm.groupId` and `-Alog4j.graalvm.artifactId` arguments optional. * If **no arguments** are provided, metadata is stored in: ``` META-INF/native-image/log4j-generated/<content-derived-value> ``` Previously an error was thrown. * If **arguments are provided**, files go to: ``` META-INF/native-image/log4j-generated/<groupId>/<artifactId> ``` Previously `META-INF/native-image/<groupId>/<artifactId>` was used. The new path prevents collisions with user-provided metadata. Co-authored-by: Copilot <[email protected]>
1 parent 32d29ae commit f93b6b1

File tree

5 files changed

+379
-50
lines changed

5 files changed

+379
-50
lines changed

log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/GraalVmProcessorTest.java

Lines changed: 154 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,56 @@
1616
*/
1717
package org.apache.logging.log4j.core.config.plugins.processor;
1818

19+
import static java.nio.charset.StandardCharsets.UTF_8;
1920
import static java.util.Arrays.asList;
2021
import static java.util.Collections.emptyList;
2122
import static java.util.Collections.singletonList;
23+
import static java.util.Objects.requireNonNull;
2224
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
2325
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
2426
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.mockito.Mockito.when;
2528

29+
import java.io.File;
2630
import java.io.IOException;
2731
import java.net.URL;
28-
import java.nio.charset.StandardCharsets;
32+
import java.nio.file.Files;
33+
import java.nio.file.Path;
34+
import java.nio.file.Paths;
35+
import java.util.ArrayList;
2936
import java.util.Collections;
3037
import java.util.LinkedHashMap;
38+
import java.util.List;
39+
import java.util.Locale;
3140
import java.util.Map;
41+
import java.util.stream.Collectors;
3242
import java.util.stream.Stream;
33-
import org.apache.commons.io.IOUtils;
43+
import javax.annotation.processing.Messager;
44+
import javax.annotation.processing.ProcessingEnvironment;
45+
import javax.lang.model.util.Elements;
46+
import javax.tools.Diagnostic;
47+
import javax.tools.DiagnosticCollector;
48+
import javax.tools.JavaCompiler;
49+
import javax.tools.JavaFileObject;
50+
import javax.tools.StandardJavaFileManager;
51+
import javax.tools.StandardLocation;
52+
import javax.tools.ToolProvider;
53+
import org.jspecify.annotations.Nullable;
3454
import org.junit.jupiter.api.BeforeAll;
55+
import org.junit.jupiter.api.Test;
56+
import org.junit.jupiter.api.io.CleanupMode;
57+
import org.junit.jupiter.api.io.TempDir;
3558
import org.junit.jupiter.params.ParameterizedTest;
3659
import org.junit.jupiter.params.provider.Arguments;
3760
import org.junit.jupiter.params.provider.MethodSource;
61+
import org.mockito.Mockito;
3862

3963
class GraalVmProcessorTest {
4064

65+
private static final String FAKE_PLUGIN_NAME = "example.FakePlugin";
4166
private static final Object FAKE_PLUGIN = asMap(
4267
"name",
43-
FakePlugin.class.getName(),
68+
FAKE_PLUGIN_NAME,
4469
"methods",
4570
asList(
4671
asMap("name", "<init>", "parameterTypes", emptyList()),
@@ -57,9 +82,10 @@ class GraalVmProcessorTest {
5782
"java.lang.String"))),
5883
"fields",
5984
emptyList());
85+
private static final String FAKE_PLUGIN_BUILDER_NAME = FAKE_PLUGIN_NAME + "$Builder";
6086
private static final Object FAKE_PLUGIN_BUILDER = asMap(
6187
"name",
62-
FakePlugin.Builder.class.getName(),
88+
FAKE_PLUGIN_BUILDER_NAME,
6389
"methods",
6490
emptyList(),
6591
"fields",
@@ -71,21 +97,27 @@ class GraalVmProcessorTest {
7197
asMap("name", "loggerContext"),
7298
asMap("name", "node"),
7399
asMap("name", "value")));
74-
private static final Object FAKE_PLUGIN_NESTED = onlyNoArgsConstructor(FakePlugin.Nested.class);
75-
private static final Object FAKE_CONSTRAINT_VALIDATOR =
76-
onlyNoArgsConstructor(FakeAnnotations.FakeConstraintValidator.class);
77-
private static final Object FAKE_PLUGIN_VISITOR = onlyNoArgsConstructor(FakeAnnotations.FakePluginVisitor.class);
100+
private static final String FAKE_PLUGIN_NESTED_NAME = FAKE_PLUGIN_NAME + "$Nested";
101+
private static final Object FAKE_PLUGIN_NESTED = onlyNoArgsConstructor(FAKE_PLUGIN_NESTED_NAME);
102+
private static final String FAKE_CONSTRAINT_VALIDATOR_NAME = "example.FakeAnnotations$FakeConstraintValidator";
103+
private static final Object FAKE_CONSTRAINT_VALIDATOR = onlyNoArgsConstructor(FAKE_CONSTRAINT_VALIDATOR_NAME);
104+
private static final String FAKE_PLUGIN_VISITOR_NAME = "example.FakeAnnotations$FakePluginVisitor";
105+
private static final Object FAKE_PLUGIN_VISITOR = onlyNoArgsConstructor(FAKE_PLUGIN_VISITOR_NAME);
106+
107+
private static final String GROUP_ID = "groupId";
108+
private static final String ARTIFACT_ID = "artifactId";
109+
private static final String FALLBACK_METADATA_FOLDER = "fooBar";
78110

79111
/**
80112
* Generates a metadata element with just a single no-arg constructor.
81113
*
82-
* @param clazz The name of the metadata element.
114+
* @param className The name of the metadata element.
83115
* @return A GraalVM metadata element.
84116
*/
85-
private static Object onlyNoArgsConstructor(Class<?> clazz) {
117+
private static Object onlyNoArgsConstructor(String className) {
86118
return asMap(
87119
"name",
88-
clazz.getName(),
120+
className,
89121
"methods",
90122
singletonList(asMap("name", "<init>", "parameterTypes", emptyList())),
91123
"fields",
@@ -103,43 +135,131 @@ private static Object onlyNoArgsConstructor(Class<?> clazz) {
103135
return map;
104136
}
105137

106-
private static String reachabilityMetadata;
138+
private static Path sourceDir;
139+
140+
@TempDir
141+
private static Path outputDir;
107142

108143
@BeforeAll
109-
static void setup() throws IOException {
110-
// There are two descriptors, choose the one in `test-classes`
111-
URL reachabilityMetadataUrl = null;
112-
for (URL url : Collections.list(GraalVmProcessor.class
113-
.getClassLoader()
114-
.getResources("META-INF/native-image/org.apache.logging.log4j/log4j-core-test/reflect-config.json"))) {
115-
if (url.getPath().contains("test-classes")) {
116-
reachabilityMetadataUrl = url;
117-
break;
118-
}
119-
}
120-
assertThat(reachabilityMetadataUrl).isNotNull();
121-
reachabilityMetadata = IOUtils.toString(reachabilityMetadataUrl, StandardCharsets.UTF_8);
144+
static void setup() throws Exception {
145+
URL sourceUrl = requireNonNull(GraalVmProcessorTest.class.getResource("/GraalVmProcessorTest/java"));
146+
sourceDir = Paths.get(sourceUrl.toURI());
147+
// Generate metadata
148+
List<String> diagnostics = generateDescriptor(sourceDir, GROUP_ID, ARTIFACT_ID, outputDir);
149+
assertThat(diagnostics).isEmpty();
122150
}
123151

124152
static Stream<Arguments> containsSpecificEntries() {
125153
return Stream.of(
126-
Arguments.of(FakePlugin.class, FAKE_PLUGIN),
127-
Arguments.of(FakePlugin.Builder.class, FAKE_PLUGIN_BUILDER),
128-
Arguments.of(FakePlugin.Nested.class, FAKE_PLUGIN_NESTED),
129-
Arguments.of(FakeAnnotations.FakeConstraintValidator.class, FAKE_CONSTRAINT_VALIDATOR),
130-
Arguments.of(FakeAnnotations.FakePluginVisitor.class, FAKE_PLUGIN_VISITOR));
154+
Arguments.of(FAKE_PLUGIN_NAME, FAKE_PLUGIN),
155+
Arguments.of(FAKE_PLUGIN_BUILDER_NAME, FAKE_PLUGIN_BUILDER),
156+
Arguments.of(FAKE_PLUGIN_NESTED_NAME, FAKE_PLUGIN_NESTED),
157+
Arguments.of(FAKE_CONSTRAINT_VALIDATOR_NAME, FAKE_CONSTRAINT_VALIDATOR),
158+
Arguments.of(FAKE_PLUGIN_VISITOR_NAME, FAKE_PLUGIN_VISITOR));
131159
}
132160

133161
@ParameterizedTest
134162
@MethodSource
135-
void containsSpecificEntries(Class<?> clazz, Object expectedJson) {
163+
void containsSpecificEntries(String className, Object expectedJson) throws IOException {
164+
// Read metadata
165+
Path reachabilityMetadataPath =
166+
outputDir.resolve("META-INF/native-image/log4j-generated/groupId/artifactId/reflect-config.json");
167+
String reachabilityMetadata = new String(Files.readAllBytes(reachabilityMetadataPath), UTF_8);
136168
assertThatJson(reachabilityMetadata)
137-
.inPath(filterByName(clazz))
169+
.inPath(String.format("$[?(@.name == '%s')]", className))
138170
.isArray()
139171
.contains(json(expectedJson));
140172
}
141173

142-
private String filterByName(Class<?> clazz) {
143-
return String.format("$[?(@.name == '%s')]", clazz.getName());
174+
static Stream<Arguments> reachabilityMetadataPath() {
175+
return Stream.of(
176+
Arguments.of(
177+
"groupId",
178+
"artifactId",
179+
"META-INF/native-image/log4j-generated/groupId/artifactId/reflect-config.json"),
180+
Arguments.of(null, "artifactId", "META-INF/native-image/log4j-generated/fooBar/reflect-config.json"),
181+
Arguments.of("groupId", null, "META-INF/native-image/log4j-generated/fooBar/reflect-config.json"),
182+
Arguments.of(null, null, "META-INF/native-image/log4j-generated/fooBar/reflect-config.json"));
183+
}
184+
185+
@ParameterizedTest
186+
@MethodSource
187+
void reachabilityMetadataPath(@Nullable String groupId, @Nullable String artifactId, String expected) {
188+
Messager messager = Mockito.mock(Messager.class);
189+
Elements elements = Mockito.mock(Elements.class);
190+
ProcessingEnvironment processingEnv = Mockito.mock(ProcessingEnvironment.class);
191+
when(processingEnv.getMessager()).thenReturn(messager);
192+
when(processingEnv.getElementUtils()).thenReturn(elements);
193+
GraalVmProcessor processor = new GraalVmProcessor();
194+
processor.init(processingEnv);
195+
assertThat(processor.getReachabilityMetadataPath(groupId, artifactId, FALLBACK_METADATA_FOLDER))
196+
.isEqualTo(expected);
197+
}
198+
199+
@Test
200+
void whenNoGroupIdAndArtifactId_thenWarningIsPrinted(@TempDir(cleanup = CleanupMode.NEVER) Path outputDir)
201+
throws Exception {
202+
List<String> diagnostics = generateDescriptor(sourceDir, null, null, outputDir);
203+
assertThat(diagnostics).hasSize(1);
204+
// The warning message should contain the information about the missing groupId and artifactId arguments
205+
assertThat(diagnostics.get(0))
206+
.contains(
207+
"recommended",
208+
"-A" + GraalVmProcessor.GROUP_ID + "=<groupId>",
209+
"-A" + GraalVmProcessor.ARTIFACT_ID + "=<artifactId>");
210+
Path path = outputDir.resolve("META-INF/native-image/log4j-generated");
211+
List<Path> reachabilityMetadataFolders;
212+
try (Stream<Path> files = Files.list(path)) {
213+
reachabilityMetadataFolders = files.filter(Files::isDirectory).collect(Collectors.toList());
214+
}
215+
// The generated folder name should be deterministic and based solely on the descriptor content.
216+
// If the descriptor changes, this test and the expected folder name must be updated accordingly.
217+
assertThat(reachabilityMetadataFolders).hasSize(1).containsExactly(path.resolve("62162090"));
218+
assertThat(reachabilityMetadataFolders.get(0).resolve("reflect-config.json"))
219+
.as("Reachability metadata file")
220+
.exists();
221+
}
222+
223+
private static List<String> generateDescriptor(
224+
Path sourceDir, @Nullable String groupId, @Nullable String artifactId, Path outputDir) throws Exception {
225+
// Instantiate the tooling
226+
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
227+
final StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, Locale.ROOT, UTF_8);
228+
229+
// Populate sources
230+
final Iterable<? extends JavaFileObject> sources;
231+
try (final Stream<Path> files = Files.walk(sourceDir)) {
232+
File[] sourceFiles =
233+
files.filter(Files::isRegularFile).map(Path::toFile).toArray(File[]::new);
234+
sources = fileManager.getJavaFileObjects(sourceFiles);
235+
}
236+
237+
// Set the target path used by `DescriptorGenerator` to dump the generated files
238+
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(outputDir.toFile()));
239+
240+
// Prepare the compiler options
241+
List<String> options = new ArrayList<>();
242+
options.add("-proc:only");
243+
options.add("-processor");
244+
options.add(GraalVmProcessor.class.getName());
245+
if (groupId != null) {
246+
options.add("-A" + GraalVmProcessor.GROUP_ID + "=" + groupId);
247+
}
248+
if (artifactId != null) {
249+
options.add("-A" + GraalVmProcessor.ARTIFACT_ID + "=" + artifactId);
250+
}
251+
252+
// Compile the sources
253+
final Path descriptorFilePath = outputDir.resolve("plugins.xml");
254+
final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
255+
final JavaCompiler.CompilationTask task =
256+
compiler.getTask(null, fileManager, diagnosticCollector, options, null, sources);
257+
task.call();
258+
259+
// Verify successful compilation
260+
return diagnosticCollector.getDiagnostics().stream()
261+
.filter(d -> d.getKind() != Diagnostic.Kind.NOTE)
262+
.map(d -> d.getMessage(Locale.ROOT))
263+
.collect(Collectors.toList());
144264
}
145265
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package example;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Member;
21+
import org.apache.logging.log4j.core.LogEvent;
22+
import org.apache.logging.log4j.core.config.Configuration;
23+
import org.apache.logging.log4j.core.config.Node;
24+
import org.apache.logging.log4j.core.config.plugins.PluginVisitorStrategy;
25+
import org.apache.logging.log4j.core.config.plugins.validation.Constraint;
26+
import org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidator;
27+
import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
28+
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
29+
30+
/**
31+
* Fake constraint and plugin visitor that are accessed through reflection.
32+
*/
33+
public class FakeAnnotations {
34+
35+
@Constraint(FakeConstraintValidator.class)
36+
public @interface FakeConstraint {}
37+
38+
public static class FakeConstraintValidator implements ConstraintValidator<FakeConstraint> {
39+
@Override
40+
public void initialize(FakeConstraint annotation) {}
41+
42+
@Override
43+
public boolean isValid(String name, Object value) {
44+
return false;
45+
}
46+
}
47+
48+
@PluginVisitorStrategy(FakePluginVisitor.class)
49+
public @interface FakeAnnotation {}
50+
51+
public static class FakePluginVisitor implements PluginVisitor<FakeAnnotation> {
52+
53+
@Override
54+
public PluginVisitor<FakeAnnotation> setAnnotation(Annotation annotation) {
55+
return null;
56+
}
57+
58+
@Override
59+
public PluginVisitor<FakeAnnotation> setAliases(String... aliases) {
60+
return null;
61+
}
62+
63+
@Override
64+
public PluginVisitor<FakeAnnotation> setStrSubstitutor(StrSubstitutor substitutor) {
65+
return null;
66+
}
67+
68+
@Override
69+
public PluginVisitor<FakeAnnotation> setMember(Member member) {
70+
return null;
71+
}
72+
73+
@Override
74+
public Object visit(Configuration configuration, Node node, LogEvent event, StringBuilder log) {
75+
return null;
76+
}
77+
78+
@Override
79+
public PluginVisitor<FakeAnnotation> setConversionType(Class<?> conversionType) {
80+
return null;
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)