Skip to content

Commit 945a314

Browse files
PLUGINAPI-42 Introduce HttpFilter class and deprecate ServletFilter which is using javax.servlet.*
1 parent afdd169 commit 945a314

File tree

6 files changed

+656
-3
lines changed

6 files changed

+656
-3
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Sonar Plugin API
3+
* Copyright (C) 2009-2023 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.api.web;
21+
22+
import java.io.IOException;
23+
import org.sonar.api.server.http.HttpRequest;
24+
import org.sonar.api.server.http.HttpResponse;
25+
26+
/**
27+
* Filters use the FilterChain to invoke the next filter in the chain, or if the calling filter
28+
* is the last filter in the chain, to invoke the resource at the end of the chain.
29+
*
30+
* @see HttpFilter
31+
* @since 9.16
32+
**/
33+
public interface FilterChain {
34+
35+
/**
36+
* Causes the next filter in the chain to be invoked, or if the calling filter is the last filter
37+
* in the chain, causes the resource at the end of the chain to be invoked.
38+
*
39+
* @param request the request to pass along the chain.
40+
* @param response the response to pass along the chain.
41+
*/
42+
void doFilter(HttpRequest request, HttpResponse response) throws IOException;
43+
44+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Sonar Plugin API
3+
* Copyright (C) 2009-2023 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.api.web;
21+
22+
import java.io.IOException;
23+
24+
import org.sonar.api.ExtensionPoint;
25+
import org.sonar.api.server.ServerSide;
26+
import org.sonar.api.server.http.HttpRequest;
27+
import org.sonar.api.server.http.HttpResponse;
28+
29+
/**
30+
* A filter is an object that performs filtering tasks on either the
31+
* request to a resource (a servlet or static content), or on the response
32+
* from a resource, or both..
33+
*
34+
* @since 9.16
35+
*/
36+
@ServerSide
37+
@ExtensionPoint
38+
public abstract class HttpFilter {
39+
40+
/**
41+
* This method is called exactly once after instantiating the filter. The init
42+
* method must complete successfully before the filter is asked to do any
43+
* filtering work.
44+
*/
45+
public void init() {
46+
}
47+
48+
49+
/**
50+
* The <code>doFilter</code> method of the Filter is called by the
51+
* SonarQube each time a request/response pair is passed through the
52+
* chain due to a client request for a resource at the end of the chain.
53+
* The FilterChain passed in to this method allows the Filter to pass
54+
* on the request and response to the next entity in the chain.
55+
*
56+
* <p>A typical implementation of this method would follow the following
57+
* pattern:
58+
* <ol>
59+
* <li>Examine the request
60+
* <li>Optionally wrap the request object with a custom implementation to
61+
* filter content or headers for input filtering
62+
* <li>Optionally wrap the response object with a custom implementation to
63+
* filter content or headers for output filtering
64+
* <li>
65+
* <ul>
66+
* <li><strong>Either</strong> invoke the next entity in the chain
67+
* using the FilterChain object
68+
* (<code>chain.doFilter()</code>),
69+
* <li><strong>or</strong> not pass on the request/response pair to
70+
* the next entity in the filter chain to
71+
* block the request processing
72+
* </ul>
73+
* <li>Directly set headers on the response after invocation of the
74+
* next entity in the filter chain.
75+
* </ol>
76+
*/
77+
public abstract void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws IOException;
78+
79+
80+
/**
81+
* Called by the SonarQube to indicate to a filter that it is being
82+
* taken out of service.
83+
*
84+
* <p>This method is only called once all threads within the filter's
85+
* doFilter method have exited or after a timeout period has passed.
86+
* After the web container calls this method, it will not call the
87+
* doFilter method again on this instance of the filter.
88+
*
89+
* <p>This method gives the filter an opportunity to clean up any
90+
* resources that are being held (for example, memory, file handles,
91+
* threads) and make sure that any persistent state is synchronized
92+
* with the filter's current state in memory.
93+
*/
94+
public void destroy() {
95+
}
96+
97+
/**
98+
* Override to change URL. Default is /*
99+
*/
100+
public UrlPattern doGetPattern() {
101+
return UrlPattern.builder().build();
102+
}
103+
}

plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.util.Set;
2929
import java.util.function.Predicate;
3030
import java.util.stream.Collectors;
31-
import javax.servlet.Filter;
3231
import org.sonar.api.ExtensionPoint;
3332
import org.sonar.api.server.ServerSide;
3433

@@ -38,11 +37,13 @@
3837
import static org.sonar.api.utils.Preconditions.checkArgument;
3938

4039
/**
40+
* {@code @deprecated} since 9.16. Use {@link org.sonar.api.web.HttpFilter} instead.
4141
* @since 3.1
4242
*/
4343
@ServerSide
4444
@ExtensionPoint
45-
public abstract class ServletFilter implements Filter {
45+
@Deprecated(forRemoval = true, since = "9.16")
46+
public abstract class ServletFilter implements javax.servlet.Filter {
4647

4748
/**
4849
* Override to change URL. Default is /*
@@ -65,7 +66,7 @@ private UrlPattern(Builder builder) {
6566
this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions));
6667
if (builder.inclusionPredicates.isEmpty()) {
6768
// because Stream#anyMatch() returns false if stream is empty
68-
this.inclusionPredicates = new Predicate[] {s -> true};
69+
this.inclusionPredicates = new Predicate[]{s -> true};
6970
} else {
7071
this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new);
7172
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Sonar Plugin API
3+
* Copyright (C) 2009-2023 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.api.web;
21+
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.Collection;
25+
import java.util.HashSet;
26+
import java.util.LinkedHashSet;
27+
import java.util.List;
28+
import java.util.Set;
29+
import java.util.function.Predicate;
30+
import java.util.stream.Collectors;
31+
32+
import static java.util.Arrays.asList;
33+
import static java.util.Collections.unmodifiableList;
34+
import static org.apache.commons.lang.StringUtils.substringBeforeLast;
35+
import static org.sonar.api.utils.Preconditions.checkArgument;
36+
37+
public final class UrlPattern {
38+
39+
private static final String MATCH_ALL = "/*";
40+
41+
private final List<String> inclusions;
42+
private final List<String> exclusions;
43+
private final Predicate<String>[] inclusionPredicates;
44+
private final Predicate<String>[] exclusionPredicates;
45+
46+
private UrlPattern(Builder builder) {
47+
this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions));
48+
this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions));
49+
if (builder.inclusionPredicates.isEmpty()) {
50+
// because Stream#anyMatch() returns false if stream is empty
51+
this.inclusionPredicates = new Predicate[]{s -> true};
52+
} else {
53+
this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new);
54+
}
55+
this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new);
56+
}
57+
58+
public boolean matches(String path) {
59+
return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) &&
60+
Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path));
61+
}
62+
63+
/**
64+
* @since 6.0
65+
*/
66+
public Collection<String> getInclusions() {
67+
return inclusions;
68+
}
69+
70+
/**
71+
* @since 6.0
72+
*/
73+
public Collection<String> getExclusions() {
74+
return exclusions;
75+
}
76+
77+
public String label() {
78+
return "UrlPattern{" +
79+
"inclusions=[" + convertPatternsToString(inclusions) + "]" +
80+
", exclusions=[" + convertPatternsToString(exclusions) + "]" +
81+
'}';
82+
}
83+
84+
private static String convertPatternsToString(List<String> input) {
85+
StringBuilder output = new StringBuilder();
86+
if (input.isEmpty()) {
87+
return "";
88+
}
89+
if (input.size() == 1) {
90+
return output.append(input.get(0)).toString();
91+
}
92+
return output.append(input.get(0)).append(", ...").toString();
93+
}
94+
95+
/**
96+
* Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}.
97+
*/
98+
public static UrlPattern create(String inclusionPattern) {
99+
return builder().includes(inclusionPattern).build();
100+
}
101+
102+
/**
103+
* @since 6.0
104+
*/
105+
public static Builder builder() {
106+
return new Builder();
107+
}
108+
109+
/**
110+
* @since 6.0
111+
*/
112+
public static class Builder {
113+
private static final String WILDCARD_CHAR = "*";
114+
private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList(
115+
"*.css", "*.css.map", "*.ico", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.js", "*.js.map", "*.pdf", "/json/*", "*.woff2",
116+
"/static/*", "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"));
117+
118+
private final Set<String> inclusions = new LinkedHashSet<>();
119+
private final Set<String> exclusions = new LinkedHashSet<>();
120+
private final Set<Predicate<String>> inclusionPredicates = new HashSet<>();
121+
private final Set<Predicate<String>> exclusionPredicates = new HashSet<>();
122+
123+
private Builder() {
124+
}
125+
126+
public static Collection<String> staticResourcePatterns() {
127+
return STATIC_RESOURCES;
128+
}
129+
130+
/**
131+
* Add inclusion patterns. Supported formats are:
132+
* <ul>
133+
* <li>path prefixed by / and ended by * or /*, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li>
134+
* <li>path prefixed by / and ended by .*, for example "/api/foo.*", to match exact path "/api/foo" with any suffix like "/api/foo.protobuf"</li>
135+
* <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li>
136+
* <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li>
137+
* </ul>
138+
*/
139+
public Builder includes(String... includePatterns) {
140+
return includes(asList(includePatterns));
141+
}
142+
143+
/**
144+
* Add exclusion patterns. See format described in {@link #includes(String...)}
145+
*/
146+
public Builder includes(Collection<String> includePatterns) {
147+
this.inclusions.addAll(includePatterns);
148+
this.inclusionPredicates.addAll(includePatterns.stream()
149+
.filter(pattern -> !MATCH_ALL.equals(pattern))
150+
.map(Builder::compile)
151+
.collect(Collectors.toList()));
152+
return this;
153+
}
154+
155+
public Builder excludes(String... excludePatterns) {
156+
return excludes(asList(excludePatterns));
157+
}
158+
159+
public Builder excludes(Collection<String> excludePatterns) {
160+
this.exclusions.addAll(excludePatterns);
161+
this.exclusionPredicates.addAll(excludePatterns.stream()
162+
.map(Builder::compile)
163+
.collect(Collectors.toList()));
164+
return this;
165+
}
166+
167+
public UrlPattern build() {
168+
return new UrlPattern(this);
169+
}
170+
171+
private static Predicate<String> compile(String pattern) {
172+
int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length();
173+
if (countStars == 0) {
174+
checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern);
175+
return url -> url.equals(pattern);
176+
}
177+
checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern);
178+
if (pattern.charAt(0) == '/') {
179+
checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern);
180+
if (pattern.endsWith("/*")) {
181+
String path = pattern.substring(0, pattern.length() - "/*".length());
182+
return url -> url.startsWith(path);
183+
}
184+
if (pattern.endsWith(".*")) {
185+
String path = pattern.substring(0, pattern.length() - ".*".length());
186+
return url -> substringBeforeLast(url, ".").equals(path);
187+
}
188+
String path = pattern.substring(0, pattern.length() - "*".length());
189+
return url -> url.startsWith(path);
190+
}
191+
checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern);
192+
// remove the leading *
193+
String path = pattern.substring(1);
194+
return url -> url.endsWith(path);
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)