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}