001/*
002 * Copyright 2002-2019 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.messaging.handler;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.Iterator;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Set;
027
028import org.springframework.lang.Nullable;
029import org.springframework.messaging.Message;
030import org.springframework.util.AntPathMatcher;
031import org.springframework.util.CollectionUtils;
032import org.springframework.util.PathMatcher;
033import org.springframework.util.RouteMatcher;
034import org.springframework.util.SimpleRouteMatcher;
035import org.springframework.util.StringUtils;
036
037/**
038 * {@link MessageCondition} to match the destination header of a Message
039 * against one or more patterns through a {@link RouteMatcher}.
040 *
041 * @author Rossen Stoyanchev
042 * @since 4.0
043 */
044public class DestinationPatternsMessageCondition
045                extends AbstractMessageCondition<DestinationPatternsMessageCondition> {
046
047        /**
048         * The name of the "lookup destination" header.
049         */
050        public static final String LOOKUP_DESTINATION_HEADER = "lookupDestination";
051
052
053        private final Set<String> patterns;
054
055        private final RouteMatcher routeMatcher;
056
057
058        /**
059         * Constructor with patterns only. Creates and uses an instance of
060         * {@link AntPathMatcher} with default settings.
061         * <p>Non-empty patterns that don't start with "/" are prepended with "/".
062         * @param patterns the URL patterns to match to, or if 0 then always match
063         */
064        public DestinationPatternsMessageCondition(String... patterns) {
065                this(patterns, (PathMatcher) null);
066        }
067
068        /**
069         * Constructor with patterns and a {@code PathMatcher} instance.
070         * @param patterns the URL patterns to match to, or if 0 then always match
071         * @param matcher the {@code PathMatcher} to use
072         */
073        public DestinationPatternsMessageCondition(String[] patterns, @Nullable PathMatcher matcher) {
074                this(patterns, new SimpleRouteMatcher(matcher != null ? matcher : new AntPathMatcher()));
075        }
076
077        /**
078         * Constructor with patterns and a {@code RouteMatcher} instance.
079         * @param patterns the URL patterns to match to, or if 0 then always match
080         * @param routeMatcher the {@code RouteMatcher} to use
081         * @since 5.2
082         */
083        public DestinationPatternsMessageCondition(String[] patterns, RouteMatcher routeMatcher) {
084                this(Collections.unmodifiableSet(prependLeadingSlash(patterns, routeMatcher)), routeMatcher);
085        }
086
087        private static Set<String> prependLeadingSlash(String[] patterns, RouteMatcher routeMatcher) {
088                boolean slashSeparator = routeMatcher.combine("a", "a").equals("a/a");
089                Set<String> result = new LinkedHashSet<>(patterns.length);
090                for (String pattern : patterns) {
091                        if (slashSeparator && StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
092                                pattern = "/" + pattern;
093                        }
094                        result.add(pattern);
095                }
096                return result;
097        }
098
099        private DestinationPatternsMessageCondition(Set<String> patterns, RouteMatcher routeMatcher) {
100                this.patterns = patterns;
101                this.routeMatcher = routeMatcher;
102        }
103
104
105
106        public Set<String> getPatterns() {
107                return this.patterns;
108        }
109
110        @Override
111        protected Collection<String> getContent() {
112                return this.patterns;
113        }
114
115        @Override
116        protected String getToStringInfix() {
117                return " || ";
118        }
119
120
121        /**
122         * Returns a new instance with URL patterns from the current instance ("this") and
123         * the "other" instance as follows:
124         * <ul>
125         * <li>If there are patterns in both instances, combine the patterns in "this" with
126         * the patterns in "other" using {@link org.springframework.util.PathMatcher#combine(String, String)}.
127         * <li>If only one instance has patterns, use them.
128         * <li>If neither instance has patterns, use an empty String (i.e. "").
129         * </ul>
130         */
131        @Override
132        public DestinationPatternsMessageCondition combine(DestinationPatternsMessageCondition other) {
133                Set<String> result = new LinkedHashSet<>();
134                if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
135                        for (String pattern1 : this.patterns) {
136                                for (String pattern2 : other.patterns) {
137                                        result.add(this.routeMatcher.combine(pattern1, pattern2));
138                                }
139                        }
140                }
141                else if (!this.patterns.isEmpty()) {
142                        result.addAll(this.patterns);
143                }
144                else if (!other.patterns.isEmpty()) {
145                        result.addAll(other.patterns);
146                }
147                else {
148                        result.add("");
149                }
150                return new DestinationPatternsMessageCondition(result, this.routeMatcher);
151        }
152
153        /**
154         * Check if any of the patterns match the given Message destination and return an instance
155         * that is guaranteed to contain matching patterns, sorted via
156         * {@link org.springframework.util.PathMatcher#getPatternComparator(String)}.
157         * @param message the message to match to
158         * @return the same instance if the condition contains no patterns;
159         * or a new condition with sorted matching patterns;
160         * or {@code null} either if a destination can not be extracted or there is no match
161         */
162        @Override
163        @Nullable
164        public DestinationPatternsMessageCondition getMatchingCondition(Message<?> message) {
165                Object destination = message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
166                if (destination == null) {
167                        return null;
168                }
169                if (this.patterns.isEmpty()) {
170                        return this;
171                }
172
173                List<String> matches = null;
174                for (String pattern : this.patterns) {
175                        if (pattern.equals(destination) || matchPattern(pattern, destination)) {
176                                if (matches == null) {
177                                        matches = new ArrayList<>();
178                                }
179                                matches.add(pattern);
180                        }
181                }
182                if (CollectionUtils.isEmpty(matches)) {
183                        return null;
184                }
185
186                matches.sort(getPatternComparator(destination));
187                return new DestinationPatternsMessageCondition(new LinkedHashSet<>(matches), this.routeMatcher);
188        }
189
190        private boolean matchPattern(String pattern, Object destination) {
191                return destination instanceof RouteMatcher.Route ?
192                                this.routeMatcher.match(pattern, (RouteMatcher.Route) destination) :
193                                ((SimpleRouteMatcher) this.routeMatcher).getPathMatcher().match(pattern, (String) destination);
194        }
195
196        private Comparator<String> getPatternComparator(Object destination) {
197                return destination instanceof RouteMatcher.Route ?
198                        this.routeMatcher.getPatternComparator((RouteMatcher.Route) destination) :
199                        ((SimpleRouteMatcher) this.routeMatcher).getPathMatcher().getPatternComparator((String) destination);
200        }
201
202        /**
203         * Compare the two conditions based on the destination patterns they contain.
204         * Patterns are compared one at a time, from top to bottom via
205         * {@link org.springframework.util.PathMatcher#getPatternComparator(String)}.
206         * If all compared patterns match equally, but one instance has more patterns,
207         * it is considered a closer match.
208         * <p>It is assumed that both instances have been obtained via
209         * {@link #getMatchingCondition(Message)} to ensure they contain only patterns
210         * that match the request and are sorted with the best matches on top.
211         */
212        @Override
213        public int compareTo(DestinationPatternsMessageCondition other, Message<?> message) {
214                Object destination = message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
215                if (destination == null) {
216                        return 0;
217                }
218
219                Comparator<String> patternComparator = getPatternComparator(destination);
220                Iterator<String> iterator = this.patterns.iterator();
221                Iterator<String> iteratorOther = other.patterns.iterator();
222                while (iterator.hasNext() && iteratorOther.hasNext()) {
223                        int result = patternComparator.compare(iterator.next(), iteratorOther.next());
224                        if (result != 0) {
225                                return result;
226                        }
227                }
228
229                if (iterator.hasNext()) {
230                        return -1;
231                }
232                else if (iteratorOther.hasNext()) {
233                        return 1;
234                }
235                else {
236                        return 0;
237                }
238        }
239
240}