001/*
002 * Copyright 2002-2018 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.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Set;
026import javax.servlet.http.HttpServletRequest;
027
028import org.springframework.http.InvalidMediaTypeException;
029import org.springframework.http.MediaType;
030import org.springframework.util.StringUtils;
031import org.springframework.web.bind.annotation.RequestMapping;
032import org.springframework.web.cors.CorsUtils;
033import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
034
035/**
036 * A logical disjunction (' || ') request condition to match a request's
037 * 'Content-Type' header to a list of media type expressions. Two kinds of
038 * media type expressions are supported, which are described in
039 * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()}
040 * where the header name is 'Content-Type'. Regardless of which syntax is
041 * used, the semantics are the same.
042 *
043 * @author Arjen Poutsma
044 * @author Rossen Stoyanchev
045 * @since 3.1
046 */
047public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> {
048
049        private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition();
050
051        private final List<ConsumeMediaTypeExpression> expressions;
052
053
054        /**
055         * Creates a new instance from 0 or more "consumes" expressions.
056         * @param consumes expressions with the syntax described in
057         * {@link RequestMapping#consumes()}; if 0 expressions are provided,
058         * the condition will match to every request
059         */
060        public ConsumesRequestCondition(String... consumes) {
061                this(consumes, null);
062        }
063
064        /**
065         * Creates a new instance with "consumes" and "header" expressions.
066         * "Header" expressions where the header name is not 'Content-Type' or have
067         * no header value defined are ignored. If 0 expressions are provided in
068         * total, the condition will match to every request
069         * @param consumes as described in {@link RequestMapping#consumes()}
070         * @param headers as described in {@link RequestMapping#headers()}
071         */
072        public ConsumesRequestCondition(String[] consumes, String[] headers) {
073                this(parseExpressions(consumes, headers));
074        }
075
076        /**
077         * Private constructor accepting parsed media type expressions.
078         */
079        private ConsumesRequestCondition(Collection<ConsumeMediaTypeExpression> expressions) {
080                this.expressions = new ArrayList<ConsumeMediaTypeExpression>(expressions);
081                Collections.sort(this.expressions);
082        }
083
084
085        private static Set<ConsumeMediaTypeExpression> parseExpressions(String[] consumes, String[] headers) {
086                Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>();
087                if (headers != null) {
088                        for (String header : headers) {
089                                HeaderExpression expr = new HeaderExpression(header);
090                                if ("Content-Type".equalsIgnoreCase(expr.name)) {
091                                        for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
092                                                result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated));
093                                        }
094                                }
095                        }
096                }
097                if (consumes != null) {
098                        for (String consume : consumes) {
099                                result.add(new ConsumeMediaTypeExpression(consume));
100                        }
101                }
102                return result;
103        }
104
105
106        /**
107         * Return the contained MediaType expressions.
108         */
109        public Set<MediaTypeExpression> getExpressions() {
110                return new LinkedHashSet<MediaTypeExpression>(this.expressions);
111        }
112
113        /**
114         * Returns the media types for this condition excluding negated expressions.
115         */
116        public Set<MediaType> getConsumableMediaTypes() {
117                Set<MediaType> result = new LinkedHashSet<MediaType>();
118                for (ConsumeMediaTypeExpression expression : this.expressions) {
119                        if (!expression.isNegated()) {
120                                result.add(expression.getMediaType());
121                        }
122                }
123                return result;
124        }
125
126        /**
127         * Whether the condition has any media type expressions.
128         */
129        @Override
130        public boolean isEmpty() {
131                return this.expressions.isEmpty();
132        }
133
134        @Override
135        protected Collection<ConsumeMediaTypeExpression> getContent() {
136                return this.expressions;
137        }
138
139        @Override
140        protected String getToStringInfix() {
141                return " || ";
142        }
143
144        /**
145         * Returns the "other" instance if it has any expressions; returns "this"
146         * instance otherwise. Practically that means a method-level "consumes"
147         * overrides a type-level "consumes" condition.
148         */
149        @Override
150        public ConsumesRequestCondition combine(ConsumesRequestCondition other) {
151                return !other.expressions.isEmpty() ? other : this;
152        }
153
154        /**
155         * Checks if any of the contained media type expressions match the given
156         * request 'Content-Type' header and returns an instance that is guaranteed
157         * to contain matching expressions only. The match is performed via
158         * {@link MediaType#includes(MediaType)}.
159         * @param request the current request
160         * @return the same instance if the condition contains no expressions;
161         * or a new condition with matching expressions only;
162         * or {@code null} if no expressions match
163         */
164        @Override
165        public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
166                if (CorsUtils.isPreFlightRequest(request)) {
167                        return PRE_FLIGHT_MATCH;
168                }
169                if (isEmpty()) {
170                        return this;
171                }
172
173                MediaType contentType;
174                try {
175                        contentType = (StringUtils.hasLength(request.getContentType()) ?
176                                        MediaType.parseMediaType(request.getContentType()) :
177                                        MediaType.APPLICATION_OCTET_STREAM);
178                }
179                catch (InvalidMediaTypeException ex) {
180                        return null;
181                }
182
183                Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(this.expressions);
184                for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
185                        ConsumeMediaTypeExpression expression = iterator.next();
186                        if (!expression.match(contentType)) {
187                                iterator.remove();
188                        }
189                }
190                return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null);
191        }
192
193        /**
194         * Returns:
195         * <ul>
196         * <li>0 if the two conditions have the same number of expressions
197         * <li>Less than 0 if "this" has more or more specific media type expressions
198         * <li>Greater than 0 if "other" has more or more specific media type expressions
199         * </ul>
200         * <p>It is assumed that both instances have been obtained via
201         * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains
202         * the matching consumable media type expression only or is otherwise empty.
203         */
204        @Override
205        public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) {
206                if (this.expressions.isEmpty() && other.expressions.isEmpty()) {
207                        return 0;
208                }
209                else if (this.expressions.isEmpty()) {
210                        return 1;
211                }
212                else if (other.expressions.isEmpty()) {
213                        return -1;
214                }
215                else {
216                        return this.expressions.get(0).compareTo(other.expressions.get(0));
217                }
218        }
219
220
221        /**
222         * Parses and matches a single media type expression to a request's 'Content-Type' header.
223         */
224        static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression {
225
226                ConsumeMediaTypeExpression(String expression) {
227                        super(expression);
228                }
229
230                ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) {
231                        super(mediaType, negated);
232                }
233
234                public final boolean match(MediaType contentType) {
235                        boolean match = getMediaType().includes(contentType);
236                        return (!isNegated() ? match : !match);
237                }
238        }
239
240}