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.InvalidMediaTypeException;
29  import org.springframework.http.MediaType;
30  import org.springframework.util.StringUtils;
31  import org.springframework.web.HttpMediaTypeNotSupportedException;
32  import org.springframework.web.bind.annotation.RequestMapping;
33  import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
34  
35  /**
36   * A logical disjunction (' || ') request condition to match a request's
37   * 'Content-Type' header to a list of media type expressions. Two kinds of
38   * media type expressions are supported, which are described in
39   * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()}
40   * where the header name is 'Content-Type'. Regardless of which syntax is
41   * used, the semantics are the same.
42   *
43   * @author Arjen Poutsma
44   * @author Rossen Stoyanchev
45   * @since 3.1
46   */
47  public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> {
48  
49  	private final List<ConsumeMediaTypeExpression> expressions;
50  
51  
52  	/**
53  	 * Creates a new instance from 0 or more "consumes" expressions.
54  	 * @param consumes expressions with the syntax described in
55  	 * {@link RequestMapping#consumes()}; if 0 expressions are provided,
56  	 * the condition will match to every request
57  	 */
58  	public ConsumesRequestCondition(String... consumes) {
59  		this(consumes, null);
60  	}
61  
62  	/**
63  	 * Creates a new instance with "consumes" and "header" expressions.
64  	 * "Header" expressions where the header name is not 'Content-Type' or have
65  	 * no header value defined are ignored. If 0 expressions are provided in
66  	 * total, the condition will match to every request
67  	 * @param consumes as described in {@link RequestMapping#consumes()}
68  	 * @param headers as described in {@link RequestMapping#headers()}
69  	 */
70  	public ConsumesRequestCondition(String[] consumes, String[] headers) {
71  		this(parseExpressions(consumes, headers));
72  	}
73  
74  	/**
75  	 * Private constructor accepting parsed media type expressions.
76  	 */
77  	private ConsumesRequestCondition(Collection<ConsumeMediaTypeExpression> expressions) {
78  		this.expressions = new ArrayList<ConsumeMediaTypeExpression>(expressions);
79  		Collections.sort(this.expressions);
80  	}
81  
82  
83  	private static Set<ConsumeMediaTypeExpression> parseExpressions(String[] consumes, String[] headers) {
84  		Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>();
85  		if (headers != null) {
86  			for (String header : headers) {
87  				HeaderExpression expr = new HeaderExpression(header);
88  				if ("Content-Type".equalsIgnoreCase(expr.name)) {
89  					for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
90  						result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated));
91  					}
92  				}
93  			}
94  		}
95  		if (consumes != null) {
96  			for (String consume : consumes) {
97  				result.add(new ConsumeMediaTypeExpression(consume));
98  			}
99  		}
100 		return result;
101 	}
102 
103 
104 	/**
105 	 * Return the contained MediaType expressions.
106 	 */
107 	public Set<MediaTypeExpression> getExpressions() {
108 		return new LinkedHashSet<MediaTypeExpression>(this.expressions);
109 	}
110 
111 	/**
112 	 * Returns the media types for this condition excluding negated expressions.
113 	 */
114 	public Set<MediaType> getConsumableMediaTypes() {
115 		Set<MediaType> result = new LinkedHashSet<MediaType>();
116 		for (ConsumeMediaTypeExpression expression : this.expressions) {
117 			if (!expression.isNegated()) {
118 				result.add(expression.getMediaType());
119 			}
120 		}
121 		return result;
122 	}
123 
124 	/**
125 	 * Whether the condition has any media type expressions.
126 	 */
127 	public boolean isEmpty() {
128 		return this.expressions.isEmpty();
129 	}
130 
131 	@Override
132 	protected Collection<ConsumeMediaTypeExpression> getContent() {
133 		return this.expressions;
134 	}
135 
136 	@Override
137 	protected String getToStringInfix() {
138 		return " || ";
139 	}
140 
141 	/**
142 	 * Returns the "other" instance if it has any expressions; returns "this"
143 	 * instance otherwise. Practically that means a method-level "consumes"
144 	 * overrides a type-level "consumes" condition.
145 	 */
146 	@Override
147 	public ConsumesRequestCondition combine(ConsumesRequestCondition other) {
148 		return !other.expressions.isEmpty() ? other : this;
149 	}
150 
151 	/**
152 	 * Checks if any of the contained media type expressions match the given
153 	 * request 'Content-Type' header and returns an instance that is guaranteed
154 	 * to contain matching expressions only. The match is performed via
155 	 * {@link MediaType#includes(MediaType)}.
156 	 * @param request the current request
157 	 * @return the same instance if the condition contains no expressions;
158 	 * or a new condition with matching expressions only;
159 	 * or {@code null} if no expressions match.
160 	 */
161 	@Override
162 	public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
163 		if (isEmpty()) {
164 			return this;
165 		}
166 		Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(expressions);
167 		for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
168 			ConsumeMediaTypeExpression expression = iterator.next();
169 			if (!expression.match(request)) {
170 				iterator.remove();
171 			}
172 		}
173 		return (result.isEmpty()) ? null : new ConsumesRequestCondition(result);
174 	}
175 
176 	/**
177 	 * Returns:
178 	 * <ul>
179 	 * <li>0 if the two conditions have the same number of expressions
180 	 * <li>Less than 0 if "this" has more or more specific media type expressions
181 	 * <li>Greater than 0 if "other" has more or more specific media type expressions
182 	 * </ul>
183 	 * <p>It is assumed that both instances have been obtained via
184 	 * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains
185 	 * the matching consumable media type expression only or is otherwise empty.
186 	 */
187 	@Override
188 	public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) {
189 		if (this.expressions.isEmpty() && other.expressions.isEmpty()) {
190 			return 0;
191 		}
192 		else if (this.expressions.isEmpty()) {
193 			return 1;
194 		}
195 		else if (other.expressions.isEmpty()) {
196 			return -1;
197 		}
198 		else {
199 			return this.expressions.get(0).compareTo(other.expressions.get(0));
200 		}
201 	}
202 
203 
204 	/**
205 	 * Parses and matches a single media type expression to a request's 'Content-Type' header.
206 	 */
207 	static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression {
208 
209 		ConsumeMediaTypeExpression(String expression) {
210 			super(expression);
211 		}
212 
213 		ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) {
214 			super(mediaType, negated);
215 		}
216 
217 		@Override
218 		protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
219 			try {
220 				MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
221 						MediaType.parseMediaType(request.getContentType()) :
222 						MediaType.APPLICATION_OCTET_STREAM;
223 						return getMediaType().includes(contentType);
224 			}
225 			catch (InvalidMediaTypeException ex) {
226 				throw new HttpMediaTypeNotSupportedException(
227 						"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
228 			}
229 		}
230 	}
231 
232 }