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.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Set;
025import java.util.SortedSet;
026import java.util.TreeSet;
027
028import org.springframework.http.server.PathContainer;
029import org.springframework.lang.Nullable;
030import org.springframework.util.ObjectUtils;
031import org.springframework.web.server.ServerWebExchange;
032import org.springframework.web.util.pattern.PathPattern;
033import org.springframework.web.util.pattern.PathPatternParser;
034
035/**
036 * A logical disjunction (' || ') request condition that matches a request
037 * against a set of URL path patterns.
038 *
039 * @author Rossen Stoyanchev
040 * @author Brian Clozel
041 * @since 5.0
042 */
043public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
044
045        private static final SortedSet<PathPattern> EMPTY_PATH_PATTERN =
046                        new TreeSet<>(Collections.singleton(PathPatternParser.defaultInstance.parse("")));
047
048
049        private final SortedSet<PathPattern> patterns;
050
051
052        /**
053         * Creates a new instance with the given URL patterns.
054         * @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
055         */
056        public PatternsRequestCondition(PathPattern... patterns) {
057                this(ObjectUtils.isEmpty(patterns) ? Collections.emptyList() : Arrays.asList(patterns));
058        }
059
060        /**
061         * Creates a new instance with the given URL patterns.
062         */
063        public PatternsRequestCondition(List<PathPattern> patterns) {
064                this.patterns = (patterns.isEmpty() ? EMPTY_PATH_PATTERN : new TreeSet<>(patterns));
065        }
066
067        private PatternsRequestCondition(SortedSet<PathPattern> patterns) {
068                this.patterns = patterns;
069        }
070
071
072        public Set<PathPattern> getPatterns() {
073                return this.patterns;
074        }
075
076        @Override
077        protected Collection<PathPattern> getContent() {
078                return this.patterns;
079        }
080
081        @Override
082        protected String getToStringInfix() {
083                return " || ";
084        }
085
086        /**
087         * Returns a new instance with URL patterns from the current instance ("this") and
088         * the "other" instance as follows:
089         * <ul>
090         * <li>If there are patterns in both instances, combine the patterns in "this" with
091         * the patterns in "other" using {@link PathPattern#combine(PathPattern)}.
092         * <li>If only one instance has patterns, use them.
093         * <li>If neither instance has patterns, use an empty String (i.e. "").
094         * </ul>
095         */
096        @Override
097        public PatternsRequestCondition combine(PatternsRequestCondition other) {
098                if (isEmptyPathPattern() && other.isEmptyPathPattern()) {
099                        return this;
100                }
101                else if (other.isEmptyPathPattern()) {
102                        return this;
103                }
104                else if (isEmptyPathPattern()) {
105                        return other;
106                }
107                else {
108                        SortedSet<PathPattern> combined = new TreeSet<>();
109                        for (PathPattern pattern1 : this.patterns) {
110                                for (PathPattern pattern2 : other.patterns) {
111                                        combined.add(pattern1.combine(pattern2));
112                                }
113                        }
114                        return new PatternsRequestCondition(combined);
115                }
116        }
117
118        private boolean isEmptyPathPattern() {
119                return this.patterns == EMPTY_PATH_PATTERN;
120        }
121
122        /**
123         * Checks if any of the patterns match the given request and returns an instance
124         * that is guaranteed to contain matching patterns, sorted.
125         * @param exchange the current exchange
126         * @return the same instance if the condition contains no patterns;
127         * or a new condition with sorted matching patterns;
128         * or {@code null} if no patterns match.
129         */
130        @Override
131        @Nullable
132        public PatternsRequestCondition getMatchingCondition(ServerWebExchange exchange) {
133                SortedSet<PathPattern> matches = getMatchingPatterns(exchange);
134                return (matches != null ? new PatternsRequestCondition(matches) : null);
135        }
136
137        @Nullable
138        private SortedSet<PathPattern> getMatchingPatterns(ServerWebExchange exchange) {
139                PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication();
140                TreeSet<PathPattern> result = null;
141                for (PathPattern pattern : this.patterns) {
142                        if (pattern.matches(lookupPath)) {
143                                result = (result != null ? result : new TreeSet<>());
144                                result.add(pattern);
145                        }
146                }
147                return result;
148        }
149
150        /**
151         * Compare the two conditions based on the URL patterns they contain.
152         * Patterns are compared one at a time, from top to bottom. If all compared
153         * patterns match equally, but one instance has more patterns, it is
154         * considered a closer match.
155         * <p>It is assumed that both instances have been obtained via
156         * {@link #getMatchingCondition(ServerWebExchange)} to ensure they
157         * contain only patterns that match the request and are sorted with
158         * the best matches on top.
159         */
160        @Override
161        public int compareTo(PatternsRequestCondition other, ServerWebExchange exchange) {
162                Iterator<PathPattern> iterator = this.patterns.iterator();
163                Iterator<PathPattern> iteratorOther = other.getPatterns().iterator();
164                while (iterator.hasNext() && iteratorOther.hasNext()) {
165                        int result = PathPattern.SPECIFICITY_COMPARATOR.compare(iterator.next(), iteratorOther.next());
166                        if (result != 0) {
167                                return result;
168                        }
169                }
170                if (iterator.hasNext()) {
171                        return -1;
172                }
173                else if (iteratorOther.hasNext()) {
174                        return 1;
175                }
176                else {
177                        return 0;
178                }
179        }
180
181}