Skip to content

Commit 50b1e54

Browse files
author
Jay Katariya
committed
Fix(apache#2769): modify the annotation processor in 2.x to fail if a plugin builder attribute does not have a public setter.
1 parent 9299639 commit 50b1e54

File tree

8 files changed

+226
-0
lines changed

8 files changed

+226
-0
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde
6262
@PluginBuilderAttribute
6363
private int attribute;
6464

65+
@PluginBuilderAttribute
66+
@SuppressWarnings("log4j.public.setter")
67+
private int attributeWithoutPublicSetterButWithSuppressAnnotation;
68+
6569
@PluginElement("layout")
6670
private Layout<? extends Serializable> layout;
6771

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

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class GraalVmProcessorTest {
6565
"fields",
6666
asList(
6767
asMap("name", "attribute"),
68+
asMap("name", "attributeWithoutPublicSetterButWithSuppressAnnotation"),
6869
asMap("name", "config"),
6970
asMap("name", "layout"),
7071
asMap("name", "loggerContext"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 org.apache.logging.log4j.core.config.plugins.processor;
18+
19+
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.util.Arrays;
26+
import java.util.List;
27+
import java.util.Locale;
28+
import java.util.stream.Collectors;
29+
import javax.tools.Diagnostic;
30+
import javax.tools.DiagnosticCollector;
31+
import javax.tools.JavaCompiler;
32+
import javax.tools.JavaFileObject;
33+
import javax.tools.StandardJavaFileManager;
34+
import javax.tools.ToolProvider;
35+
import org.junit.Test;
36+
37+
public class PluginProcessorPublicSetterTest {
38+
39+
private static final String FAKE_PLUGIN_CLASS_PATH =
40+
"src/test/java/org/apache/logging/log4j/core/config/plugins/processor/" + FakePlugin.class.getSimpleName()
41+
+ ".java";
42+
43+
@Test
44+
public void warnWhenPluginBuilderAttributeLacksPublicSetter() {
45+
// Instantiate the tooling
46+
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
47+
final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
48+
final StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, Locale.ROOT, UTF_8);
49+
50+
// Get the source files
51+
Path sourceFile = Paths.get(FAKE_PLUGIN_CLASS_PATH);
52+
53+
assertThat(Files.exists(sourceFile)).isTrue();
54+
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(sourceFile.toFile());
55+
56+
JavaCompiler.CompilationTask task = compiler.getTask(
57+
null,
58+
fileManager,
59+
diagnosticCollector,
60+
Arrays.asList("-proc:only", "-processor", PluginProcessor.class.getName()),
61+
null,
62+
compilationUnits);
63+
task.call();
64+
65+
List<Diagnostic<? extends JavaFileObject>> warningDiagnostics = diagnosticCollector.getDiagnostics().stream()
66+
.filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.WARNING)
67+
.collect(Collectors.toList());
68+
69+
assertThat(warningDiagnostics).anyMatch(warningMessage -> warningMessage
70+
.getMessage(Locale.ROOT)
71+
.contains("The field `attribute` does not have a public setter"));
72+
}
73+
74+
@Test
75+
public void IgnoreWarningWhenSuppressWarningsIsPresent() {
76+
// Instantiate the tooling
77+
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
78+
final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
79+
final StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, Locale.ROOT, UTF_8);
80+
81+
// Get the source files
82+
Path sourceFile = Paths.get(FAKE_PLUGIN_CLASS_PATH);
83+
System.out.println(FAKE_PLUGIN_CLASS_PATH);
84+
assertThat(Files.exists(sourceFile)).isTrue();
85+
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(sourceFile.toFile());
86+
87+
JavaCompiler.CompilationTask task = compiler.getTask(
88+
null,
89+
fileManager,
90+
diagnosticCollector,
91+
Arrays.asList("-proc:only", "-processor", PluginProcessor.class.getName()),
92+
null,
93+
compilationUnits);
94+
task.call();
95+
96+
List<Diagnostic<? extends JavaFileObject>> warningDiagnostics = diagnosticCollector.getDiagnostics().stream()
97+
.filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.WARNING)
98+
.collect(Collectors.toList());
99+
100+
assertThat(warningDiagnostics)
101+
.allMatch(
102+
warningMessage -> !warningMessage
103+
.getMessage(Locale.ROOT)
104+
.contains(
105+
"The field `attributeWithoutPublicSetterButWithSuppressAnnotation` does not have a public setter"));
106+
}
107+
}

