1616 */
1717package org .apache .logging .log4j .core .config .plugins .processor ;
1818
19+ import static java .nio .charset .StandardCharsets .UTF_8 ;
1920import static java .util .Arrays .asList ;
2021import static java .util .Collections .emptyList ;
2122import static java .util .Collections .singletonList ;
23+ import static java .util .Objects .requireNonNull ;
2224import static net .javacrumbs .jsonunit .assertj .JsonAssertions .assertThatJson ;
2325import static net .javacrumbs .jsonunit .assertj .JsonAssertions .json ;
2426import static org .assertj .core .api .Assertions .assertThat ;
27+ import static org .mockito .Mockito .when ;
2528
29+ import java .io .File ;
2630import java .io .IOException ;
2731import 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 ;
2936import java .util .Collections ;
3037import java .util .LinkedHashMap ;
38+ import java .util .List ;
39+ import java .util .Locale ;
3140import java .util .Map ;
41+ import java .util .stream .Collectors ;
3242import 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 ;
3454import 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 ;
3558import org .junit .jupiter .params .ParameterizedTest ;
3659import org .junit .jupiter .params .provider .Arguments ;
3760import org .junit .jupiter .params .provider .MethodSource ;
61+ import org .mockito .Mockito ;
3862
3963class 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}
0 commit comments