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}