001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.web;
021
022import com.google.common.collect.ImmutableList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashSet;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.function.Predicate;
030import java.util.stream.Collectors;
031import javax.servlet.Filter;
032import org.sonar.api.ExtensionPoint;
033import org.sonar.api.server.ServerSide;
034
035import static com.google.common.base.Preconditions.checkArgument;
036import static java.util.Arrays.asList;
037
038/**
039 * @since 3.1
040 */
041@ServerSide
042@ExtensionPoint
043public abstract class ServletFilter implements Filter {
044
045  /**
046   * Override to change URL. Default is /*
047   */
048  public UrlPattern doGetPattern() {
049    return UrlPattern.builder().build();
050  }
051
052  public static final class UrlPattern {
053
054    private static final String MATCH_ALL = "/*";
055
056    private final List<String> inclusions;
057    private final List<String> exclusions;
058    private final Predicate<String>[] inclusionPredicates;
059    private final Predicate<String>[] exclusionPredicates;
060
061    private UrlPattern(Builder builder) {
062      this.inclusions = ImmutableList.copyOf(builder.inclusions);
063      this.exclusions = ImmutableList.copyOf(builder.exclusions);
064      if (builder.inclusionPredicates.isEmpty()) {
065        // because Stream#anyMatch() returns false if stream is empty
066        this.inclusionPredicates = new Predicate[] {s -> true};
067      } else {
068        this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new);
069      }
070      this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new);
071    }
072
073    public boolean matches(String path) {
074      return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) &&
075        Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path));
076    }
077
078    /**
079     * @since 6.0
080     */
081    public Collection<String> getInclusions() {
082      return inclusions;
083    }
084
085    /**
086     * @since 6.0
087     */
088    public Collection<String> getExclusions() {
089      return exclusions;
090    }
091
092    /**
093     * @deprecated replaced in version 6.0 by {@link #getInclusions()} and {@link #getExclusions()}
094     * @throws IllegalStateException if at least one exclusion or more than one inclusions are defined
095     */
096    @Deprecated
097    public String getUrl() {
098      // Before 6.0, it was only possible to include one url
099      if (exclusions.isEmpty() && inclusions.size() == 1) {
100        return inclusions.get(0);
101      }
102      throw new IllegalStateException("this method is deprecated and should not be used anymore");
103    }
104
105    /**
106     * Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}.
107     */
108    public static UrlPattern create(String inclusionPattern) {
109      return builder().includes(inclusionPattern).build();
110    }
111
112    /**
113     * @since 6.0
114     */
115    public static Builder builder() {
116      return new Builder();
117    }
118
119    /**
120     * @since 6.0
121     */
122    public static class Builder {
123      private static final String WILDCARD_CHAR = "*";
124      private static final Collection<String> STATIC_RESOURCES = ImmutableList.of("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*");
125
126      private final Set<String> inclusions = new LinkedHashSet<>();
127      private final Set<String> exclusions = new LinkedHashSet<>();
128      private final Set<Predicate<String>> inclusionPredicates = new HashSet<>();
129      private final Set<Predicate<String>> exclusionPredicates = new HashSet<>();
130
131      private Builder() {
132      }
133
134      public static Collection<String> staticResourcePatterns() {
135        return STATIC_RESOURCES;
136      }
137
138      /**
139       * Add inclusion patterns. Supported formats are:
140       * <ul>
141       *   <li>path prefixed by / and ended by *, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li>
142       *   <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li>
143       *   <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li>
144       * </ul>
145       */
146      public Builder includes(String... includePatterns) {
147        return includes(asList(includePatterns));
148      }
149
150      /**
151       * Add exclusion patterns. See format described in {@link #includes(String...)}
152       */
153      public Builder includes(Collection<String> includePatterns) {
154        this.inclusions.addAll(includePatterns);
155        this.inclusionPredicates.addAll(includePatterns.stream()
156          .filter(pattern -> !MATCH_ALL.equals(pattern))
157          .map(Builder::compile)
158          .collect(Collectors.toList()));
159        return this;
160      }
161
162      public Builder excludes(String... excludePatterns) {
163        return excludes(asList(excludePatterns));
164      }
165
166      public Builder excludes(Collection<String> excludePatterns) {
167        this.exclusions.addAll(excludePatterns);
168        this.exclusionPredicates.addAll(excludePatterns.stream()
169          .map(Builder::compile)
170          .collect(Collectors.toList()));
171        return this;
172      }
173
174      public UrlPattern build() {
175        return new UrlPattern(this);
176      }
177
178      private static Predicate<String> compile(String pattern) {
179        int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length();
180        if (countStars == 0) {
181          checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern);
182          return url -> url.equals(pattern);
183        }
184        checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern);
185        if (pattern.charAt(0) == '/') {
186          checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern);
187          // remove the ending /* or *
188          String path = pattern.replaceAll("/?\\*", "");
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  }
198}