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.MediaType;
029import org.springframework.web.HttpMediaTypeException;
030import org.springframework.web.HttpMediaTypeNotAcceptableException;
031import org.springframework.web.accept.ContentNegotiationManager;
032import org.springframework.web.bind.annotation.RequestMapping;
033import org.springframework.web.context.request.ServletWebRequest;
034import org.springframework.web.cors.CorsUtils;
035import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
036
037/**
038 * A logical disjunction (' || ') request condition to match a request's 'Accept' header
039 * to a list of media type expressions. Two kinds of media type expressions are
040 * supported, which are described in {@link RequestMapping#produces()} and
041 * {@link RequestMapping#headers()} where the header name is 'Accept'.
042 * Regardless of which syntax is used, the semantics are the same.
043 *
044 * @author Arjen Poutsma
045 * @author Rossen Stoyanchev
046 * @since 3.1
047 */
048public final class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> {
049
050        private static final ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition();
051
052        private static final ProducesRequestCondition EMPTY_CONDITION = new ProducesRequestCondition();
053
054        private static final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST =
055                        Collections.singletonList(new ProduceMediaTypeExpression("*/*"));
056
057
058        private final List<ProduceMediaTypeExpression> expressions;
059
060        private final ContentNegotiationManager contentNegotiationManager;
061
062
063        /**
064         * Creates a new instance from "produces" expressions. If 0 expressions
065         * are provided in total, this condition will match to any request.
066         * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
067         */
068        public ProducesRequestCondition(String... produces) {
069                this(produces, null, null);
070        }
071
072        /**
073         * Creates a new instance with "produces" and "header" expressions. "Header"
074         * expressions where the header name is not 'Accept' or have no header value
075         * defined are ignored. If 0 expressions are provided in total, this condition
076         * will match to any request.
077         * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
078         * @param headers expressions with syntax defined by {@link RequestMapping#headers()}
079         */
080        public ProducesRequestCondition(String[] produces, String[] headers) {
081                this(produces, headers, null);
082        }
083
084        /**
085         * Same as {@link #ProducesRequestCondition(String[], String[])} but also
086         * accepting a {@link ContentNegotiationManager}.
087         * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
088         * @param headers expressions with syntax defined by {@link RequestMapping#headers()}
089         * @param manager used to determine requested media types
090         */
091        public ProducesRequestCondition(String[] produces, String[] headers, ContentNegotiationManager manager) {
092                this.expressions = new ArrayList<ProduceMediaTypeExpression>(parseExpressions(produces, headers));
093                Collections.sort(this.expressions);
094                this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
095        }
096
097        /**
098         * Private constructor with already parsed media type expressions.
099         */
100        private ProducesRequestCondition(Collection<ProduceMediaTypeExpression> expressions, ContentNegotiationManager manager) {
101                this.expressions = new ArrayList<ProduceMediaTypeExpression>(expressions);
102                Collections.sort(this.expressions);
103                this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
104        }
105
106
107        private Set<ProduceMediaTypeExpression> parseExpressions(String[] produces, String[] headers) {
108                Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>();
109                if (headers != null) {
110                        for (String header : headers) {
111                                HeaderExpression expr = new HeaderExpression(header);
112                                if ("Accept".equalsIgnoreCase(expr.name)) {
113                                        for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
114                                                result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated));
115                                        }
116                                }
117                        }
118                }
119                if (produces != null) {
120                        for (String produce : produces) {
121                                result.add(new ProduceMediaTypeExpression(produce));
122                        }
123                }
124                return result;
125        }
126
127        /**
128         * Return the contained "produces" expressions.
129         */
130        public Set<MediaTypeExpression> getExpressions() {
131                return new LinkedHashSet<MediaTypeExpression>(this.expressions);
132        }
133
134        /**
135         * Return the contained producible media types excluding negated expressions.
136         */
137        public Set<MediaType> getProducibleMediaTypes() {
138                Set<MediaType> result = new LinkedHashSet<MediaType>();
139                for (ProduceMediaTypeExpression expression : this.expressions) {
140                        if (!expression.isNegated()) {
141                                result.add(expression.getMediaType());
142                        }
143                }
144                return result;
145        }
146
147        /**
148         * Whether the condition has any media type expressions.
149         */
150        @Override
151        public boolean isEmpty() {
152                return this.expressions.isEmpty();
153        }
154
155        @Override
156        protected List<ProduceMediaTypeExpression> getContent() {
157                return this.expressions;
158        }
159
160        @Override
161        protected String getToStringInfix() {
162                return " || ";
163        }
164
165        /**
166         * Returns the "other" instance if it has any expressions; returns "this"
167         * instance otherwise. Practically that means a method-level "produces"
168         * overrides a type-level "produces" condition.
169         */
170        @Override
171        public ProducesRequestCondition combine(ProducesRequestCondition other) {
172                return (!other.expressions.isEmpty() ? other : this);
173        }
174
175        /**
176         * Checks if any of the contained media type expressions match the given
177         * request 'Content-Type' header and returns an instance that is guaranteed
178         * to contain matching expressions only. The match is performed via
179         * {@link MediaType#isCompatibleWith(MediaType)}.
180         * @param request the current request
181         * @return the same instance if there are no expressions;
182         * or a new condition with matching expressions;
183         * or {@code null} if no expressions match.
184         */
185        @Override
186        public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) {
187                if (CorsUtils.isPreFlightRequest(request)) {
188                        return PRE_FLIGHT_MATCH;
189                }
190                if (isEmpty()) {
191                        return this;
192                }
193
194                List<MediaType> acceptedMediaTypes;
195                try {
196                        acceptedMediaTypes = getAcceptedMediaTypes(request);
197                }
198                catch (HttpMediaTypeException ex) {
199                        return null;
200                }
201
202                Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>(expressions);
203                for (Iterator<ProduceMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
204                        ProduceMediaTypeExpression expression = iterator.next();
205                        if (!expression.match(acceptedMediaTypes)) {
206                                iterator.remove();
207                        }
208                }
209                if (!result.isEmpty()) {
210                        return new ProducesRequestCondition(result, this.contentNegotiationManager);
211                }
212                else if (acceptedMediaTypes.contains(MediaType.ALL)) {
213                        return EMPTY_CONDITION;
214                }
215                else {
216                        return null;
217                }
218        }
219
220        /**
221         * Compares this and another "produces" condition as follows:
222         * <ol>
223         * <li>Sort 'Accept' header media types by quality value via
224         * {@link MediaType#sortByQualityValue(List)} and iterate the list.
225         * <li>Get the first index of matching media types in each "produces"
226         * condition first matching with {@link MediaType#equals(Object)} and
227         * then with {@link MediaType#includes(MediaType)}.
228         * <li>If a lower index is found, the condition at that index wins.
229         * <li>If both indexes are equal, the media types at the index are
230         * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
231         * </ol>
232         * <p>It is assumed that both instances have been obtained via
233         * {@link #getMatchingCondition(HttpServletRequest)} and each instance
234         * contains the matching producible media type expression only or
235         * is otherwise empty.
236         */
237        @Override
238        public int compareTo(ProducesRequestCondition other, HttpServletRequest request) {
239                try {
240                        List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
241                        for (MediaType acceptedMediaType : acceptedMediaTypes) {
242                                int thisIndex = this.indexOfEqualMediaType(acceptedMediaType);
243                                int otherIndex = other.indexOfEqualMediaType(acceptedMediaType);
244                                int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
245                                if (result != 0) {
246                                        return result;
247                                }
248                                thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
249                                otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
250                                result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
251                                if (result != 0) {
252                                        return result;
253                                }
254                        }
255                        return 0;
256                }
257                catch (HttpMediaTypeNotAcceptableException ex) {
258                        // should never happen
259                        throw new IllegalStateException("Cannot compare without having any requested media types", ex);
260                }
261        }
262
263        private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
264                List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
265                return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes;
266        }
267
268        private int indexOfEqualMediaType(MediaType mediaType) {
269                for (int i = 0; i < getExpressionsToCompare().size(); i++) {
270                        MediaType currentMediaType = getExpressionsToCompare().get(i).getMediaType();
271                        if (mediaType.getType().equalsIgnoreCase(currentMediaType.getType()) &&
272                                        mediaType.getSubtype().equalsIgnoreCase(currentMediaType.getSubtype())) {
273                                return i;
274                        }
275                }
276                return -1;
277        }
278
279        private int indexOfIncludedMediaType(MediaType mediaType) {
280                for (int i = 0; i < getExpressionsToCompare().size(); i++) {
281                        if (mediaType.includes(getExpressionsToCompare().get(i).getMediaType())) {
282                                return i;
283                        }
284                }
285                return -1;
286        }
287
288        private int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1,
289                        ProducesRequestCondition condition2, int index2) {
290
291                int result = 0;
292                if (index1 != index2) {
293                        result = index2 - index1;
294                }
295                else if (index1 != -1) {
296                        ProduceMediaTypeExpression expr1 = condition1.getExpressionsToCompare().get(index1);
297                        ProduceMediaTypeExpression expr2 = condition2.getExpressionsToCompare().get(index2);
298                        result = expr1.compareTo(expr2);
299                        result = (result != 0) ? result : expr1.g2.getMediaType());
300                }
301                return result;
302        }
303
304        /**
305         * Return the contained "produces" expressions or if that's empty, a list
306         * with a {@code MediaType_ALL} expression.
307         */
308        private List<ProduceMediaTypeExpression> getExpressionsToCompare() {
309                return (this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions);
310        }
311
312
313        /**
314         * Parses and matches a single media type expression to a request's 'Accept' header.
315         */
316        static class ProduceMediaTypeExpression extends AbstractMediaTypeExpression {
317
318                ProduceMediaTypeExpression(MediaType mediaType, boolean negated) {
319                        super(mediaType, negated);
320                }
321
322                ProduceMediaTypeExpression(String expression) {
323                        super(expression);
324                }
325
326                public final boolean match(List<MediaType> acceptedMediaTypes) {
327                        boolean match = matchMediaType(acceptedMediaTypes);
328                        return (!isNegated() ? match : !match);
329                }
330
331                private boolean matchMediaType(List<MediaType> acceptedMediaTypes) {
332                        for (MediaType acceptedMediaType : acceptedMediaTypes) {
333                                if (getMediaType().isCompatibleWith(acceptedMediaType)) {
334                                        return true;
335                                }
336                        }
337                        return false;
338                }
339        }
340
341}