2525import java .io .OutputStreamWriter ;
2626import java .io .PrintWriter ;
2727import java .io .StringWriter ;
28- import java .util .ArrayList ;
29- import java .util .Collection ;
30- import java .util .Collections ;
28+ import java .util .Arrays ;
29+ import java .util .HashSet ;
3130import java .util .List ;
3231import java .util .Locale ;
33- import java .util .Map ;
3432import java .util .Objects ;
3533import java .util .Optional ;
3634import java .util .Set ;
3735import javax .annotation .processing .AbstractProcessor ;
3836import javax .annotation .processing .Messager ;
37+ import javax .annotation .processing .ProcessingEnvironment ;
3938import javax .annotation .processing .Processor ;
4039import javax .annotation .processing .RoundEnvironment ;
4140import javax .annotation .processing .SupportedAnnotationTypes ;
4241import javax .lang .model .SourceVersion ;
4342import javax .lang .model .element .AnnotationValue ;
44- import javax .lang .model .element .Element ;
45- import javax .lang .model .element .Name ;
4643import javax .lang .model .element .TypeElement ;
47- import javax .lang .model .util .SimpleElementVisitor8 ;
44+ import javax .lang .model .util .ElementFilter ;
45+ import javax .lang .model .util .Elements ;
4846import javax .tools .Diagnostic .Kind ;
4947import javax .tools .FileObject ;
5048import javax .tools .JavaFileObject ;
5149import javax .tools .StandardLocation ;
5250import org .apache .logging .log4j .LoggingException ;
5351import org .apache .logging .log4j .plugins .Configurable ;
5452import org .apache .logging .log4j .plugins .Namespace ;
55- import org .apache .logging .log4j .plugins .Node ;
5653import org .apache .logging .log4j .plugins .Plugin ;
5754import org .apache .logging .log4j .plugins .PluginAliases ;
5855import org .apache .logging .log4j .plugins .model .PluginEntry ;
56+ import org .apache .logging .log4j .plugins .model .PluginIndex ;
5957import org .apache .logging .log4j .util .Strings ;
6058import org .jspecify .annotations .NullMarked ;
6159
7472 * </p>
7573 */
7674@ NullMarked
77- @ SupportedAnnotationTypes ({ "org.apache.logging.log4j.plugins.*" , "org.apache.logging.log4j.core.config.plugins.*" } )
75+ @ SupportedAnnotationTypes ("org.apache.logging.log4j.plugins.Plugin" )
7876@ ServiceProvider (value = Processor .class , resolution = Resolution .OPTIONAL )
7977public class PluginProcessor extends AbstractProcessor {
8078
@@ -98,7 +96,9 @@ public class PluginProcessor extends AbstractProcessor {
9896 "META-INF/services/org.apache.logging.log4j.plugins.model.PluginService" ;
9997
10098 private boolean enableBndAnnotations ;
101- private String packageName = "" ;
99+ private CharSequence packageName = "" ;
100+ private final PluginIndex pluginIndex = new PluginIndex ();
101+ private final Set <TypeElement > processedElements = new HashSet <>();
102102
103103 public PluginProcessor () {}
104104
@@ -112,89 +112,124 @@ public SourceVersion getSupportedSourceVersion() {
112112 return SourceVersion .latest ();
113113 }
114114
115+ @ Override
116+ public synchronized void init (ProcessingEnvironment processingEnv ) {
117+ super .init (processingEnv );
118+ handleOptions ();
119+ }
120+
115121 @ Override
116122 public boolean process (final Set <? extends TypeElement > annotations , final RoundEnvironment roundEnv ) {
117- handleOptions (processingEnv .getOptions ());
118- final Messager messager = processingEnv .getMessager ();
119- messager .printMessage (Kind .NOTE , "Processing Log4j annotations" );
120- try {
121- final Set <? extends Element > elements = roundEnv .getElementsAnnotatedWith (Plugin .class );
122- if (elements .isEmpty ()) {
123- messager .printMessage (Kind .NOTE , "No elements to process" );
124- return true ;
123+ // Process the elements for this round
124+ if (!annotations .isEmpty ()) {
125+ processPluginAnnotatedClasses (ElementFilter .typesIn (roundEnv .getElementsAnnotatedWith (Plugin .class )));
126+ }
127+ // Write the generated code
128+ if (roundEnv .processingOver () && !pluginIndex .isEmpty ()) {
129+ try {
130+ final Messager messager = processingEnv .getMessager ();
131+ messager .printMessage (Kind .NOTE , "Writing Log4j plugin metadata using base package " + packageName );
132+ writeClassFile ();
133+ writeServiceFile ();
134+ messager .printMessage (Kind .NOTE , "Log4j annotations processed" );
135+ } catch (final Exception e ) {
136+ handleUnexpectedError (e );
125137 }
126- messager .printMessage (Kind .NOTE , "Retrieved " + elements .size () + " Plugin elements" );
127- final List <PluginEntry > list = new ArrayList <>();
128- packageName = collectPlugins (packageName , elements , list );
129- messager .printMessage (Kind .NOTE , "Writing plugin metadata using base package " + packageName );
130- Collections .sort (list );
131- writeClassFile (packageName , list );
132- writeServiceFile (packageName );
133- messager .printMessage (Kind .NOTE , "Annotations processed" );
134- } catch (final Exception ex ) {
135- var writer = new StringWriter ();
136- ex .printStackTrace (new PrintWriter (writer ));
137- error (writer .toString ());
138138 }
139+ // Do not claim the annotations to allow other annotation processors to run
139140 return false ;
140141 }
141142
142- private void error (final CharSequence message ) {
143- processingEnv .getMessager ().printMessage (Kind .ERROR , message );
143+ private void processPluginAnnotatedClasses (Set <TypeElement > pluginClasses ) {
144+ final boolean calculatePackageName = packageName .isEmpty ();
145+ final Elements elements = processingEnv .getElementUtils ();
146+ final Messager messager = processingEnv .getMessager ();
147+ for (var pluginClass : pluginClasses ) {
148+ final String name = getPluginName (pluginClass );
149+ final String namespace = getNamespace (pluginClass );
150+ final String className = elements .getBinaryName (pluginClass ).toString ();
151+ var builder =
152+ PluginEntry .builder ().setName (name ).setNamespace (namespace ).setClassName (className );
153+ processConfigurableAnnotation (pluginClass , builder );
154+ var entry = builder .get ();
155+ messager .printMessage (Kind .NOTE , "Parsed Log4j plugin " + entry , pluginClass );
156+ if (!pluginIndex .add (entry )) {
157+ messager .printMessage (Kind .WARNING , "Duplicate Log4j plugin parsed " + entry , pluginClass );
158+ }
159+ pluginIndex .addAll (createPluginAliases (pluginClass , builder ));
160+ if (calculatePackageName ) {
161+ packageName = calculatePackageName (elements , pluginClass , packageName );
162+ }
163+ processedElements .add (pluginClass );
164+ }
144165 }
145166
146- private String collectPlugins (
147- String packageName , final Iterable <? extends Element > elements , final List <PluginEntry > list ) {
148- final boolean calculatePackage = packageName .isEmpty ();
149- final var pluginVisitor = new PluginElementVisitor ();
150- final var pluginAliasesVisitor = new PluginAliasesElementVisitor ();
151- for (final Element element : elements ) {
152- // The elements must be annotated with `Plugin`
153- Plugin plugin = element .getAnnotation (Plugin .class );
154- final var entry = element .accept (pluginVisitor , plugin );
155- list .add (entry );
156- if (calculatePackage ) {
157- packageName = calculatePackage (element , packageName );
158- }
159- list .addAll (element .accept (pluginAliasesVisitor , plugin ));
167+ private static void processConfigurableAnnotation (TypeElement pluginClass , PluginEntry .Builder builder ) {
168+ var configurable = pluginClass .getAnnotation (Configurable .class );
169+ if (configurable != null ) {
170+ var elementType = configurable .elementType ();
171+ builder .setElementType (elementType .isEmpty () ? builder .getName () : elementType )
172+ .setDeferChildren (configurable .deferChildren ())
173+ .setPrintable (configurable .printObject ());
160174 }
161- return packageName ;
162175 }
163176
164- private String calculatePackage (Element element , String packageName ) {
165- final Name name = processingEnv .getElementUtils ().getPackageOf (element ).getQualifiedName ();
166- if (name .isEmpty ()) {
167- return "" ;
177+ private static List <PluginEntry > createPluginAliases (TypeElement pluginClass , PluginEntry .Builder builder ) {
178+ return Optional .ofNullable (pluginClass .getAnnotation (PluginAliases .class )).map (PluginAliases ::value ).stream ()
179+ .flatMap (Arrays ::stream )
180+ .map (alias -> alias .toLowerCase (Locale .ROOT ))
181+ .map (key -> builder .setKey (key ).get ())
182+ .toList ();
183+ }
184+
185+ private void handleUnexpectedError (final Exception e ) {
186+ var writer = new StringWriter ();
187+ e .printStackTrace (new PrintWriter (writer ));
188+ processingEnv
189+ .getMessager ()
190+ .printMessage (Kind .ERROR , "Unexpected error processing Log4j annotations: " + writer );
191+ }
192+
193+ private static CharSequence calculatePackageName (
194+ Elements elements , TypeElement typeElement , CharSequence packageName ) {
195+ var qualifiedName = elements .getPackageOf (typeElement ).getQualifiedName ();
196+ if (qualifiedName .isEmpty ()) {
197+ return packageName ;
168198 }
169- final String pkgName = name .toString ();
170199 if (packageName .isEmpty ()) {
171- return pkgName ;
200+ return qualifiedName ;
172201 }
173- if (pkgName .length () == packageName .length ()) {
202+ int packageLength = packageName .length ();
203+ int qualifiedLength = qualifiedName .length ();
204+ if (packageLength == qualifiedLength ) {
174205 return packageName ;
175206 }
176- if (pkgName .length () < packageName .length () && packageName .startsWith (pkgName )) {
177- return pkgName ;
207+ if (qualifiedLength < packageLength
208+ && qualifiedName .contentEquals (packageName .subSequence (0 , qualifiedLength ))) {
209+ return qualifiedName ;
178210 }
179-
180- return commonPrefix (pkgName , packageName );
211+ return commonPrefix (qualifiedName , packageName );
181212 }
182213
183- private void writeServiceFile (final String pkgName ) throws IOException {
214+ private void writeServiceFile () throws IOException {
184215 final FileObject fileObject = processingEnv
185216 .getFiler ()
186- .createResource (StandardLocation .CLASS_OUTPUT , Strings .EMPTY , SERVICE_FILE_NAME );
217+ .createResource (
218+ StandardLocation .CLASS_OUTPUT ,
219+ Strings .EMPTY ,
220+ SERVICE_FILE_NAME ,
221+ processedElements .toArray (TypeElement []::new ));
187222 try (final PrintWriter writer =
188223 new PrintWriter (new BufferedWriter (new OutputStreamWriter (fileObject .openOutputStream (), UTF_8 )))) {
189224 writer .println ("# Generated by " + PluginProcessor .class .getName ());
190- writer .println (createFqcn (pkgName ));
225+ writer .println (createFqcn (packageName ));
191226 }
192227 }
193228
194- private void writeClassFile (final String pkg , final List < PluginEntry > list ) {
195- final String fqcn = createFqcn (pkg );
229+ private void writeClassFile () {
230+ final String fqcn = createFqcn (packageName );
196231 try (final PrintWriter writer = createSourceFile (fqcn )) {
197- writer .println ("package " + pkg + ".plugins;" );
232+ writer .println ("package " + packageName + ".plugins;" );
198233 writer .println ("" );
199234 if (enableBndAnnotations ) {
200235 writer .println ("import aQute.bnd.annotation.Resolution;" );
@@ -209,9 +244,9 @@ private void writeClassFile(final String pkg, final List<PluginEntry> list) {
209244 writer .println ("public class Log4jPlugins extends PluginService {" );
210245 writer .println ("" );
211246 writer .println (" private static final PluginEntry[] ENTRIES = new PluginEntry[] {" );
212- final int max = list .size () - 1 ;
213- for ( int i = 0 ; i < list . size (); ++ i ) {
214- final PluginEntry entry = list . get ( i );
247+ final int max = pluginIndex .size () - 1 ;
248+ int current = 0 ;
249+ for ( final PluginEntry entry : pluginIndex ) {
215250 writer .println (" PluginEntry.builder()" );
216251 writer .println (String .format (" .setKey(\" %s\" )" , entry .key ()));
217252 writer .println (String .format (" .setClassName(\" %s\" )" , entry .className ()));
@@ -227,7 +262,8 @@ private void writeClassFile(final String pkg, final List<PluginEntry> list) {
227262 if (entry .deferChildren ()) {
228263 writer .println (" .setDeferChildren(true)" );
229264 }
230- writer .println (" .get()" + (i < max ? "," : Strings .EMPTY ));
265+ writer .println (" .get()" + (current < max ? "," : Strings .EMPTY ));
266+ current ++;
231267 }
232268 writer .println (" };" );
233269 writer .println (" @Override" );
@@ -238,17 +274,25 @@ private void writeClassFile(final String pkg, final List<PluginEntry> list) {
238274
239275 private PrintWriter createSourceFile (final String fqcn ) {
240276 try {
241- final JavaFileObject sourceFile = processingEnv .getFiler ().createSourceFile (fqcn );
277+ final JavaFileObject sourceFile =
278+ processingEnv .getFiler ().createSourceFile (fqcn , processedElements .toArray (TypeElement []::new ));
242279 return new PrintWriter (sourceFile .openWriter ());
243280 } catch (IOException e ) {
244281 throw new LoggingException ("Unable to create Plugin Service Class " + fqcn , e );
245282 }
246283 }
247284
248- private String createFqcn (String packageName ) {
285+ private String createFqcn (CharSequence packageName ) {
249286 return packageName + ".plugins.Log4jPlugins" ;
250287 }
251288
289+ private static String getPluginName (TypeElement pluginClass ) {
290+ return Optional .ofNullable (pluginClass .getAnnotation (Plugin .class ))
291+ .map (Plugin ::value )
292+ .filter (s -> !s .isEmpty ())
293+ .orElseGet (() -> pluginClass .getSimpleName ().toString ());
294+ }
295+
252296 private static String getNamespace (final TypeElement e ) {
253297 return Optional .ofNullable (e .getAnnotation (Namespace .class ))
254298 .map (Namespace ::value )
@@ -267,60 +311,27 @@ private static String getNamespace(final TypeElement e) {
267311 .orElse (Plugin .EMPTY ));
268312 }
269313
270- private static PluginEntry configureNamespace (final TypeElement e , final PluginEntry .Builder builder ) {
271- final Configurable configurable = e .getAnnotation (Configurable .class );
272- if (configurable != null ) {
273- builder .setNamespace (Node .CORE_NAMESPACE )
274- .setElementType (
275- configurable .elementType ().isEmpty () ? builder .getName () : configurable .elementType ())
276- .setDeferChildren (configurable .deferChildren ())
277- .setPrintable (configurable .printObject ());
278- } else {
279- builder .setNamespace (getNamespace (e ));
280- }
281- return builder .get ();
282- }
283-
284- /**
285- * ElementVisitor to scan the Plugin annotation.
286- */
287- private final class PluginElementVisitor extends SimpleElementVisitor8 <PluginEntry , Plugin > {
288- @ Override
289- public PluginEntry visitType (final TypeElement e , final Plugin plugin ) {
290- Objects .requireNonNull (plugin , "Plugin annotation is null." );
291- String name = plugin .value ();
292- if (name .isEmpty ()) {
293- name = e .getSimpleName ().toString ();
294- }
295- final PluginEntry .Builder builder = PluginEntry .builder ()
296- .setKey (name .toLowerCase (Locale .ROOT ))
297- .setName (name )
298- .setClassName (
299- processingEnv .getElementUtils ().getBinaryName (e ).toString ());
300- return configureNamespace (e , builder );
301- }
302- }
303-
304- private String commonPrefix (final String str1 , final String str2 ) {
314+ private static CharSequence commonPrefix (final CharSequence str1 , final CharSequence str2 ) {
305315 final int minLength = Math .min (str1 .length (), str2 .length ());
306316 for (int i = 0 ; i < minLength ; i ++) {
307317 if (str1 .charAt (i ) != str2 .charAt (i )) {
308318 if (i > 1 && str1 .charAt (i - 1 ) == '.' ) {
309- return str1 .substring (0 , i - 1 );
319+ return str1 .subSequence (0 , i - 1 );
310320 } else {
311- return str1 .substring (0 , i );
321+ return str1 .subSequence (0 , i );
312322 }
313323 }
314324 }
315- return str1 .substring (0 , minLength );
325+ return str1 .subSequence (0 , minLength );
316326 }
317327
318328 private boolean isServiceConsumerClassPresent () {
319329 // Looks for the presence of the annotation on the classpath, not the annotation processor path.
320330 return processingEnv .getElementUtils ().getTypeElement ("aQute.bnd.annotation.spi.ServiceConsumer" ) != null ;
321331 }
322332
323- private void handleOptions (Map <String , String > options ) {
333+ private void handleOptions () {
334+ var options = processingEnv .getOptions ();
324335 packageName = options .getOrDefault (PLUGIN_PACKAGE , "" );
325336 String enableBndAnnotationsOption = options .get (ENABLE_BND_ANNOTATIONS );
326337 if (enableBndAnnotationsOption != null ) {
@@ -329,38 +340,4 @@ private void handleOptions(Map<String, String> options) {
329340 this .enableBndAnnotations = isServiceConsumerClassPresent ();
330341 }
331342 }
332-
333- /**
334- * ElementVisitor to scan the PluginAliases annotation.
335- */
336- private final class PluginAliasesElementVisitor extends SimpleElementVisitor8 <Collection <PluginEntry >, Plugin > {
337-
338- private PluginAliasesElementVisitor () {
339- super (List .of ());
340- }
341-
342- @ Override
343- public Collection <PluginEntry > visitType (final TypeElement e , final Plugin plugin ) {
344- final PluginAliases aliases = e .getAnnotation (PluginAliases .class );
345- if (aliases == null ) {
346- return DEFAULT_VALUE ;
347- }
348- String name = plugin .value ();
349- if (name .isEmpty ()) {
350- name = e .getSimpleName ().toString ();
351- }
352- final PluginEntry .Builder builder = PluginEntry .builder ()
353- .setName (name )
354- .setClassName (
355- processingEnv .getElementUtils ().getBinaryName (e ).toString ());
356- configureNamespace (e , builder );
357- final Collection <PluginEntry > entries = new ArrayList <>(aliases .value ().length );
358- for (final String alias : aliases .value ()) {
359- final PluginEntry entry =
360- builder .setKey (alias .toLowerCase (Locale .ROOT )).get ();
361- entries .add (entry );
362- }
363- return entries ;
364- }
365- }
366343}
0 commit comments