log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static class Builder<B extends Builder<B>> extends AbstractDatabaseAppend
6060
private ConnectionSource connectionSource;
6161

6262
@PluginBuilderAttribute
63+
@SuppressWarnings("log4j.public.setter")
6364
private boolean immediateFail;
6465

6566
@PluginBuilderAttribute
@@ -80,6 +81,7 @@ public static class Builder<B extends Builder<B>> extends AbstractDatabaseAppend
8081

8182
// TODO Consider moving up to AbstractDatabaseAppender.Builder.
8283
@PluginBuilderAttribute
84+
@SuppressWarnings("log4j.public.setter")
8385
private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS;
8486

8587
@Override

log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java

+100
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
import java.io.PrintWriter;
2626
import java.io.StringWriter;
2727
import java.util.ArrayList;
28+
import java.util.Arrays;
2829
import java.util.Collection;
2930
import java.util.Collections;
3031
import java.util.List;
32+
import java.util.Locale;
3133
import java.util.Map;
3234
import java.util.Objects;
3335
import java.util.Set;
@@ -39,14 +41,19 @@
3941
import javax.lang.model.SourceVersion;
4042
import javax.lang.model.element.Element;
4143
import javax.lang.model.element.ElementVisitor;
44+
import javax.lang.model.element.ExecutableElement;
45+
import javax.lang.model.element.Modifier;
4246
import javax.lang.model.element.TypeElement;
47+
import javax.lang.model.element.VariableElement;
4348
import javax.lang.model.util.Elements;
4449
import javax.lang.model.util.SimpleElementVisitor7;
50+
import javax.lang.model.util.Types;
4551
import javax.tools.Diagnostic;
4652
import javax.tools.FileObject;
4753
import javax.tools.StandardLocation;
4854
import org.apache.logging.log4j.core.config.plugins.Plugin;
4955
import org.apache.logging.log4j.core.config.plugins.PluginAliases;
56+
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
5057
import org.apache.logging.log4j.util.Strings;
5158

