1 /*
2  * Copyright 2002-2018 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 module hunt.stomp.handler.DestinationPatternsMessageCondition;
18 
19 import hunt.stomp.handler.AbstractMessageCondition;
20 import hunt.stomp.Message;
21 
22 import hunt.collection;
23 import hunt.Nullable;
24 import hunt.text.PathMatcher;
25 
26 import std.array;
27 import std.string;
28 
29 /**
30  * A {@link MessageCondition} for matching the destination of a Message
31  * against one or more destination patterns using a {@link PathMatcher}.
32  *
33  * @author Rossen Stoyanchev
34  * @since 4.0
35  */
36 class DestinationPatternsMessageCondition
37 		: AbstractMessageCondition!(DestinationPatternsMessageCondition, string) {
38 
39 	/**
40 	 * The name of the "lookup destination" header.
41 	 */
42 	enum string LOOKUP_DESTINATION_HEADER = "lookupDestination";
43 
44 
45 	private Set!(string) patterns;
46 
47 	private PathMatcher pathMatcher;
48 
49 
50 	/**
51 	 * Creates a new instance with the given destination patterns.
52 	 * Each pattern that is not empty and does not start with "/" is prepended with "/".
53 	 * @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
54 	 */
55 	this(string[] patterns...) {
56 		this(patterns.dup, null);
57 	}
58 
59 	/**
60 	 * Alternative constructor accepting a custom PathMatcher.
61 	 * @param patterns the URL patterns to use; if 0, the condition will match to every request.
62 	 * @param pathMatcher the PathMatcher to use
63 	 */
64 	this(string[] patterns, PathMatcher pathMatcher) {
65 		this(new ArrayList!(string)(patterns), pathMatcher);
66 	}
67 
68 	private this(Collection!(string) patterns, PathMatcher pathMatcher) {
69 		this.pathMatcher = (pathMatcher !is null ? pathMatcher : new AntPathMatcher());
70 		this.patterns = prependLeadingSlash(patterns, this.pathMatcher);
71 	}
72 
73 
74 	private static Set!(string) prependLeadingSlash(Collection!(string) patterns, PathMatcher pathMatcher) {
75 		bool slashSeparator = pathMatcher.combine("a", "a") == ("a/a");
76 		Set!(string) result = new LinkedHashSet!(string)(patterns.size());
77 		foreach (string pattern ; patterns) {
78 			if (slashSeparator && !pattern.empty() && !pattern.startsWith("/")) {
79 				pattern = "/" ~ pattern;
80 			}
81 			result.add(pattern);
82 		}
83 		return result;
84 	}
85 
86 
87 	Set!(string) getPatterns() {
88 		return this.patterns;
89 	}
90 
91 	override
92 	protected Collection!(string) getContent() {
93 		return this.patterns;
94 	}
95 
96 	override
97 	protected string getToStringInfix() {
98 		return " || ";
99 	}
100 
101 
102 	/**
103 	 * Returns a new instance with URL patterns from the current instance ("this") and
104 	 * the "other" instance as follows:
105 	 * <ul>
106 	 * <li>If there are patterns in both instances, combine the patterns in "this" with
107 	 * the patterns in "other" using {@link hunt.framework.util.PathMatcher#combine(string, string)}.
108 	 * <li>If only one instance has patterns, use them.
109 	 * <li>If neither instance has patterns, use an empty string (i.e. "").
110 	 * </ul>
111 	 */
112 	// override
113 	DestinationPatternsMessageCondition combine(DestinationPatternsMessageCondition other) {
114 		Set!(string) result = new LinkedHashSet!(string)();
115 		if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
116 			foreach (string pattern1 ; this.patterns) {
117 				foreach (string pattern2 ; other.patterns) {
118 					result.add(this.pathMatcher.combine(pattern1, pattern2));
119 				}
120 			}
121 		}
122 		else if (!this.patterns.isEmpty()) {
123 			result.addAll(this.patterns);
124 		}
125 		else if (!other.patterns.isEmpty()) {
126 			result.addAll(other.patterns);
127 		}
128 		else {
129 			result.add("");
130 		}
131 		return new DestinationPatternsMessageCondition(result, this.pathMatcher);
132 	}
133 
134 	/**
135 	 * Check if any of the patterns match the given Message destination and return an instance
136 	 * that is guaranteed to contain matching patterns, sorted via
137 	 * {@link hunt.framework.util.PathMatcher#getPatternComparator(string)}.
138 	 * @param message the message to match to
139 	 * @return the same instance if the condition contains no patterns;
140 	 * or a new condition with sorted matching patterns;
141 	 * or {@code null} either if a destination can not be extracted or there is no match
142 	 */
143 	// override	
144 	DestinationPatternsMessageCondition getMatchingCondition(MessageBase message) {
145 		string destination = cast(string)cast(Nullable!string)message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
146 		if (destination is null) {
147 			return null;
148 		}
149 		if (this.patterns.isEmpty()) {
150 			return this;
151 		}
152 
153 		List!(string) matches = new ArrayList!(string)();
154 		foreach (string pattern ; this.patterns) {
155 			if (pattern == destination || this.pathMatcher.match(pattern, destination)) {
156 				matches.add(pattern);
157 			}
158 		}
159 		if (matches.isEmpty()) {
160 			return null;
161 		}
162 
163 		// TODO: Tasks pending completion -@zxp at 10/31/2018, 10:08:57 AM
164 		// 
165 		// matches.sort(this.pathMatcher.getPatternComparator(destination));
166 		return new DestinationPatternsMessageCondition(matches, this.pathMatcher);
167 	}
168 
169 	/**
170 	 * Compare the two conditions based on the destination patterns they contain.
171 	 * Patterns are compared one at a time, from top to bottom via
172 	 * {@link hunt.framework.util.PathMatcher#getPatternComparator(string)}.
173 	 * If all compared patterns match equally, but one instance has more patterns,
174 	 * it is considered a closer match.
175 	 * <p>It is assumed that both instances have been obtained via
176 	 * {@link #getMatchingCondition(Message)} to ensure they contain only patterns
177 	 * that match the request and are sorted with the best matches on top.
178 	 */
179 	// override
180 	int compareTo(DestinationPatternsMessageCondition other, MessageBase message) {
181 		string destination = cast(string)cast(Nullable!string)message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
182 		if (destination is null) {
183 			return 0;
184 		}
185 
186 // TODO: Tasks pending completion -@zxp at 10/31/2018, 10:09:43 AM
187 // 
188 		return 1;
189 
190 		// Comparator!(string) patternComparator = this.pathMatcher.getPatternComparator(destination);
191 		// Iterator!(string) iterator = this.patterns.iterator();
192 		// Iterator!(string) iteratorOther = other.patterns.iterator();
193 		// while (iterator.hasNext() && iteratorOther.hasNext()) {
194 		// 	int result = patternComparator.compare(iterator.next(), iteratorOther.next());
195 		// 	if (result != 0) {
196 		// 		return result;
197 		// 	}
198 		// }
199 
200 		// if (iterator.hasNext()) {
201 		// 	return -1;
202 		// }
203 		// else if (iteratorOther.hasNext()) {
204 		// 	return 1;
205 		// }
206 		// else {
207 		// 	return 0;
208 		// }
209 	}
210 
211 }