Skip to content

Commit 7466e15

Browse files
committed
add generic name matcher for commands, queries and notifications
1 parent 7020d43 commit 7466e15

File tree

7 files changed

+132
-68
lines changed

7 files changed

+132
-68
lines changed

async/async-commons/async-commons.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ dependencies {
1010
compileOnly 'io.projectreactor:reactor-core'
1111
api 'io.projectreactor.rabbitmq:reactor-rabbitmq'
1212
api 'com.fasterxml.jackson.core:jackson-databind'
13+
implementation 'commons-io:commons-io:2.11.0'
14+
1315
testImplementation 'io.projectreactor:reactor-test'
1416
}
Lines changed: 30 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,46 @@
11
package org.reactivecommons.async.commons.utils.matcher;
22

3-
import lombok.Data;
4-
import reactor.util.function.Tuple2;
5-
import reactor.util.function.Tuples;
3+
import org.apache.commons.io.FilenameUtils;
64

75
import java.util.Set;
8-
import java.util.stream.IntStream;
96

10-
@Data
117
public class KeyMatcher implements Matcher {
12-
private static final String WILD_CARD = "*";
13-
private static final String SEGMENT_DELIMITER_REGEX = "\\.";
148

15-
private String[] getSegments(String s) {
16-
return s.split(SEGMENT_DELIMITER_REGEX);
17-
}
18-
19-
private boolean isBoundedToTarget(String[] current, String[] target) {
20-
final String lastSegment = current[current.length -1];
21-
return (current.length == target.length ||
22-
(current.length < target.length && WILD_CARD.equals(lastSegment)));
23-
}
24-
25-
private boolean isRootSegmentCoincident(String[] current, String[] target) {
26-
final String currentFirstSegment = current[0];
27-
final String targetFirstSegment = target[0];
28-
return currentFirstSegment.equalsIgnoreCase(targetFirstSegment);
29-
}
30-
31-
private boolean isCandidate(String[] current, String[] target ) {
32-
return isRootSegmentCoincident(current, target) &&
33-
isBoundedToTarget(current,target);
34-
}
9+
public static final String SEPARATOR_REGEX = "\\.";
10+
public static final String WILDCARD_CHAR = "*";
3511

36-
private long calculateMatchingScore(String[] current, String[] target) {
37-
return IntStream.range(0, Math.min(current.length, target.length))
38-
.filter(segment -> current[segment].equals(WILD_CARD) ||
39-
current[segment].equalsIgnoreCase(target[segment]))
40-
.count();
41-
}
42-
43-
private long getMatchingScore(String[] current, String[] target) {
44-
final long totalMatches = calculateMatchingScore(current,target);
45-
final boolean allSegmentsMatched = totalMatches == current.length;
46-
return allSegmentsMatched ? totalMatches: 0;
12+
@Override
13+
public String match(Set<String> sources, String target) {
14+
return sources.contains(target) || sources.isEmpty() ?
15+
target :
16+
matchMissingKey(sources, target);
4717
}
4818

49-
private Candidate getScoredCandidate(Tuple2<String,String[]> candidate, String[] target) {
50-
final long matchingScore = getMatchingScore(candidate.getT2(), target);
51-
return new Candidate(candidate.getT1(), matchingScore);
19+
private String matchMissingKey(Set<String> names, String target) {
20+
return names.stream()
21+
.filter(name -> name.contains(WILDCARD_CHAR))
22+
.sorted(this::compare)
23+
.filter(name -> FilenameUtils.wildcardMatch(target, name))
24+
.findFirst()
25+
.orElse(target);
5226
}
5327

54-
private String matchMissingKey(Set<String> sources, String target ) {
55-
final String[] targetSegments = getSegments(target);
56-
return sources.stream()
57-
.map(source -> Tuples.of(source, getSegments(source)))
58-
.filter(source -> isCandidate(source.getT2(), targetSegments))
59-
.map(candidate -> getScoredCandidate(candidate, targetSegments))
60-
.max(Candidate::compareTo)
61-
.map(Candidate::getKey)
62-
.orElse(target);
28+
private int compare(String firstExpression, String secondExpression) {
29+
String[] firstExpressionArr = getSeparated(firstExpression);
30+
String[] secondExpressionArr = getSeparated(secondExpression);
31+
for (int i = 0; i < firstExpressionArr.length && i < secondExpressionArr.length; i++) {
32+
if (firstExpressionArr[i].equals(secondExpressionArr[i])) {
33+
continue;
34+
}
35+
if (firstExpressionArr[i].equals(WILDCARD_CHAR)) {
36+
return 1;
37+
}
38+
return -1;
39+
}
40+
return secondExpressionArr.length - firstExpressionArr.length;
6341
}
6442

65-
public String match(Set<String> sources, String target ) {
66-
return sources.contains(target) || sources.isEmpty() ?
67-
target :
68-
matchMissingKey(sources,target);
43+
private String[] getSeparated(String expression) {
44+
return expression.split(SEPARATOR_REGEX);
6945
}
7046
}

async/async-commons/src/test/java/org/reactivecommons/async/commons/utils/matcher/KeyMatcherTest.java

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@
1111

1212

1313
class KeyMatcherTest {
14-
private KeyMatcher keyMatcher;
14+
private Matcher keyMatcher;
1515
private Set<String> listeners;
1616

1717
@BeforeEach
1818
public void init() {
19-
keyMatcher = new KeyMatcher();
19+
keyMatcher = new KeyMatcher();
2020
listeners = new HashSet<>();
2121
listeners.add("A.*");
2222
listeners.add("A.B");
2323
listeners.add("A.B.*");
2424
listeners.add("A.B.C");
2525
listeners.add("A.B.*.D");
2626
listeners.add("A.B.C.D");
27+
listeners.add("W.X.Y");
28+
listeners.add("app.event-prefix.any");
2729
}
2830

2931
@Test
@@ -81,4 +83,63 @@ void matchDefaultForNonExistentSecondLevel() {
8183
final String match = keyMatcher.match(listeners, nonExistentTarget);
8284
assertEquals("A.B.*", match);
8385
}
84-
}
86+
87+
@Test
88+
void shouldNotMatch() {
89+
String nonExistentTarget = "X.Y.Z";
90+
final String match = keyMatcher.match(listeners, nonExistentTarget);
91+
assertEquals(nonExistentTarget, match);
92+
}
93+
94+
@Test
95+
void shouldNotMatchWhenNoWildcard() {
96+
String nonExistentTarget = "W.X.Y.Z";
97+
final String match = keyMatcher.match(listeners, nonExistentTarget);
98+
assertEquals(nonExistentTarget, match);
99+
}
100+
101+
@Test
102+
void shouldNotMatchWhenNoWildcardSameLength() {
103+
String nonExistentTarget = "app.event.test";
104+
final String match = keyMatcher.match(listeners, nonExistentTarget);
105+
assertEquals(nonExistentTarget, match);
106+
}
107+
108+
@Test
109+
void shouldApplyPriority() {
110+
listeners = new HashSet<>();
111+
listeners.add("*.*.*");
112+
listeners.add("prefix.*.*");
113+
listeners.add("*.middle.*");
114+
listeners.add("*.*.suffix");
115+
listeners.add("*.middle.suffix");
116+
listeners.add("prefix.*.suffix");
117+
listeners.add("prefix.middle.*");
118+
listeners.add("prefix.middle.suffix");
119+
listeners.add("prefix.other.other");
120+
listeners.add("other.middle.other");
121+
listeners.add("other.other.suffix");
122+
listeners.add("other.other.other");
123+
listeners.add("in.depend.ent");
124+
assertEquals("in.depend.ent", keyMatcher.match(listeners, "in.depend.ent"));
125+
assertEquals("other.other.other", keyMatcher.match(listeners, "other.other.other"));
126+
assertEquals("other.other.suffix", keyMatcher.match(listeners, "other.other.suffix"));
127+
assertEquals("other.middle.other", keyMatcher.match(listeners, "other.middle.other"));
128+
assertEquals("prefix.other.other", keyMatcher.match(listeners, "prefix.other.other"));
129+
assertEquals("prefix.middle.suffix", keyMatcher.match(listeners, "prefix.middle.suffix"));
130+
assertEquals("prefix.middle.*", keyMatcher.match(listeners, "prefix.middle.any"));
131+
assertEquals("prefix.middle.*", keyMatcher.match(listeners, "prefix.middle.any.any"));
132+
assertEquals("prefix.*.suffix", keyMatcher.match(listeners, "prefix.any.suffix"));
133+
assertEquals("prefix.*.suffix", keyMatcher.match(listeners, "prefix.any.any.suffix"));
134+
assertEquals("*.middle.suffix", keyMatcher.match(listeners, "any.middle.suffix"));
135+
assertEquals("*.middle.suffix", keyMatcher.match(listeners, "any.any.middle.suffix"));
136+
assertEquals("*.*.suffix", keyMatcher.match(listeners, "any.any.suffix"));
137+
assertEquals("*.*.suffix", keyMatcher.match(listeners, "any.any.any.suffix"));
138+
assertEquals("*.middle.*", keyMatcher.match(listeners, "any.middle.any"));
139+
assertEquals("*.middle.*", keyMatcher.match(listeners, "any.any.middle.any.any"));
140+
assertEquals("prefix.*.*", keyMatcher.match(listeners, "prefix.any.any"));
141+
assertEquals("prefix.*.*", keyMatcher.match(listeners, "prefix.any.any.any.any"));
142+
assertEquals("*.*.*", keyMatcher.match(listeners, "any.any.any"));
143+
assertEquals("*.*.*", keyMatcher.match(listeners, "any.any.any.any.any"));
144+
}
145+
}

async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/HandlerResolver.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import org.reactivecommons.async.api.handlers.registered.RegisteredCommandHandler;
55
import org.reactivecommons.async.api.handlers.registered.RegisteredEventListener;
66
import org.reactivecommons.async.api.handlers.registered.RegisteredQueryHandler;
7+
import org.reactivecommons.async.commons.utils.matcher.KeyMatcher;
8+
import org.reactivecommons.async.commons.utils.matcher.Matcher;
79

810
import java.util.Collection;
911
import java.util.HashSet;
1012
import java.util.Map;
1113
import java.util.Set;
14+
import java.util.function.Function;
1215

1316
@RequiredArgsConstructor
1417
public class HandlerResolver {
@@ -18,15 +21,18 @@ public class HandlerResolver {
1821
private final Map<String, RegisteredEventListener<?>> eventNotificationListeners;
1922
private final Map<String, RegisteredEventListener<?>> dynamicEventsHandlers;
2023
private final Map<String, RegisteredCommandHandler<?>> commandHandlers;
24+
private final Matcher matcher = new KeyMatcher();
2125

2226
@SuppressWarnings("unchecked")
2327
public <T, M> RegisteredQueryHandler<T, M> getQueryHandler(String path) {
24-
return (RegisteredQueryHandler<T, M>) queryHandlers.get(path);
28+
return (RegisteredQueryHandler<T, M>) queryHandlers
29+
.computeIfAbsent(path, getMatchHandler(queryHandlers));
2530
}
2631

2732
@SuppressWarnings("unchecked")
2833
public <T> RegisteredCommandHandler<T> getCommandHandler(String path) {
29-
return (RegisteredCommandHandler<T>) commandHandlers.get(path);
34+
return (RegisteredCommandHandler<T>) commandHandlers
35+
.computeIfAbsent(path, getMatchHandler(commandHandlers));
3036
}
3137

3238
@SuppressWarnings("unchecked")
@@ -45,7 +51,8 @@ public Collection<RegisteredEventListener<?>> getNotificationListeners() {
4551

4652
@SuppressWarnings("unchecked")
4753
public <T> RegisteredEventListener<T> getNotificationListener(String path) {
48-
return (RegisteredEventListener<T>) eventNotificationListeners.get(path);
54+
return (RegisteredEventListener<T>) eventNotificationListeners
55+
.computeIfAbsent(path, getMatchHandler(eventNotificationListeners));
4956
}
5057

5158
public Collection<RegisteredEventListener<?>> getEventListeners() {
@@ -62,7 +69,15 @@ public Set<String> getToListenEventNames() {
6269
return toListenEventNames;
6370
}
6471

65-
void addEventListener(RegisteredEventListener listener) {
72+
void addEventListener(RegisteredEventListener<?> listener) {
6673
eventListeners.put(listener.getPath(), listener);
6774
}
75+
76+
private <T> Function<String, T> getMatchHandler(Map<String, T> handlers) {
77+
return name -> {
78+
String matched = matcher.match(handlers.keySet(), name);
79+
return handlers.get(matched);
80+
};
81+
}
82+
6883
}

samples/async/receiver-responder/src/main/java/sample/SampleReceiverApp.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public HandlerRegistry handlerRegistrySubs(DirectAsyncGateway gateway) {
5353
log.info("resolving from direct query");
5454
return just(new RespQuery1("Ok", message));
5555
}, Call.class)
56+
.serveQuery("sample.query.*", message -> {
57+
log.info("resolving from direct query");
58+
return just(new RespQuery1("Ok", message));
59+
}, Call.class)
5660
.serveQuery("query2", (from, message) -> {
5761
log.info("resolving from delegate query");
5862
return gateway.reply(new RespQuery1("Ok", message), from).then();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
spring.application.name=receiverW
1+
spring.application.name=receiver
22
spring.rabbitmq.virtual-host=test

samples/async/sender-client/src/main/java/sample/SampleRestController.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public Mono<RespQuery1> sampleService(@RequestBody Call call) {
3030
return directAsyncGateway.requestReply(query, target, RespQuery1.class);
3131
}
3232

33+
@PostMapping(path = "/sample/match", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
34+
public Mono<RespQuery1> sampleServices(@RequestBody Call call) {
35+
AsyncQuery<?> query = new AsyncQuery<>("sample.query.any.that.matches", call);
36+
return directAsyncGateway.requestReply(query, target, RespQuery1.class);
37+
}
38+
3339
@PostMapping(path = "/sample2", consumes = MediaType.APPLICATION_JSON_VALUE, produces =
3440
MediaType.APPLICATION_JSON_VALUE)
3541
public Mono<RespQuery1> sampleServiceDelegate(@RequestBody Call call) {
@@ -41,11 +47,11 @@ public Mono<RespQuery1> sampleServiceDelegate(@RequestBody Call call) {
4147
public Mono<RespQuery1> sampleServiceHttp(@RequestBody Call call) {
4248
DummyQuery dummyQuery = new DummyQuery(queryName, call);
4349
final Mono<RespQuery1> response = webClient.post().uri("http://127.0.0.1:4004/sample_destination")
44-
.contentType(MediaType.APPLICATION_JSON)
45-
.bodyValue(dummyQuery)
46-
.accept(MediaType.APPLICATION_JSON)
47-
.retrieve()
48-
.bodyToMono(RespQuery1.class);
50+
.contentType(MediaType.APPLICATION_JSON)
51+
.bodyValue(dummyQuery)
52+
.accept(MediaType.APPLICATION_JSON)
53+
.retrieve()
54+
.bodyToMono(RespQuery1.class);
4955
return response;
5056
}
5157

@@ -74,7 +80,7 @@ static class Call {
7480
@Data
7581
@AllArgsConstructor
7682
@NoArgsConstructor
77-
static class DummyQuery{
83+
static class DummyQuery {
7884
private String resource;
7985
private Call call;
8086
}

0 commit comments

Comments
 (0)