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.InvalidMediaTypeException; 029import org.springframework.http.MediaType; 030import org.springframework.util.StringUtils; 031import org.springframework.web.bind.annotation.RequestMapping; 032import org.springframework.web.cors.CorsUtils; 033import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; 034 035/** 036 * A logical disjunction (' || ') request condition to match a request's 037 * 'Content-Type' header to a list of media type expressions. Two kinds of 038 * media type expressions are supported, which are described in 039 * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()} 040 * where the header name is 'Content-Type'. Regardless of which syntax is 041 * used, the semantics are the same. 042 * 043 * @author Arjen Poutsma 044 * @author Rossen Stoyanchev 045 * @since 3.1 046 */ 047public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> { 048 049 private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition(); 050 051 private final List<ConsumeMediaTypeExpression> expressions; 052 053 054 /** 055 * Creates a new instance from 0 or more "consumes" expressions. 056 * @param consumes expressions with the syntax described in 057 * {@link RequestMapping#consumes()}; if 0 expressions are provided, 058 * the condition will match to every request 059 */ 060 public ConsumesRequestCondition(String... consumes) { 061 this(consumes, null); 062 } 063 064 /** 065 * Creates a new instance with "consumes" and "header" expressions. 066 * "Header" expressions where the header name is not 'Content-Type' or have 067 * no header value defined are ignored. If 0 expressions are provided in 068 * total, the condition will match to every request 069 * @param consumes as described in {@link RequestMapping#consumes()} 070 * @param headers as described in {@link RequestMapping#headers()} 071 */ 072 public ConsumesRequestCondition(String[] consumes, String[] headers) { 073 this(parseExpressions(consumes, headers)); 074 } 075 076 /** 077 * Private constructor accepting parsed media type expressions. 078 */ 079 private ConsumesRequestCondition(Collection<ConsumeMediaTypeExpression> expressions) { 080 this.expressions = new ArrayList<ConsumeMediaTypeExpression>(expressions); 081 Collections.sort(this.expressions); 082 } 083 084 085 private static Set<ConsumeMediaTypeExpression> parseExpressions(String[] consumes, String[] headers) { 086 Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(); 087 if (headers != null) { 088 for (String header : headers) { 089 HeaderExpression expr = new HeaderExpression(header); 090 if ("Content-Type".equalsIgnoreCase(expr.name)) { 091 for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { 092 result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated)); 093 } 094 } 095 } 096 } 097 if (consumes != null) { 098 for (String consume : consumes) { 099 result.add(new ConsumeMediaTypeExpression(consume)); 100 } 101 } 102 return result; 103 } 104 105 106 /** 107 * Return the contained MediaType expressions. 108 */ 109 public Set<MediaTypeExpression> getExpressions() { 110 return new LinkedHashSet<MediaTypeExpression>(this.expressions); 111 } 112 113 /** 114 * Returns the media types for this condition excluding negated expressions. 115 */ 116 public Set<MediaType> getConsumableMediaTypes() { 117 Set<MediaType> result = new LinkedHashSet<MediaType>(); 118 for (ConsumeMediaTypeExpression expression : this.expressions) { 119 if (!expression.isNegated()) { 120 result.add(expression.getMediaType()); 121 } 122 } 123 return result; 124 } 125 126 /** 127 * Whether the condition has any media type expressions. 128 */ 129 @Override 130 public boolean isEmpty() { 131 return this.expressions.isEmpty(); 132 } 133 134 @Override 135 protected Collection<ConsumeMediaTypeExpression> getContent() { 136 return this.expressions; 137 } 138 139 @Override 140 protected String getToStringInfix() { 141 return " || "; 142 } 143 144 /** 145 * Returns the "other" instance if it has any expressions; returns "this" 146 * instance otherwise. Practically that means a method-level "consumes" 147 * overrides a type-level "consumes" condition. 148 */ 149 @Override 150 public ConsumesRequestCondition combine(ConsumesRequestCondition other) { 151 return !other.expressions.isEmpty() ? other : this; 152 } 153 154 /** 155 * Checks if any of the contained media type expressions match the given 156 * request 'Content-Type' header and returns an instance that is guaranteed 157 * to contain matching expressions only. The match is performed via 158 * {@link MediaType#includes(MediaType)}. 159 * @param request the current request 160 * @return the same instance if the condition contains no expressions; 161 * or a new condition with matching expressions only; 162 * or {@code null} if no expressions match 163 */ 164 @Override 165 public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) { 166 if (CorsUtils.isPreFlightRequest(request)) { 167 return PRE_FLIGHT_MATCH; 168 } 169 if (isEmpty()) { 170 return this; 171 } 172 173 MediaType contentType; 174 try { 175 contentType = (StringUtils.hasLength(request.getContentType()) ? 176 MediaType.parseMediaType(request.getContentType()) : 177 MediaType.APPLICATION_OCTET_STREAM); 178 } 179 catch (InvalidMediaTypeException ex) { 180 return null; 181 } 182 183 Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(this.expressions); 184 for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) { 185 ConsumeMediaTypeExpression expression = iterator.next(); 186 if (!expression.match(contentType)) { 187 iterator.remove(); 188 } 189 } 190 return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null); 191 } 192 193 /** 194 * Returns: 195 * <ul> 196 * <li>0 if the two conditions have the same number of expressions 197 * <li>Less than 0 if "this" has more or more specific media type expressions 198 * <li>Greater than 0 if "other" has more or more specific media type expressions 199 * </ul> 200 * <p>It is assumed that both instances have been obtained via 201 * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains 202 * the matching consumable media type expression only or is otherwise empty. 203 */ 204 @Override 205 public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) { 206 if (this.expressions.isEmpty() && other.expressions.isEmpty()) { 207 return 0; 208 } 209 else if (this.expressions.isEmpty()) { 210 return 1; 211 } 212 else if (other.expressions.isEmpty()) { 213 return -1; 214 } 215 else { 216 return this.expressions.get(0).compareTo(other.expressions.get(0)); 217 } 218 } 219 220 221 /** 222 * Parses and matches a single media type expression to a request's 'Content-Type' header. 223 */ 224 static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression { 225 226 ConsumeMediaTypeExpression(String expression) { 227 super(expression); 228 } 229 230 ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) { 231 super(mediaType, negated); 232 } 233 234 public final boolean match(MediaType contentType) { 235 boolean match = getMediaType().includes(contentType); 236 return (!isNegated() ? match : !match); 237 } 238 } 239 240}