001/* 002 * Copyright 2002-2020 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.LinkedHashSet; 023import java.util.List; 024import java.util.Set; 025 026import javax.servlet.http.HttpServletRequest; 027 028import org.springframework.http.HttpHeaders; 029import org.springframework.http.InvalidMediaTypeException; 030import org.springframework.http.MediaType; 031import org.springframework.lang.Nullable; 032import org.springframework.util.CollectionUtils; 033import org.springframework.util.ObjectUtils; 034import org.springframework.util.StringUtils; 035import org.springframework.web.bind.annotation.RequestMapping; 036import org.springframework.web.cors.CorsUtils; 037import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; 038 039/** 040 * A logical disjunction (' || ') request condition to match a request's 041 * 'Content-Type' header to a list of media type expressions. Two kinds of 042 * media type expressions are supported, which are described in 043 * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()} 044 * where the header name is 'Content-Type'. Regardless of which syntax is 045 * used, the semantics are the same. 046 * 047 * @author Arjen Poutsma 048 * @author Rossen Stoyanchev 049 * @since 3.1 050 */ 051public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> { 052 053 private static final ConsumesRequestCondition EMPTY_CONDITION = new ConsumesRequestCondition(); 054 055 056 private final List<ConsumeMediaTypeExpression> expressions; 057 058 private boolean bodyRequired = true; 059 060 061 /** 062 * Creates a new instance from 0 or more "consumes" expressions. 063 * @param consumes expressions with the syntax described in 064 * {@link RequestMapping#consumes()}; if 0 expressions are provided, 065 * the condition will match to every request 066 */ 067 public ConsumesRequestCondition(String... consumes) { 068 this(consumes, null); 069 } 070 071 /** 072 * Creates a new instance with "consumes" and "header" expressions. 073 * "Header" expressions where the header name is not 'Content-Type' or have 074 * no header value defined are ignored. If 0 expressions are provided in 075 * total, the condition will match to every request 076 * @param consumes as described in {@link RequestMapping#consumes()} 077 * @param headers as described in {@link RequestMapping#headers()} 078 */ 079 public ConsumesRequestCondition(String[] consumes, @Nullable String[] headers) { 080 this.expressions = parseExpressions(consumes, headers); 081 if (this.expressions.size() > 1) { 082 Collections.sort(this.expressions); 083 } 084 } 085 086 private static List<ConsumeMediaTypeExpression> parseExpressions(String[] consumes, @Nullable String[] headers) { 087 Set<ConsumeMediaTypeExpression> result = null; 088 if (!ObjectUtils.isEmpty(headers)) { 089 for (String header : headers) { 090 HeaderExpression expr = new HeaderExpression(header); 091 if ("Content-Type".equalsIgnoreCase(expr.name) && expr.value != null) { 092 result = (result != null ? result : new LinkedHashSet<>()); 093 for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { 094 result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated)); 095 } 096 } 097 } 098 } 099 if (!ObjectUtils.isEmpty(consumes)) { 100 result = (result != null ? result : new LinkedHashSet<>()); 101 for (String consume : consumes) { 102 result.add(new ConsumeMediaTypeExpression(consume)); 103 } 104 } 105 return (result != null ? new ArrayList<>(result) : Collections.emptyList()); 106 } 107 108 /** 109 * Private constructor for internal when creating matching conditions. 110 * Note the expressions List is neither sorted nor deep copied. 111 */ 112 private ConsumesRequestCondition(List<ConsumeMediaTypeExpression> expressions) { 113 this.expressions = expressions; 114 } 115 116 117 /** 118 * Return the contained MediaType expressions. 119 */ 120 public Set<MediaTypeExpression> getExpressions() { 121 return new LinkedHashSet<>(this.expressions); 122 } 123 124 /** 125 * Returns the media types for this condition excluding negated expressions. 126 */ 127 public Set<MediaType> getConsumableMediaTypes() { 128 Set<MediaType> result = new LinkedHashSet<>(); 129 for (ConsumeMediaTypeExpression expression : this.expressions) { 130 if (!expression.isNegated()) { 131 result.add(expression.getMediaType()); 132 } 133 } 134 return result; 135 } 136 137 /** 138 * Whether the condition has any media type expressions. 139 */ 140 @Override 141 public boolean isEmpty() { 142 return this.expressions.isEmpty(); 143 } 144 145 @Override 146 protected Collection<ConsumeMediaTypeExpression> getContent() { 147 return this.expressions; 148 } 149 150 @Override 151 protected String getToStringInfix() { 152 return " || "; 153 } 154 155 /** 156 * Whether this condition should expect requests to have a body. 157 * <p>By default this is set to {@code true} in which case it is assumed a 158 * request body is required and this condition matches to the "Content-Type" 159 * header or falls back on "Content-Type: application/octet-stream". 160 * <p>If set to {@code false}, and the request does not have a body, then this 161 * condition matches automatically, i.e. without checking expressions. 162 * @param bodyRequired whether requests are expected to have a body 163 * @since 5.2 164 */ 165 public void setBodyRequired(boolean bodyRequired) { 166 this.bodyRequired = bodyRequired; 167 } 168 169 /** 170 * Return the setting for {@link #setBodyRequired(boolean)}. 171 * @since 5.2 172 */ 173 public boolean isBodyRequired() { 174 return this.bodyRequired; 175 } 176 177 178 /** 179 * Returns the "other" instance if it has any expressions; returns "this" 180 * instance otherwise. Practically that means a method-level "consumes" 181 * overrides a type-level "consumes" condition. 182 */ 183 @Override 184 public ConsumesRequestCondition combine(ConsumesRequestCondition other) { 185 return (!other.expressions.isEmpty() ? other : this); 186 } 187 188 /** 189 * Checks if any of the contained media type expressions match the given 190 * request 'Content-Type' header and returns an instance that is guaranteed 191 * to contain matching expressions only. The match is performed via 192 * {@link MediaType#includes(MediaType)}. 193 * @param request the current request 194 * @return the same instance if the condition contains no expressions; 195 * or a new condition with matching expressions only; 196 * or {@code null} if no expressions match 197 */ 198 @Override 199 @Nullable 200 public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) { 201 if (CorsUtils.isPreFlightRequest(request)) { 202 return EMPTY_CONDITION; 203 } 204 if (isEmpty()) { 205 return this; 206 } 207 if (!hasBody(request) && !this.bodyRequired) { 208 return EMPTY_CONDITION; 209 } 210 211 // Common media types are cached at the level of MimeTypeUtils 212 213 MediaType contentType; 214 try { 215 contentType = StringUtils.hasLength(request.getContentType()) ? 216 MediaType.parseMediaType(request.getContentType()) : 217 MediaType.APPLICATION_OCTET_STREAM; 218 } 219 catch (InvalidMediaTypeException ex) { 220 return null; 221 } 222 223 List<ConsumeMediaTypeExpression> result = getMatchingExpressions(contentType); 224 return !CollectionUtils.isEmpty(result) ? new ConsumesRequestCondition(result) : null; 225 } 226 227 private boolean hasBody(HttpServletRequest request) { 228 String contentLength = request.getHeader(HttpHeaders.CONTENT_LENGTH); 229 String transferEncoding = request.getHeader(HttpHeaders.TRANSFER_ENCODING); 230 return StringUtils.hasText(transferEncoding) || 231 (StringUtils.hasText(contentLength) && !contentLength.trim().equals("0")); 232 } 233 234 @Nullable 235 private List<ConsumeMediaTypeExpression> getMatchingExpressions(MediaType contentType) { 236 List<ConsumeMediaTypeExpression> result = null; 237 for (ConsumeMediaTypeExpression expression : this.expressions) { 238 if (expression.match(contentType)) { 239 result = result != null ? result : new ArrayList<>(); 240 result.add(expression); 241 } 242 } 243 return result; 244 } 245 246 /** 247 * Returns: 248 * <ul> 249 * <li>0 if the two conditions have the same number of expressions 250 * <li>Less than 0 if "this" has more or more specific media type expressions 251 * <li>Greater than 0 if "other" has more or more specific media type expressions 252 * </ul> 253 * <p>It is assumed that both instances have been obtained via 254 * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains 255 * the matching consumable media type expression only or is otherwise empty. 256 */ 257 @Override 258 public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) { 259 if (this.expressions.isEmpty() && other.expressions.isEmpty()) { 260 return 0; 261 } 262 else if (this.expressions.isEmpty()) { 263 return 1; 264 } 265 else if (other.expressions.isEmpty()) { 266 return -1; 267 } 268 else { 269 return this.expressions.get(0).compareTo(other.expressions.get(0)); 270 } 271 } 272 273 274 /** 275 * Parses and matches a single media type expression to a request's 'Content-Type' header. 276 */ 277 static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression { 278 279 ConsumeMediaTypeExpression(String expression) { 280 super(expression); 281 } 282 283 ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) { 284 super(mediaType, negated); 285 } 286 287 public final boolean match(MediaType contentType) { 288 boolean match = getMediaType().includes(contentType); 289 return !isNegated() == match; 290 } 291 } 292 293}