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.reactive.result.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 org.springframework.http.HttpMethod;
028import org.springframework.http.server.reactive.ServerHttpRequest;
029import org.springframework.lang.Nullable;
030import org.springframework.util.ObjectUtils;
031import org.springframework.web.bind.annotation.RequestMethod;
032import org.springframework.web.cors.reactive.CorsUtils;
033import org.springframework.web.server.ServerWebExchange;
034
035/**
036 * A logical disjunction (' || ') request condition that matches a request
037 * against a set of {@link RequestMethod RequestMethods}.
038 *
039 * @author Rossen Stoyanchev
040 * @since 5.0
041 */
042public final class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> {
043
044        /** Per HTTP method cache to return ready instances from getMatchingCondition. */
045        private static final Map<HttpMethod, RequestMethodsRequestCondition> requestMethodConditionCache;
046
047        static {
048                requestMethodConditionCache = new HashMap<>(RequestMethod.values().length);
049                for (RequestMethod method : RequestMethod.values()) {
050                        requestMethodConditionCache.put(
051                                        HttpMethod.valueOf(method.name()), new RequestMethodsRequestCondition(method));
052                }
053        }
054
055
056        private final Set<RequestMethod> methods;
057
058
059        /**
060         * Create a new instance with the given request methods.
061         * @param requestMethods 0 or more HTTP request methods;
062         * if, 0 the condition will match to every request
063         */
064        public RequestMethodsRequestCondition(RequestMethod... requestMethods) {
065                this.methods = (ObjectUtils.isEmpty(requestMethods) ?
066                                Collections.emptySet() : new LinkedHashSet<>(Arrays.asList(requestMethods)));
067        }
068
069        /**
070         * Private constructor for internal use when combining conditions.
071         */
072        private RequestMethodsRequestCondition(Set<RequestMethod> requestMethods) {
073                this.methods = requestMethods;
074        }
075
076
077        /**
078         * Returns all {@link RequestMethod RequestMethods} contained in this condition.
079         */
080        public Set<RequestMethod> getMethods() {
081                return this.methods;
082        }
083
084        @Override
085        protected Collection<RequestMethod> getContent() {
086                return this.methods;
087        }
088
089        @Override
090        protected String getToStringInfix() {
091                return " || ";
092        }
093
094        /**
095         * Returns a new instance with a union of the HTTP request methods
096         * from "this" and the "other" instance.
097         */
098        @Override
099        public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) {
100                if (isEmpty() && other.isEmpty()) {
101                        return this;
102                }
103                else if (other.isEmpty()) {
104                        return this;
105                }
106                else if (isEmpty()) {
107                        return other;
108                }
109                Set<RequestMethod> set = new LinkedHashSet<>(this.methods);
110                set.addAll(other.methods);
111                return new RequestMethodsRequestCondition(set);
112        }
113
114        /**
115         * Check if any of the HTTP request methods match the given request and
116         * return an instance that contains the matching HTTP request method only.
117         * @param exchange the current exchange
118         * @return the same instance if the condition is empty (unless the request
119         * method is HTTP OPTIONS), a new condition with the matched request method,
120         * or {@code null} if there is no match or the condition is empty and the
121         * request method is OPTIONS.
122         */
123        @Override
124        @Nullable
125        public RequestMethodsRequestCondition getMatchingCondition(ServerWebExchange exchange) {
126                if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
127                        return matchPreFlight(exchange.getRequest());
128                }
129                if (getMethods().isEmpty()) {
130                        if (RequestMethod.OPTIONS.name().equals(exchange.getRequest().getMethodValue())) {
131                                return null; // We handle OPTIONS transparently, so don't match if no explicit declarations
132                        }
133                        return this;
134                }
135                return matchRequestMethod(exchange.getRequest().getMethod());
136        }
137
138        /**
139         * On a pre-flight request match to the would-be, actual request.
140         * Hence empty conditions is a match, otherwise try to match to the HTTP
141         * method in the "Access-Control-Request-Method" header.
142         */
143        @Nullable
144        private RequestMethodsRequestCondition matchPreFlight(ServerHttpRequest request) {
145                if (getMethods().isEmpty()) {
146                        return this;
147                }
148                HttpMethod expectedMethod = request.getHeaders().getAccessControlRequestMethod();
149                return expectedMethod != null ? matchRequestMethod(expectedMethod) : null;
150        }
151
152        @Nullable
153        private RequestMethodsRequestCondition matchRequestMethod(@Nullable HttpMethod httpMethod) {
154                if (httpMethod == null) {
155                        return null;
156                }
157                RequestMethod requestMethod = RequestMethod.valueOf(httpMethod.name());
158                if (getMethods().contains(requestMethod)) {
159                        return requestMethodConditionCache.get(httpMethod);
160                }
161                if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
162                        return requestMethodConditionCache.get(HttpMethod.GET);
163                }
164                return null;
165        }
166
167        /**
168         * Returns:
169         * <ul>
170         * <li>0 if the two conditions contain the same number of HTTP request methods
171         * <li>Less than 0 if "this" instance has an HTTP request method but "other" doesn't
172         * <li>Greater than 0 "other" has an HTTP request method but "this" doesn't
173         * </ul>
174         * <p>It is assumed that both instances have been obtained via
175         * {@link #getMatchingCondition(ServerWebExchange)} and therefore each instance
176         * contains the matching HTTP request method only or is otherwise empty.
177         */
178        @Override
179        public int compareTo(RequestMethodsRequestCondition other, ServerWebExchange exchange) {
180                if (other.methods.size() != this.methods.size()) {
181                        return other.methods.size() - this.methods.size();
182                }
183                else if (this.methods.size() == 1) {
184                        if (this.methods.contains(RequestMethod.HEAD) && other.methods.contains(RequestMethod.GET)) {
185                                return -1;
186                        }
187                        else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
188                                return 1;
189                        }
190                }
191                return 0;
192        }
193
194}