001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.condition; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.LinkedHashSet; 024import java.util.Map; 025import java.util.Set; 026 027import javax.servlet.DispatcherType; 028import javax.servlet.http.HttpServletRequest; 029 030import org.springframework.http.HttpHeaders; 031import org.springframework.http.HttpMethod; 032import org.springframework.lang.Nullable; 033import org.springframework.util.ObjectUtils; 034import org.springframework.web.bind.annotation.RequestMethod; 035import org.springframework.web.cors.CorsUtils; 036 037/** 038 * A logical disjunction (' || ') request condition that matches a request 039 * against a set of {@link RequestMethod RequestMethods}. 040 * 041 * @author Arjen Poutsma 042 * @author Rossen Stoyanchev 043 * @since 3.1 044 */ 045public final class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> { 046 047 /** Per HTTP method cache to return ready instances from getMatchingCondition. */ 048 private static final Map<String, RequestMethodsRequestCondition> requestMethodConditionCache; 049 050 static { 051 requestMethodConditionCache = new HashMap<>(RequestMethod.values().length); 052 for (RequestMethod method : RequestMethod.values()) { 053 requestMethodConditionCache.put(method.name(), new RequestMethodsRequestCondition(method)); 054 } 055 } 056 057 058 private final Set<RequestMethod> methods; 059 060 061 /** 062 * Create a new instance with the given request methods. 063 * @param requestMethods 0 or more HTTP request methods; 064 * if, 0 the condition will match to every request 065 */ 066 public RequestMethodsRequestCondition(RequestMethod... requestMethods) { 067 this.methods = (ObjectUtils.isEmpty(requestMethods) ? 068 Collections.emptySet() : new LinkedHashSet<>(Arrays.asList(requestMethods))); 069 } 070 071 /** 072 * Private constructor for internal use when combining conditions. 073 */ 074 private RequestMethodsRequestCondition(Set<RequestMethod> methods) { 075 this.methods = methods; 076 } 077 078 079 /** 080 * Returns all {@link RequestMethod RequestMethods} contained in this condition. 081 */ 082 public Set<RequestMethod> getMethods() { 083 return this.methods; 084 } 085 086 @Override 087 protected Collection<RequestMethod> getContent() { 088 return this.methods; 089 } 090 091 @Override 092 protected String getToStringInfix() { 093 return " || "; 094 } 095 096 /** 097 * Returns a new instance with a union of the HTTP request methods 098 * from "this" and the "other" instance. 099 */ 100 @Override 101 public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) { 102 if (isEmpty() && other.isEmpty()) { 103 return this; 104 } 105 else if (other.isEmpty()) { 106 return this; 107 } 108 else if (isEmpty()) { 109 return other; 110 } 111 Set<RequestMethod> set = new LinkedHashSet<>(this.methods); 112 set.addAll(other.methods); 113 return new RequestMethodsRequestCondition(set); 114 } 115 116 /** 117 * Check if any of the HTTP request methods match the given request and 118 * return an instance that contains the matching HTTP request method only. 119 * @param request the current request 120 * @return the same instance if the condition is empty (unless the request 121 * method is HTTP OPTIONS), a new condition with the matched request method, 122 * or {@code null} if there is no match or the condition is empty and the 123 * request method is OPTIONS. 124 */ 125 @Override 126 @Nullable 127 public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) { 128 if (CorsUtils.isPreFlightRequest(request)) { 129 return matchPreFlight(request); 130 } 131 132 if (getMethods().isEmpty()) { 133 if (RequestMethod.OPTIONS.name().equals(request.getMethod()) && 134 !DispatcherType.ERROR.equals(request.getDispatcherType())) { 135 136 return null; // We handle OPTIONS transparently, so don't match if no explicit declarations 137 } 138 return this; 139 } 140 141 return matchRequestMethod(request.getMethod()); 142 } 143 144 /** 145 * On a pre-flight request match to the would-be, actual request. 146 * Hence empty conditions is a match, otherwise try to match to the HTTP 147 * method in the "Access-Control-Request-Method" header. 148 */ 149 @Nullable 150 private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) { 151 if (getMethods().isEmpty()) { 152 return this; 153 } 154 String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); 155 return matchRequestMethod(expectedMethod); 156 } 157 158 @Nullable 159 private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { 160 RequestMethod requestMethod; 161 try { 162 requestMethod = RequestMethod.valueOf(httpMethodValue); 163 if (getMethods().contains(requestMethod)) { 164 return requestMethodConditionCache.get(httpMethodValue); 165 } 166 if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) { 167 return requestMethodConditionCache.get(HttpMethod.GET.name()); 168 } 169 } 170 catch (IllegalArgumentException ex) { 171 // Custom request method 172 } 173 return null; 174 } 175 176 /** 177 * Returns: 178 * <ul> 179 * <li>0 if the two conditions contain the same number of HTTP request methods 180 * <li>Less than 0 if "this" instance has an HTTP request method but "other" doesn't 181 * <li>Greater than 0 "other" has an HTTP request method but "this" doesn't 182 * </ul> 183 * <p>It is assumed that both instances have been obtained via 184 * {@link #getMatchingCondition(HttpServletRequest)} and therefore each instance 185 * contains the matching HTTP request method only or is otherwise empty. 186 */ 187 @Override 188 public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) { 189 if (other.methods.size() != this.methods.size()) { 190 return other.methods.size() - this.methods.size(); 191 } 192 else if (this.methods.size() == 1) { 193 if (this.methods.contains(RequestMethod.HEAD) && other.methods.contains(RequestMethod.GET)) { 194 return -1; 195 } 196 else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) { 197 return 1; 198 } 199 } 200 return 0; 201 } 202 203}