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.Collection; 020import java.util.Collections; 021import java.util.LinkedHashSet; 022import java.util.Set; 023 024import javax.servlet.http.HttpServletRequest; 025 026import org.springframework.lang.Nullable; 027import org.springframework.util.ObjectUtils; 028import org.springframework.web.bind.annotation.RequestMapping; 029import org.springframework.web.cors.CorsUtils; 030 031/** 032 * A logical conjunction (' && ') request condition that matches a request against 033 * a set of header expressions with syntax defined in {@link RequestMapping#headers()}. 034 * 035 * <p>Expressions passed to the constructor with header names 'Accept' or 036 * 'Content-Type' are ignored. See {@link ConsumesRequestCondition} and 037 * {@link ProducesRequestCondition} for those. 038 * 039 * @author Arjen Poutsma 040 * @author Rossen Stoyanchev 041 * @since 3.1 042 */ 043public final class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> { 044 045 private static final HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition(); 046 047 048 private final Set<HeaderExpression> expressions; 049 050 051 /** 052 * Create a new instance from the given header expressions. Expressions with 053 * header names 'Accept' or 'Content-Type' are ignored. See {@link ConsumesRequestCondition} 054 * and {@link ProducesRequestCondition} for those. 055 * @param headers media type expressions with syntax defined in {@link RequestMapping#headers()}; 056 * if 0, the condition will match to every request 057 */ 058 public HeadersRequestCondition(String... headers) { 059 this.expressions = parseExpressions(headers); 060 } 061 062 private static Set<HeaderExpression> parseExpressions(String... headers) { 063 Set<HeaderExpression> result = null; 064 if (!ObjectUtils.isEmpty(headers)) { 065 for (String header : headers) { 066 HeaderExpression expr = new HeaderExpression(header); 067 if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { 068 continue; 069 } 070 result = (result != null ? result : new LinkedHashSet<>(headers.length)); 071 result.add(expr); 072 } 073 } 074 return (result != null ? result : Collections.emptySet()); 075 } 076 077 private HeadersRequestCondition(Set<HeaderExpression> conditions) { 078 this.expressions = conditions; 079 } 080 081 082 /** 083 * Return the contained request header expressions. 084 */ 085 public Set<NameValueExpression<String>> getExpressions() { 086 return new LinkedHashSet<>(this.expressions); 087 } 088 089 @Override 090 protected Collection<HeaderExpression> getContent() { 091 return this.expressions; 092 } 093 094 @Override 095 protected String getToStringInfix() { 096 return " && "; 097 } 098 099 /** 100 * Returns a new instance with the union of the header expressions 101 * from "this" and the "other" instance. 102 */ 103 @Override 104 public HeadersRequestCondition combine(HeadersRequestCondition other) { 105 if (isEmpty() && other.isEmpty()) { 106 return this; 107 } 108 else if (other.isEmpty()) { 109 return this; 110 } 111 else if (isEmpty()) { 112 return other; 113 } 114 Set<HeaderExpression> set = new LinkedHashSet<>(this.expressions); 115 set.addAll(other.expressions); 116 return new HeadersRequestCondition(set); 117 } 118 119 /** 120 * Returns "this" instance if the request matches all expressions; 121 * or {@code null} otherwise. 122 */ 123 @Override 124 @Nullable 125 public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) { 126 if (CorsUtils.isPreFlightRequest(request)) { 127 return PRE_FLIGHT_MATCH; 128 } 129 for (HeaderExpression expression : this.expressions) { 130 if (!expression.match(request)) { 131 return null; 132 } 133 } 134 return this; 135 } 136 137 /** 138 * Compare to another condition based on header expressions. A condition 139 * is considered to be a more specific match, if it has: 140 * <ol> 141 * <li>A greater number of expressions. 142 * <li>A greater number of non-negated expressions with a concrete value. 143 * </ol> 144 * <p>It is assumed that both instances have been obtained via 145 * {@link #getMatchingCondition(HttpServletRequest)} and each instance 146 * contains the matching header expression only or is otherwise empty. 147 */ 148 @Override 149 public int compareTo(HeadersRequestCondition other, HttpServletRequest request) { 150 int result = other.expressions.size() - this.expressions.size(); 151 if (result != 0) { 152 return result; 153 } 154 return (int) (getValueMatchCount(other.expressions) - getValueMatchCount(this.expressions)); 155 } 156 157 private long getValueMatchCount(Set<HeaderExpression> expressions) { 158 long count = 0; 159 for (HeaderExpression e : expressions) { 160 if (e.getValue() != null && !e.isNegated()) { 161 count++; 162 } 163 } 164 return count; 165 } 166 167 168 /** 169 * Parses and matches a single header expression to a request. 170 */ 171 static class HeaderExpression extends AbstractNameValueExpression<String> { 172 173 HeaderExpression(String expression) { 174 super(expression); 175 } 176 177 @Override 178 protected boolean isCaseSensitiveName() { 179 return false; 180 } 181 182 @Override 183 protected String parseValue(String valueExpression) { 184 return valueExpression; 185 } 186 187 @Override 188 protected boolean matchName(HttpServletRequest request) { 189 return (request.getHeader(this.name) != null); 190 } 191 192 @Override 193 protected boolean matchValue(HttpServletRequest request) { 194 return ObjectUtils.nullSafeEquals(this.value, request.getHeader(this.name)); 195 } 196 } 197 198}