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