25
25
import java .io .PrintWriter ;
26
26
import java .io .StringWriter ;
27
27
import java .util .ArrayList ;
28
+ import java .util .Arrays ;
28
29
import java .util .Collection ;
29
30
import java .util .Collections ;
30
31
import java .util .List ;
32
+ import java .util .Locale ;
31
33
import java .util .Map ;
32
34
import java .util .Objects ;
33
35
import java .util .Set ;
39
41
import javax .lang .model .SourceVersion ;
40
42
import javax .lang .model .element .Element ;
41
43
import javax .lang .model .element .ElementVisitor ;
44
+ import javax .lang .model .element .ExecutableElement ;
45
+ import javax .lang .model .element .Modifier ;
42
46
import javax .lang .model .element .TypeElement ;
47
+ import javax .lang .model .element .VariableElement ;
43
48
import javax .lang .model .util .Elements ;
44
49
import javax .lang .model .util .SimpleElementVisitor7 ;
50
+ import javax .lang .model .util .Types ;
45
51
import javax .tools .Diagnostic ;
46
52
import javax .tools .FileObject ;
47
53
import javax .tools .StandardLocation ;
54
+ import org .apache .commons .lang3 .StringUtils ;
48
55
import org .apache .logging .log4j .core .config .plugins .Plugin ;
49
56
import org .apache .logging .log4j .core .config .plugins .PluginAliases ;
57
+ import org .apache .logging .log4j .core .config .plugins .PluginBuilderAttribute ;
50
58
import org .apache .logging .log4j .util .Strings ;
51
59
52
60
/**
@@ -60,6 +68,8 @@ public class PluginProcessor extends AbstractProcessor {
60
68
61
69
private static final Element [] EMPTY_ELEMENT_ARRAY = {};
62
70
71
+ private static final String SUPPRESS_WARNING_PUBLIC_SETTER_STRING = "log4j.public.setter" ;
72
+
63
73
/**
64
74
* The location of the plugin cache data file. This file is written to by this processor, and read from by
65
75
* {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}.
@@ -83,6 +93,12 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
83
93
final Set <? extends Element > elements = roundEnv .getElementsAnnotatedWith (Plugin .class );
84
94
collectPlugins (elements );
85
95
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 );
86
102
}
87
103
// Write the cache file
88
104
if (roundEnv .processingOver () && !processedElements .isEmpty ()) {
@@ -107,6 +123,93 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
107
123
return false ;
108
124
}
109
125
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
+
110
213
private void collectPlugins (final Iterable <? extends Element > elements ) {
111
214
final Elements elementUtils = processingEnv .getElementUtils ();
112
215
final ElementVisitor <PluginEntry , Plugin > pluginVisitor = new PluginElementVisitor (elementUtils );
0 commit comments