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