5259
/**
@@ -83,6 +90,12 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
8390
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
8491
collectPlugins(elements);
8592
processedElements.addAll(elements);
93+
94+
// process plugin builder Attributes
95+
final Set<? extends Element> pluginAttributeBuilderElements =
96+
roundEnv.getElementsAnnotatedWith(PluginBuilderAttribute.class);
97+
processBuilderAttributes(pluginAttributeBuilderElements);
98+
processedElements.addAll(pluginAttributeBuilderElements);
8699
}
87100
// Write the cache file
88101
if (roundEnv.processingOver() && !processedElements.isEmpty()) {
@@ -107,6 +120,93 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
107120
return false;
108121
}
109122

123+
private void processBuilderAttributes(final Iterable<? extends Element> elements) {
124+
for (final Element element : elements) {
125+
if (element instanceof VariableElement) {
126+
processBuilderAttributes((VariableElement) element);
127+
}
128+
}
129+
}
130+
131+
private void processBuilderAttributes(final VariableElement element) {
132+
final String fieldName = element.getSimpleName().toString(); // Getting the name of the field
133+
SuppressWarnings suppress = element.getAnnotation(SuppressWarnings.class);
134+
if (suppress != null && Arrays.asList(suppress.value()).contains("log4j.public.setter")) {
135+
// Suppress the warning due to annotation
136+
return;
137+
}
138+
final Element enclosingElement = element.getEnclosingElement();
139+
// `element is a field
140+
if (enclosingElement instanceof TypeElement) {
141+
final TypeElement typeElement = (TypeElement) enclosingElement;
142+
// Check the siblings of the field
143+
for (final Element enclosedElement : typeElement.getEnclosedElements()) {
144+
// `enclosedElement is a method or constructor
145+
if (enclosedElement instanceof ExecutableElement) {
146+
final ExecutableElement methodElement = (ExecutableElement) enclosedElement;
147+
final String methodName = methodElement.getSimpleName().toString();
148+
149+
if ((methodName.toLowerCase(Locale.ROOT).startsWith("set") // Typical pattern for setters
150+
|| methodName
151+
.toLowerCase(Locale.ROOT)
152+
.startsWith("with")) // Typical pattern for setters
153+
&& methodElement.getParameters().size()
154+
== 1 // It is a weird pattern to not have public setter
155+
) {
156+
157+
Types typeUtils = processingEnv.getTypeUtils();
158+
159+
boolean followsNamePattern = methodName
160+
.toLowerCase(Locale.ROOT)
161+
.equals(String.format("set%s", fieldName.toLowerCase(Locale.ROOT)))
162+
|| methodName
163+
.toLowerCase(Locale.ROOT)
164+
.equals(String.format("with%s", fieldName.toLowerCase(Locale.ROOT)));
165+
166+
if (fieldName.toLowerCase(Locale.ROOT).startsWith("is")) {
167+
followsNamePattern = methodName
168+
.toLowerCase(Locale.ROOT)
169+
.equals(String.format(
170+
"set%s",
171+
fieldName
172+
.toLowerCase(Locale.ROOT)
173+
.substring(2)))
174+
|| methodName
175+
.toLowerCase(Locale.ROOT)
176+
.equals(String.format(
177+
"with%s",
178+
fieldName
179+
.toLowerCase(Locale.ROOT)
180+
.substring(2)));
181+
}
182+
183+
// Check if method is public
184+
boolean isPublicMethod = methodElement.getModifiers().contains(Modifier.PUBLIC);
185+
186+
// Check if the return type of the method element is Assignable.
187+
// Assuming it is a builder class the type of it should be assignable to its parent
188+
boolean checkForAssignable = typeUtils.isAssignable(
189+
methodElement.getReturnType(),
190+
methodElement.getEnclosingElement().asType());
191+
192+
boolean foundPublicSetter = followsNamePattern && checkForAssignable && isPublicMethod;
193+
if (foundPublicSetter) {
194+
// Hurray we found a public setter for the field!
195+
return;
196+
}
197+
}
198+
}
199+
}
200+
// If the setter was not found generate a compiler warning.
201+
processingEnv
202+
.getMessager()
203+
.printMessage(
204+
Diagnostic.Kind.WARNING,
205+
String.format("The field `%s` does not have a public setter", fieldName),
206+
element);
207+
}
208+
}
209+
110210
private void collectPlugins(final Iterable<? extends Element> elements) {
111211
final Elements elementUtils = processingEnv.getElementUtils();
112212
final ElementVisitor<PluginEntry, Plugin> pluginVisitor = new PluginElementVisitor(elementUtils);

log4j-core/src/main/java/org/apache/logging/log4j/core/net/SocketPerformancePreferences.java

+3
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@ public static SocketPerformancePreferences newBuilder() {
4040

4141
@PluginBuilderAttribute
4242
@Required
43+
@SuppressWarnings("log4j.public.setter")
4344
private int bandwidth;
4445

4546
@PluginBuilderAttribute
4647
@Required
48+
@SuppressWarnings("log4j.public.setter")
4749
private int connectionTime;
4850

4951
@PluginBuilderAttribute
5052
@Required
53+
@SuppressWarnings("log4j.public.setter")
5154
private int latency;
5255

5356
public void apply(final Socket socket) {

log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/appender/ServletAppender.java

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static class Builder<B extends Builder<B>> extends AbstractAppender.Build
4040
implements org.apache.logging.log4j.core.util.Builder<ServletAppender> {
4141

4242
@PluginBuilderAttribute
43+
@SuppressWarnings("log4j.public.setter")
4344
private boolean logThrowables;
4445

4546
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="2769" link="https://github.com/apache/logging-log4j2/issues/2769"/>
7+
<description format="asciidoc">Adding a compilation warning for Plugin Builder Attributes that do not have a public setter.</description>
8+
</entry>

0 commit comments

Comments
 (0)