001/* 002 * Copyright 2002-2016 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.LinkedHashSet; 023import java.util.List; 024import java.util.Set; 025import javax.servlet.DispatcherType; 026import javax.servlet.http.HttpServletRequest; 027 028import org.springframework.http.HttpHeaders; 029import org.springframework.http.HttpMethod; 030import org.springframework.web.bind.annotation.RequestMethod; 031import org.springframework.web.cors.CorsUtils; 032 033/** 034 * A logical disjunction (' || ') request condition that matches a request 035 * against a set of {@link RequestMethod}s. 036 * 037 * @author Arjen Poutsma 038 * @author Rossen Stoyanchev 039 * @since 3.1 040 */ 041public final class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> { 042 043 private static final RequestMethodsRequestCondition GET_CONDITION = 044 new RequestMethodsRequestCondition(RequestMethod.GET); 045 046 047 private final Set<RequestMethod> methods; 048 049 050 /** 051 * Create a new instance with the given request methods. 052 * @param requestMethods 0 or more HTTP request methods; 053 * if, 0 the condition will match to every request 054 */ 055 public RequestMethodsRequestCondition(RequestMethod... requestMethods) { 056 this(asList(requestMethods)); 057 } 058 059 private RequestMethodsRequestCondition(Collection<RequestMethod> requestMethods) { 060 this.methods = Collections.unmodifiableSet(new LinkedHashSet<RequestMethod>(requestMethods)); 061 } 062 063 064 private static List<RequestMethod> asList(RequestMethod... requestMethods) { 065 return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.<RequestMethod>emptyList()); 066 } 067 068 069 /** 070 * Returns all {@link RequestMethod}s contained in this condition. 071 */ 072 public Set<RequestMethod> getMethods() { 073 return this.methods; 074 } 075 076 @Override 077 protected Collection<RequestMethod> getContent() { 078 return this.methods; 079 } 080 081 @Override 082 protected String getToStringInfix() { 083 return " || "; 084 } 085 086 /** 087 * Returns a new instance with a union of the HTTP request methods 088 * from "this" and the "other" instance. 089 */ 090 @Override 091 public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) { 092 Set<RequestMethod> set = new LinkedHashSet<RequestMethod>(this.methods); 093 set.addAll(other.methods); 094 return new RequestMethodsRequestCondition(set); 095 } 096 097 /** 098 * Check if any of the HTTP request methods match the given request and 099 * return an instance that contains the matching HTTP request method only. 100 * @param request the current request 101 * @return the same instance if the condition is empty (unless the request 102 * method is HTTP OPTIONS), a new condition with the matched request method, 103 * or {@code null} if there is no match or the condition is empty and the 104 * request method is OPTIONS. 105 */ 106 @Override 107 public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) { 108 if (CorsUtils.isPreFlightRequest(request)) { 109 return matchPreFlight(request); 110 } 111 112 if (getMethods().isEmpty()) { 113 if (RequestMethod.OPTIONS.name().equals(request.getMethod()) && 114 !DispatcherType.ERROR.equals(request.getDispatcherType())) { 115 116 return null; // No implicit match for OPTIONS (we handle it) 117 } 118 return this; 119 } 120 121 return matchRequestMethod(request.getMethod()); 122 } 123 124 /** 125 * On a pre-flight request match to the would-be, actual request. 126 * Hence empty conditions is a match, otherwise try to match to the HTTP 127 * method in the "Access-Control-Request-Method" header. 128 */ 129 private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) { 130 if (getMethods().isEmpty()) { 131 return this; 132 } 133 String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); 134 return matchRequestMethod(expectedMethod); 135 } 136 137 private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { 138 HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue); 139 if (httpMethod != null) { 140 for (RequestMethod method : getMethods()) { 141 if (httpMethod.matches(method.name())) { 142 return new RequestMethodsRequestCondition(method); 143 } 144 } 145 if (httpMethod == HttpMethod.HEAD && getMethods().contains(RequestMethod.GET)) { 146 return GET_CONDITION; 147 } 148 } 149 return null; 150 } 151 152 /** 153 * Returns: 154 * <ul> 155 * <li>0 if the two conditions contain the same number of HTTP request methods 156 * <li>Less than 0 if "this" instance has an HTTP request method but "other" doesn't 157 * <li>Greater than 0 "other" has an HTTP request method but "this" doesn't 158 * </ul> 159 * <p>It is assumed that both instances have been obtained via 160 * {@link #getMatchingCondition(HttpServletRequest)} and therefore each instance 161 * contains the matching HTTP request method only or is otherwise empty. 162 */ 163 @Override 164 public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) { 165 if (other.methods.size() != this.methods.size()) { 166 return other.methods.size() - this.methods.size(); 167 } 168 else if (this.methods.size() == 1) { 169 if (this.methods.contains(RequestMethod.HEAD) && other.methods.contains(RequestMethod.GET)) { 170 return -1; 171 } 172 else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) { 173 return 1; 174 } 175 } 176 return 0; 177 } 178 179}