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