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