View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.mvc.condition;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.LinkedHashSet;
24  import java.util.List;
25  import java.util.Set;
26  import javax.servlet.http.HttpServletRequest;
27  
28  import org.springframework.http.MediaType;
29  import org.springframework.web.HttpMediaTypeNotAcceptableException;
30  import org.springframework.web.accept.ContentNegotiationManager;
31  import org.springframework.web.bind.annotation.RequestMapping;
32  import org.springframework.web.context.request.ServletWebRequest;
33  import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
34  
35  /**
36   * A logical disjunction (' || ') request condition to match a request's 'Accept' header
37   * to a list of media type expressions. Two kinds of media type expressions are
38   * supported, which are described in {@link RequestMapping#produces()} and
39   * {@link RequestMapping#headers()} where the header name is 'Accept'.
40   * Regardless of which syntax is used, the semantics are the same.
41   *
42   * @author Arjen Poutsma
43   * @author Rossen Stoyanchev
44   * @since 3.1
45   */
46  public final class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> {
47  
48  	private final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST =
49  			Collections.singletonList(new ProduceMediaTypeExpression("*/*"));
50  
51  	private final List<ProduceMediaTypeExpression> expressions;
52  
53  	private final ContentNegotiationManager contentNegotiationManager;
54  
55  
56  	/**
57  	 * Creates a new instance from "produces" expressions. If 0 expressions
58  	 * are provided in total, this condition will match to any request.
59  	 * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
60  	 */
61  	public ProducesRequestCondition(String... produces) {
62  		this(produces, (String[]) null);
63  	}
64  
65  	/**
66  	 * Creates a new instance with "produces" and "header" expressions. "Header"
67  	 * expressions where the header name is not 'Accept' or have no header value
68  	 * defined are ignored. If 0 expressions are provided in total, this condition
69  	 * will match to any request.
70  	 * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
71  	 * @param headers expressions with syntax defined by {@link RequestMapping#headers()}
72  	 */
73  	public ProducesRequestCondition(String[] produces, String[] headers) {
74  		this(produces, headers, null);
75  	}
76  
77  	/**
78  	 * Same as {@link #ProducesRequestCondition(String[], String[])} but also
79  	 * accepting a {@link ContentNegotiationManager}.
80  	 * @param produces expressions with syntax defined by {@link RequestMapping#produces()}
81  	 * @param headers expressions with syntax defined by {@link RequestMapping#headers()}
82  	 * @param manager used to determine requested media types
83  	 */
84  	public ProducesRequestCondition(String[] produces, String[] headers, ContentNegotiationManager manager) {
85  		this.expressions = new ArrayList<ProduceMediaTypeExpression>(parseExpressions(produces, headers));
86  		Collections.sort(this.expressions);
87  		this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
88  	}
89  
90  	/**
91  	 * Private constructor with already parsed media type expressions.
92  	 */
93  	private ProducesRequestCondition(Collection<ProduceMediaTypeExpression> expressions, ContentNegotiationManager manager) {
94  		this.expressions = new ArrayList<ProduceMediaTypeExpression>(expressions);
95  		Collections.sort(this.expressions);
96  		this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
97  	}
98  
99  
100 	private Set<ProduceMediaTypeExpression> parseExpressions(String[] produces, String[] headers) {
101 		Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>();
102 		if (headers != null) {
103 			for (String header : headers) {
104 				HeaderExpression expr = new HeaderExpression(header);
105 				if ("Accept".equalsIgnoreCase(expr.name)) {
106 					for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
107 						result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated));
108 					}
109 				}
110 			}
111 		}
112 		if (produces != null) {
113 			for (String produce : produces) {
114 				result.add(new ProduceMediaTypeExpression(produce));
115 			}
116 		}
117 		return result;
118 	}
119 
120 	/**
121 	 * Return the contained "produces" expressions.
122 	 */
123 	public Set<MediaTypeExpression> getExpressions() {
124 		return new LinkedHashSet<MediaTypeExpression>(this.expressions);
125 	}
126 
127 	/**
128 	 * Return the contained producible media types excluding negated expressions.
129 	 */
130 	public Set<MediaType> getProducibleMediaTypes() {
131 		Set<MediaType> result = new LinkedHashSet<MediaType>();
132 		for (ProduceMediaTypeExpression expression : this.expressions) {
133 			if (!expression.isNegated()) {
134 				result.add(expression.getMediaType());
135 			}
136 		}
137 		return result;
138 	}
139 
140 	/**
141 	 * Whether the condition has any media type expressions.
142 	 */
143 	public boolean isEmpty() {
144 		return this.expressions.isEmpty();
145 	}
146 
147 	@Override
148 	protected List<ProduceMediaTypeExpression> getContent() {
149 		return this.expressions;
150 	}
151 
152 	@Override
153 	protected String getToStringInfix() {
154 		return " || ";
155 	}
156 
157 	/**
158 	 * Returns the "other" instance if it has any expressions; returns "this"
159 	 * instance otherwise. Practically that means a method-level "produces"
160 	 * overrides a type-level "produces" condition.
161 	 */
162 	@Override
163 	public ProducesRequestCondition combine(ProducesRequestCondition other) {
164 		return (!other.expressions.isEmpty() ? other : this);
165 	}
166 
167 	/**
168 	 * Checks if any of the contained media type expressions match the given
169 	 * request 'Content-Type' header and returns an instance that is guaranteed
170 	 * to contain matching expressions only. The match is performed via
171 	 * {@link MediaType#isCompatibleWith(MediaType)}.
172 	 * @param request the current request
173 	 * @return the same instance if there are no expressions;
174 	 * or a new condition with matching expressions;
175 	 * or {@code null} if no expressions match.
176 	 */
177 	@Override
178 	public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) {
179 		if (isEmpty()) {
180 			return this;
181 		}
182 		Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>(expressions);
183 		for (Iterator<ProduceMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
184 			ProduceMediaTypeExpression expression = iterator.next();
185 			if (!expression.match(request)) {
186 				iterator.remove();
187 			}
188 		}
189 		return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentNegotiationManager);
190 	}
191 
192 	/**
193 	 * Compares this and another "produces" condition as follows:
194 	 * <ol>
195 	 * <li>Sort 'Accept' header media types by quality value via
196 	 * {@link MediaType#sortByQualityValue(List)} and iterate the list.
197 	 * <li>Get the first index of matching media types in each "produces"
198 	 * condition first matching with {@link MediaType#equals(Object)} and
199 	 * then with {@link MediaType#includes(MediaType)}.
200 	 * <li>If a lower index is found, the condition at that index wins.
201 	 * <li>If both indexes are equal, the media types at the index are
202 	 * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
203 	 * </ol>
204 	 * <p>It is assumed that both instances have been obtained via
205 	 * {@link #getMatchingCondition(HttpServletRequest)} and each instance
206 	 * contains the matching producible media type expression only or
207 	 * is otherwise empty.
208 	 */
209 	@Override
210 	public int compareTo(ProducesRequestCondition other, HttpServletRequest request) {
211 		try {
212 			List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
213 			for (MediaType acceptedMediaType : acceptedMediaTypes) {
214 				int thisIndex = this.indexOfEqualMediaType(acceptedMediaType);
215 				int otherIndex = other.indexOfEqualMediaType(acceptedMediaType);
216 				int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
217 				if (result != 0) {
218 					return result;
219 				}
220 				thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
221 				otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
222 				result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
223 				if (result != 0) {
224 					return result;
225 				}
226 			}
227 			return 0;
228 		}
229 		catch (HttpMediaTypeNotAcceptableException ex) {
230 			// should never happen
231 			throw new IllegalStateException("Cannot compare without having any requested media types", ex);
232 		}
233 	}
234 
235 	private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
236 		List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
237 		return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes;
238 	}
239 
240 	private int indexOfEqualMediaType(MediaType mediaType) {
241 		for (int i = 0; i < getExpressionsToCompare().size(); i++) {
242 			MediaType currentMediaType = getExpressionsToCompare().get(i).getMediaType();
243 			if (mediaType.getType().equalsIgnoreCase(currentMediaType.getType()) &&
244 					mediaType.getSubtype().equalsIgnoreCase(currentMediaType.getSubtype())) {
245 				return i;
246 			}
247 		}
248 		return -1;
249 	}
250 
251 	private int indexOfIncludedMediaType(MediaType mediaType) {
252 		for (int i = 0; i < getExpressionsToCompare().size(); i++) {
253 			if (mediaType.includes(getExpressionsToCompare().get(i).getMediaType())) {
254 				return i;
255 			}
256 		}
257 		return -1;
258 	}
259 
260 	private int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1,
261 			ProducesRequestCondition condition2, int index2) {
262 
263 		int result = 0;
264 		if (index1 != index2) {
265 			result = index2 - index1;
266 		}
267 		else if (index1 != -1) {
268 			ProduceMediaTypeExpression expr1 = condition1.getExpressionsToCompare().get(index1);
269 			ProduceMediaTypeExpression expr2 = condition2.getExpressionsToCompare().get(index2);
270 			result = expr1.compareTo(expr2);
271 			result = (result != 0) ? result : expr1.getMediaType().compareTo(expr2.getMediaType());
272 		}
273 		return result;
274 	}
275 
276 	/**
277 	 * Return the contained "produces" expressions or if that's empty, a list
278 	 * with a {@code MediaType_ALL} expression.
279 	 */
280 	private List<ProduceMediaTypeExpression> getExpressionsToCompare() {
281 		return (this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions);
282 	}
283 
284 
285 	/**
286 	 * Parses and matches a single media type expression to a request's 'Accept' header.
287 	 */
288 	class ProduceMediaTypeExpression extends AbstractMediaTypeExpression {
289 
290 		ProduceMediaTypeExpression(MediaType mediaType, boolean negated) {
291 			super(mediaType, negated);
292 		}
293 
294 		ProduceMediaTypeExpression(String expression) {
295 			super(expression);
296 		}
297 
298 		@Override
299 		protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
300 			List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
301 			for (MediaType acceptedMediaType : acceptedMediaTypes) {
302 				if (getMediaType().isCompatibleWith(acceptedMediaType)) {
303 					return true;
304 				}
305 			}
306 			return false;
307 		}
308 	}
309 
310 }