From 80ee8d358011ac0fcf54b52b0dcc3f2262fa84c7 Mon Sep 17 00:00:00 2001 From: Jacek Poreda Date: Fri, 21 Apr 2023 13:36:39 +0200 Subject: [PATCH] PLUGINAPI-42 Reduce code duplication on UrlPattern class --- .../org/sonar/api/web/AbstractUrlPattern.java | 185 ++++++++++++++++++ .../java/org/sonar/api/web/ServletFilter.java | 150 +------------- .../java/org/sonar/api/web/UrlPattern.java | 155 +-------------- 3 files changed, 199 insertions(+), 291 deletions(-) create mode 100644 plugin-api/src/main/java/org/sonar/api/web/AbstractUrlPattern.java diff --git a/plugin-api/src/main/java/org/sonar/api/web/AbstractUrlPattern.java b/plugin-api/src/main/java/org/sonar/api/web/AbstractUrlPattern.java new file mode 100644 index 00000000..a1c16977 --- /dev/null +++ b/plugin-api/src/main/java/org/sonar/api/web/AbstractUrlPattern.java @@ -0,0 +1,185 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.api.web; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; +import static org.apache.commons.lang.StringUtils.substringBeforeLast; +import static org.sonar.api.utils.Preconditions.checkArgument; + +/** + * Logic of this class should be moved to URLPattern class after deprecation period. + */ +abstract class AbstractUrlPattern { + + private static final String MATCH_ALL = "/*"; + + private final List inclusions; + private final List exclusions; + private final Predicate[] inclusionPredicates; + private final Predicate[] exclusionPredicates; + + AbstractUrlPattern(Builder builder) { + this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions)); + this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions)); + if (builder.inclusionPredicates.isEmpty()) { + // because Stream#anyMatch() returns false if stream is empty + this.inclusionPredicates = new Predicate[]{s -> true}; + } else { + this.inclusionPredicates = (Predicate[]) builder.inclusionPredicates.stream().toArray(Predicate[]::new); + } + this.exclusionPredicates = (Predicate[]) builder.exclusionPredicates.stream().toArray(Predicate[]::new); + } + + public boolean matches(String path) { + return Arrays.stream(exclusionPredicates).noneMatch(pattern -> pattern.test(path)) && + Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path)); + } + + /** + * @since 6.0 + */ + public Collection getInclusions() { + return inclusions; + } + + /** + * @since 6.0 + */ + public Collection getExclusions() { + return exclusions; + } + + public String label() { + return "UrlPattern{" + + "inclusions=[" + convertPatternsToString(inclusions) + "]" + + ", exclusions=[" + convertPatternsToString(exclusions) + "]" + + '}'; + } + + private static String convertPatternsToString(List input) { + StringBuilder output = new StringBuilder(); + if (input.isEmpty()) { + return ""; + } + if (input.size() == 1) { + return output.append(input.get(0)).toString(); + } + return output.append(input.get(0)).append(", ...").toString(); + } + + /** + * @since 6.0 + */ + public abstract static class Builder { + private static final String WILDCARD_CHAR = "*"; + static final Collection STATIC_RESOURCES = List.of("*.css", "*.css.map", "*.ico", "*.png", + "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.js", "*.js.map", "*.pdf", "/json/*", "*.woff2", "/static/*", + "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"); + + private final Set inclusions = new LinkedHashSet<>(); + private final Set exclusions = new LinkedHashSet<>(); + private final Set> inclusionPredicates = new HashSet<>(); + private final Set> exclusionPredicates = new HashSet<>(); + + Builder() { + } + + public static Collection staticResourcePatterns() { + return STATIC_RESOURCES; + } + + /** + * Add inclusion patterns. Supported formats are: + *
    + *
  • path prefixed by / and ended by * or /*, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"
  • + *
  • path prefixed by / and ended by .*, for example "/api/foo.*", to match exact path "/api/foo" with any suffix like "/api/foo.protobuf"
  • + *
  • path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"
  • + *
  • path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"
  • + *
+ */ + public B includes(String... includePatterns) { + return includes(asList(includePatterns)); + } + + /** + * Add exclusion patterns. See format described in {@link #includes(String...)} + */ + public B includes(Collection includePatterns) { + this.inclusions.addAll(includePatterns); + this.inclusionPredicates.addAll(includePatterns.stream() + .filter(pattern -> !MATCH_ALL.equals(pattern)) + .map(Builder::compile) + .collect(Collectors.toList())); + return (B) this; + } + + public B excludes(String... excludePatterns) { + return excludes(asList(excludePatterns)); + } + + public B excludes(Collection excludePatterns) { + this.exclusions.addAll(excludePatterns); + this.exclusionPredicates.addAll(excludePatterns.stream() + .map(Builder::compile) + .collect(Collectors.toList())); + return (B) this; + } + + + public abstract T build(); + + private static Predicate compile(String pattern) { + int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length(); + if (countStars == 0) { + checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern); + return url -> url.equals(pattern); + } + checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern); + if (pattern.charAt(0) == '/') { + checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern); + if (pattern.endsWith("/*")) { + String path = pattern.substring(0, pattern.length() - "/*".length()); + return url -> url.startsWith(path); + } + if (pattern.endsWith(".*")) { + String path = pattern.substring(0, pattern.length() - ".*".length()); + return url -> substringBeforeLast(url, ".").equals(path); + } + String path = pattern.substring(0, pattern.length() - "*".length()); + return url -> url.startsWith(path); + } + checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern); + // remove the leading * + String path = pattern.substring(1); + return url -> url.endsWith(path); + } + } +} diff --git a/plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java b/plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java index c5eac494..bcc627ad 100644 --- a/plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java +++ b/plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java @@ -19,25 +19,12 @@ */ package org.sonar.api.web; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; import org.sonar.api.ExtensionPoint; import org.sonar.api.server.ServerSide; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; -import static org.apache.commons.lang.StringUtils.substringBeforeLast; -import static org.sonar.api.utils.Preconditions.checkArgument; - /** * {@code @deprecated} since 9.16. Use {@link org.sonar.api.web.HttpFilter} instead. + * * @since 3.1 */ @ServerSide @@ -52,62 +39,10 @@ public UrlPattern doGetPattern() { return UrlPattern.builder().build(); } - public static final class UrlPattern { - - private static final String MATCH_ALL = "/*"; - - private final List inclusions; - private final List exclusions; - private final Predicate[] inclusionPredicates; - private final Predicate[] exclusionPredicates; + public static final class UrlPattern extends AbstractUrlPattern { private UrlPattern(Builder builder) { - this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions)); - this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions)); - if (builder.inclusionPredicates.isEmpty()) { - // because Stream#anyMatch() returns false if stream is empty - this.inclusionPredicates = new Predicate[]{s -> true}; - } else { - this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new); - } - this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new); - } - - public boolean matches(String path) { - return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) && - Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path)); - } - - /** - * @since 6.0 - */ - public Collection getInclusions() { - return inclusions; - } - - /** - * @since 6.0 - */ - public Collection getExclusions() { - return exclusions; - } - - public String label() { - return "UrlPattern{" + - "inclusions=[" + convertPatternsToString(inclusions) + "]" + - ", exclusions=[" + convertPatternsToString(exclusions) + "]" + - '}'; - } - - private static String convertPatternsToString(List input) { - StringBuilder output = new StringBuilder(); - if (input.isEmpty()) { - return ""; - } - if (input.size() == 1) { - return output.append(input.get(0)).toString(); - } - return output.append(input.get(0)).append(", ...").toString(); + super(builder); } /** @@ -127,90 +62,17 @@ public static Builder builder() { /** * @since 6.0 */ - public static class Builder { - private static final String WILDCARD_CHAR = "*"; - private static final Collection STATIC_RESOURCES = unmodifiableList(asList( - "*.css", "*.css.map", "*.ico", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.js", "*.js.map", "*.pdf", "/json/*", "*.woff2", - "/static/*", "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*")); + public static class Builder extends AbstractUrlPattern.Builder { - private final Set inclusions = new LinkedHashSet<>(); - private final Set exclusions = new LinkedHashSet<>(); - private final Set> inclusionPredicates = new HashSet<>(); - private final Set> exclusionPredicates = new HashSet<>(); private Builder() { + super(); } - public static Collection staticResourcePatterns() { - return STATIC_RESOURCES; - } - - /** - * Add inclusion patterns. Supported formats are: - *
    - *
  • path prefixed by / and ended by * or /*, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"
  • - *
  • path prefixed by / and ended by .*, for example "/api/foo.*", to match exact path "/api/foo" with any suffix like "/api/foo.protobuf"
  • - *
  • path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"
  • - *
  • path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"
  • - *
- */ - public Builder includes(String... includePatterns) { - return includes(asList(includePatterns)); - } - - /** - * Add exclusion patterns. See format described in {@link #includes(String...)} - */ - public Builder includes(Collection includePatterns) { - this.inclusions.addAll(includePatterns); - this.inclusionPredicates.addAll(includePatterns.stream() - .filter(pattern -> !MATCH_ALL.equals(pattern)) - .map(Builder::compile) - .collect(Collectors.toList())); - return this; - } - - public Builder excludes(String... excludePatterns) { - return excludes(asList(excludePatterns)); - } - - public Builder excludes(Collection excludePatterns) { - this.exclusions.addAll(excludePatterns); - this.exclusionPredicates.addAll(excludePatterns.stream() - .map(Builder::compile) - .collect(Collectors.toList())); - return this; - } - + @Override public UrlPattern build() { return new UrlPattern(this); } - - private static Predicate compile(String pattern) { - int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length(); - if (countStars == 0) { - checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern); - return url -> url.equals(pattern); - } - checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern); - if (pattern.charAt(0) == '/') { - checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern); - if (pattern.endsWith("/*")) { - String path = pattern.substring(0, pattern.length() - "/*".length()); - return url -> url.startsWith(path); - } - if (pattern.endsWith(".*")) { - String path = pattern.substring(0, pattern.length() - ".*".length()); - return url -> substringBeforeLast(url, ".").equals(path); - } - String path = pattern.substring(0, pattern.length() - "*".length()); - return url -> url.startsWith(path); - } - checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern); - // remove the leading * - String path = pattern.substring(1); - return url -> url.endsWith(path); - } } } } diff --git a/plugin-api/src/main/java/org/sonar/api/web/UrlPattern.java b/plugin-api/src/main/java/org/sonar/api/web/UrlPattern.java index 420cc4fc..51ea4673 100644 --- a/plugin-api/src/main/java/org/sonar/api/web/UrlPattern.java +++ b/plugin-api/src/main/java/org/sonar/api/web/UrlPattern.java @@ -19,77 +19,10 @@ */ package org.sonar.api.web; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; -import static org.apache.commons.lang.StringUtils.substringBeforeLast; -import static org.sonar.api.utils.Preconditions.checkArgument; - -public final class UrlPattern { - - private static final String MATCH_ALL = "/*"; - - private final List inclusions; - private final List exclusions; - private final Predicate[] inclusionPredicates; - private final Predicate[] exclusionPredicates; +public final class UrlPattern extends AbstractUrlPattern { private UrlPattern(Builder builder) { - this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions)); - this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions)); - if (builder.inclusionPredicates.isEmpty()) { - // because Stream#anyMatch() returns false if stream is empty - this.inclusionPredicates = new Predicate[]{s -> true}; - } else { - this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new); - } - this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new); - } - - public boolean matches(String path) { - return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) && - Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path)); - } - - /** - * @since 6.0 - */ - public Collection getInclusions() { - return inclusions; - } - - /** - * @since 6.0 - */ - public Collection getExclusions() { - return exclusions; - } - - public String label() { - return "UrlPattern{" + - "inclusions=[" + convertPatternsToString(inclusions) + "]" + - ", exclusions=[" + convertPatternsToString(exclusions) + "]" + - '}'; - } - - private static String convertPatternsToString(List input) { - StringBuilder output = new StringBuilder(); - if (input.isEmpty()) { - return ""; - } - if (input.size() == 1) { - return output.append(input.get(0)).toString(); - } - return output.append(input.get(0)).append(", ...").toString(); + super(builder); } /** @@ -102,96 +35,24 @@ public static UrlPattern create(String inclusionPattern) { /** * @since 6.0 */ - public static Builder builder() { - return new Builder(); + public static UrlPattern.Builder builder() { + return new UrlPattern.Builder(); } /** * @since 6.0 */ - public static class Builder { - private static final String WILDCARD_CHAR = "*"; - private static final Collection STATIC_RESOURCES = unmodifiableList(asList( - "*.css", "*.css.map", "*.ico", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.js", "*.js.map", "*.pdf", "/json/*", "*.woff2", - "/static/*", "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*")); + public static class Builder extends AbstractUrlPattern.Builder { - private final Set inclusions = new LinkedHashSet<>(); - private final Set exclusions = new LinkedHashSet<>(); - private final Set> inclusionPredicates = new HashSet<>(); - private final Set> exclusionPredicates = new HashSet<>(); private Builder() { + super(); } - public static Collection staticResourcePatterns() { - return STATIC_RESOURCES; - } - - /** - * Add inclusion patterns. Supported formats are: - *
    - *
  • path prefixed by / and ended by * or /*, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"
  • - *
  • path prefixed by / and ended by .*, for example "/api/foo.*", to match exact path "/api/foo" with any suffix like "/api/foo.protobuf"
  • - *
  • path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"
  • - *
  • path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"
  • - *
- */ - public Builder includes(String... includePatterns) { - return includes(asList(includePatterns)); - } - - /** - * Add exclusion patterns. See format described in {@link #includes(String...)} - */ - public Builder includes(Collection includePatterns) { - this.inclusions.addAll(includePatterns); - this.inclusionPredicates.addAll(includePatterns.stream() - .filter(pattern -> !MATCH_ALL.equals(pattern)) - .map(Builder::compile) - .collect(Collectors.toList())); - return this; - } - - public Builder excludes(String... excludePatterns) { - return excludes(asList(excludePatterns)); - } - - public Builder excludes(Collection excludePatterns) { - this.exclusions.addAll(excludePatterns); - this.exclusionPredicates.addAll(excludePatterns.stream() - .map(Builder::compile) - .collect(Collectors.toList())); - return this; - } - + @Override public UrlPattern build() { return new UrlPattern(this); } - - private static Predicate compile(String pattern) { - int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length(); - if (countStars == 0) { - checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern); - return url -> url.equals(pattern); - } - checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern); - if (pattern.charAt(0) == '/') { - checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern); - if (pattern.endsWith("/*")) { - String path = pattern.substring(0, pattern.length() - "/*".length()); - return url -> url.startsWith(path); - } - if (pattern.endsWith(".*")) { - String path = pattern.substring(0, pattern.length() - ".*".length()); - return url -> substringBeforeLast(url, ".").equals(path); - } - String path = pattern.substring(0, pattern.length() - "*".length()); - return url -> url.startsWith(path); - } - checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern); - // remove the leading * - String path = pattern.substring(1); - return url -> url.endsWith(path); - } } + }