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.Collection;
020import java.util.Collections;
021import java.util.LinkedHashSet;
022import java.util.Set;
023
024import javax.servlet.http.HttpServletRequest;
025
026import org.springframework.lang.Nullable;
027import org.springframework.util.ObjectUtils;
028import org.springframework.web.bind.annotation.RequestMapping;
029import org.springframework.web.cors.CorsUtils;
030
031/**
032 * A logical conjunction (' && ') request condition that matches a request against
033 * a set of header expressions with syntax defined in {@link RequestMapping#headers()}.
034 *
035 * <p>Expressions passed to the constructor with header names 'Accept' or
036 * 'Content-Type' are ignored. See {@link ConsumesRequestCondition} and
037 * {@link ProducesRequestCondition} for those.
038 *
039 * @author Arjen Poutsma
040 * @author Rossen Stoyanchev
041 * @since 3.1
042 */
043public final class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> {
044
045        private static final HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition();
046
047
048        private final Set<HeaderExpression> expressions;
049
050
051        /**
052         * Create a new instance from the given header expressions. Expressions with
053         * header names 'Accept' or 'Content-Type' are ignored. See {@link ConsumesRequestCondition}
054         * and {@link ProducesRequestCondition} for those.
055         * @param headers media type expressions with syntax defined in {@link RequestMapping#headers()};
056         * if 0, the condition will match to every request
057         */
058        public HeadersRequestCondition(String... headers) {
059                this.expressions = parseExpressions(headers);
060        }
061
062        private static Set<HeaderExpression> parseExpressions(String... headers) {
063                Set<HeaderExpression> result = null;
064                if (!ObjectUtils.isEmpty(headers)) {
065                        for (String header : headers) {
066                                HeaderExpression expr = new HeaderExpression(header);
067                                if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) {
068                                        continue;
069                                }
070                                result = (result != null ? result : new LinkedHashSet<>(headers.length));
071                                result.add(expr);
072                        }
073                }
074                return (result != null ? result : Collections.emptySet());
075        }
076
077        private HeadersRequestCondition(Set<HeaderExpression> conditions) {
078                this.expressions = conditions;
079        }
080
081
082        /**
083         * Return the contained request header expressions.
084         */
085        public Set<NameValueExpression<String>> getExpressions() {
086                return new LinkedHashSet<>(this.expressions);
087        }
088
089        @Override
090        protected Collection<HeaderExpression> getContent() {
091                return this.expressions;
092        }
093
094        @Override
095        protected String getToStringInfix() {
096                return " && ";
097        }
098
099        /**
100         * Returns a new instance with the union of the header expressions
101         * from "this" and the "other" instance.
102         */
103        @Override
104        public HeadersRequestCondition combine(HeadersRequestCondition other) {
105                if (isEmpty() && other.isEmpty()) {
106                        return this;
107                }
108                else if (other.isEmpty()) {
109                        return this;
110                }
111                else if (isEmpty()) {
112                        return other;
113                }
114                Set<HeaderExpression> set = new LinkedHashSet<>(this.expressions);
115                set.addAll(other.expressions);
116                return new HeadersRequestCondition(set);
117        }
118
119        /**
120         * Returns "this" instance if the request matches all expressions;
121         * or {@code null} otherwise.
122         */
123        @Override
124        @Nullable
125        public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) {
126                if (CorsUtils.isPreFlightRequest(request)) {
127                        return PRE_FLIGHT_MATCH;
128                }
129                for (HeaderExpression expression : this.expressions) {
130                        if (!expression.match(request)) {
131                                return null;
132                        }
133                }
134                return this;
135        }
136
137        /**
138         * Compare to another condition based on header expressions. A condition
139         * is considered to be a more specific match, if it has:
140         * <ol>
141         * <li>A greater number of expressions.
142         * <li>A greater number of non-negated expressions with a concrete value.
143         * </ol>
144         * <p>It is assumed that both instances have been obtained via
145         * {@link #getMatchingCondition(HttpServletRequest)} and each instance
146         * contains the matching header expression only or is otherwise empty.
147         */
148        @Override
149        public int compareTo(HeadersRequestCondition other, HttpServletRequest request) {
150                int result = other.expressions.size() - this.expressions.size();
151                if (result != 0) {
152                        return result;
153                }
154                return (int) (getValueMatchCount(other.expressions) - getValueMatchCount(this.expressions));
155        }
156
157        private long getValueMatchCount(Set<HeaderExpression> expressions) {
158                long count = 0;
159                for (HeaderExpression e : expressions) {
160                        if (e.getValue() != null && !e.isNegated()) {
161                                count++;
162                        }
163                }
164                return count;
165        }
166
167
168        /**
169         * Parses and matches a single header expression to a request.
170         */
171        static class HeaderExpression extends AbstractNameValueExpression<String> {
172
173                HeaderExpression(String expression) {
174                        super(expression);
175                }
176
177                @Override
178                protected boolean isCaseSensitiveName() {
179                        return false;
180                }
181
182                @Override
183                protected String parseValue(String valueExpression) {
184                        return valueExpression;
185                }
186
187                @Override
188                protected boolean matchName(HttpServletRequest request) {
189                        return (request.getHeader(this.name) != null);
190                }
191
192                @Override
193                protected boolean matchValue(HttpServletRequest request) {
194                        return ObjectUtils.nullSafeEquals(this.value, request.getHeader(this.name));
195                }
196        }
197
198}