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.getMediaType().compareTo(expr2.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}