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}