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}