2525import java .io .PrintWriter ;
2626import java .io .StringWriter ;
2727import java .util .ArrayList ;
28+ import java .util .Arrays ;
2829import java .util .Collection ;
2930import java .util .Collections ;
3031import java .util .List ;
32+ import java .util .Locale ;
3133import java .util .Map ;
3234import java .util .Objects ;
3335import java .util .Set ;
3941import javax .lang .model .SourceVersion ;
4042import javax .lang .model .element .Element ;
4143import javax .lang .model .element .ElementVisitor ;
44+ import javax .lang .model .element .ExecutableElement ;
45+ import javax .lang .model .element .Modifier ;
4246import javax .lang .model .element .TypeElement ;
47+ import javax .lang .model .element .VariableElement ;
4348import javax .lang .model .util .Elements ;
4449import javax .lang .model .util .SimpleElementVisitor7 ;
50+ import javax .lang .model .util .Types ;
4551import javax .tools .Diagnostic ;
4652import javax .tools .FileObject ;
4753import javax .tools .StandardLocation ;
54+ import org .apache .commons .lang3 .StringUtils ;
4855import org .apache .logging .log4j .core .config .plugins .Plugin ;
4956import org .apache .logging .log4j .core .config .plugins .PluginAliases ;
57+ import org .apache .logging .log4j .core .config .plugins .PluginBuilderAttribute ;
5058import org .apache .logging .log4j .util .Strings ;
5159
5260/**
@@ -60,6 +68,8 @@ public class PluginProcessor extends AbstractProcessor {
6068
6169 private static final Element [] EMPTY_ELEMENT_ARRAY = {};
6270
71+ private static final String SUPPRESS_WARNING_PUBLIC_SETTER_STRING = "log4j.public.setter" ;
72+
6373 /**
6474 * The location of the plugin cache data file. This file is written to by this processor, and read from by
6575 * {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}.
@@ -83,6 +93,12 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
8393 final Set <? extends Element > elements = roundEnv .getElementsAnnotatedWith (Plugin .class );
8494 collectPlugins (elements );
8595 processedElements .addAll (elements );
96+
97+ // process plugin builder Attributes
98+ final Set <? extends Element > pluginAttributeBuilderElements =
99+ roundEnv .getElementsAnnotatedWith (PluginBuilderAttribute .class );
100+ processBuilderAttributes (pluginAttributeBuilderElements );
101+ processedElements .addAll (pluginAttributeBuilderElements );
86102 }
87103 // Write the cache file
88104 if (roundEnv .processingOver () && !processedElements .isEmpty ()) {
@@ -107,6 +123,93 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
107123 return false ;
108124 }
109125
126+ private void processBuilderAttributes (final Iterable <? extends Element > elements ) {
127+ for (final Element element : elements ) {
128+ if (element instanceof VariableElement ) {
129+ processBuilderAttributes ((VariableElement ) element );
130+ }
131+ }
132+ }
133+
134+ private void processBuilderAttributes (final VariableElement element ) {
135+ final String fieldName = element .getSimpleName ().toString (); // Getting the name of the field
136+ SuppressWarnings suppress = element .getAnnotation (SuppressWarnings .class );
137+ if (suppress != null && Arrays .asList (suppress .value ()).contains (SUPPRESS_WARNING_PUBLIC_SETTER_STRING )) {
138+ // Suppress the warning due to annotation
139+ return ;
140+ }
141+ final Element enclosingElement = element .getEnclosingElement ();
142+ // `element is a field
143+ if (enclosingElement instanceof TypeElement ) {
144+ final TypeElement typeElement = (TypeElement ) enclosingElement ;
145+ // Check the siblings of the field
146+ for (final Element enclosedElement : typeElement .getEnclosedElements ()) {
147+ // `enclosedElement is a method or constructor
148+ if (enclosedElement instanceof ExecutableElement ) {
149+ final ExecutableElement methodElement = (ExecutableElement ) enclosedElement ;
150+ final String methodName = methodElement .getSimpleName ().toString ();
151+
152+ if ((methodName .toLowerCase (Locale .ROOT ).startsWith ("set" ) // Typical pattern for setters
153+ || methodName
154+ .toLowerCase (Locale .ROOT )
155+ .startsWith ("with" )) // Typical pattern for setters
156+ && methodElement .getParameters ().size ()
157+ == 1 // It is a weird pattern to not have public setter
158+ ) {
159+
160+ Types typeUtils = processingEnv .getTypeUtils ();
161+
162+ boolean followsNamePattern = methodName .equals (
163+ String .format ("set%s" , expectedFieldNameInASetter (fieldName )))
164+ || methodName .equals (String .format ("with%s" , expectedFieldNameInASetter (fieldName )));
165+
166+ // Check if method is public
167+ boolean isPublicMethod = methodElement .getModifiers ().contains (Modifier .PUBLIC );
168+
169+ // Check if the return type of the method element is Assignable.
170+ // Assuming it is a builder class the type of it should be assignable to its parent
171+ boolean checkForAssignable = typeUtils .isAssignable (
172+ methodElement .getReturnType (),
173+ methodElement .getEnclosingElement ().asType ());
174+
175+ boolean foundPublicSetter = followsNamePattern && checkForAssignable && isPublicMethod ;
176+ if (foundPublicSetter ) {
177+ // Hurray we found a public setter for the field!
178+ return ;
179+ }
180+ }
181+ }
182+ }
183+ // If the setter was not found generate a compiler warning.
184+ processingEnv
185+ .getMessager ()
186+ .printMessage (
187+ Diagnostic .Kind .ERROR ,
188+ String .format (
189+ "The field `%s` does not have a public setter, Note that @SuppressWarnings(\" %s\" ), can be used on the field to suppress the compilation error. " ,
190+ fieldName , SUPPRESS_WARNING_PUBLIC_SETTER_STRING ),
191+ element );
192+ }
193+ }
194+
195+ /**
196+ * Helper method to get the expected Method name in a field.
197+ * For example if the field name is 'isopen', then the expected setter would be 'setOpen' or 'withOpen'
198+ * This method is supposed to return the capitalized 'Open', fieldName which is expected in the setter.
199+ * @param fieldName who's setter we are checking.
200+ * @return The expected fieldName that will come after withxxxx or setxxxx
201+ */
202+ public static String expectedFieldNameInASetter (String fieldName ) {
203+ if (fieldName .startsWith ("is" )) fieldName = fieldName .substring (2 );
204+
205+ if (StringUtils .isEmpty (fieldName )) return "" ; // Return empty string if the field is a blank String.
206+
207+ return String .format (
208+ "%s%s" ,
209+ fieldName .substring (0 , 1 ).toUpperCase (Locale .ROOT ),
210+ fieldName .substring (1 )); // we do not need to Lowercase here examples like field name 'ignoreExceptions'
211+ }
212+
110213 private void collectPlugins (final Iterable <? extends Element > elements ) {
111214 final Elements elementUtils = processingEnv .getElementUtils ();
112215 final ElementVisitor <PluginEntry , Plugin > pluginVisitor = new PluginElementVisitor (elementUtils );
0 commit comments