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