This is an automated email from the ASF dual-hosted git repository. timothyjward pushed a commit to branch feature/single-level-wildcard in repository https://gitbox.apache.org/repos/asf/aries-typedevent.git
commit 4b899a1dc5007c1e1249ce2b9a1eb71faf190a90 Author: Tim Ward <[email protected]> AuthorDate: Wed Sep 6 12:55:42 2023 +0100 Backport support for single-level wildcards This commit backports support for single-level wildcards from the Typed Events 1.1 specification into the 1.0 release stream so that it can be used by current clients. This support must be explicitly configured for it to be enabled. The commit updates the topic validation to support single level wildcards, and it also enhances the delivery filtering to understand single level wildcards. Tests are included for the basic filtering, and for event delivery. Signed-off-by: Tim Ward <[email protected]> --- .../aries/typedevent/bus/impl/EventSelector.java | 157 +++++++++++++++++++++ .../typedevent/bus/impl/TypedEventBusImpl.java | 86 +++++++---- .../typedevent/bus/impl/EventSelectorTest.java | 128 +++++++++++++++++ .../typedevent/bus/impl/TypedEventBusImplTest.java | 138 ++++++++++++++++-- org.apache.aries.typedevent.bus/test.bndrun | 9 +- .../test.bndrun | 12 +- 6 files changed, 482 insertions(+), 48 deletions(-) diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventSelector.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventSelector.java new file mode 100644 index 0000000..08699f3 --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventSelector.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.aries.typedevent.bus.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.osgi.framework.Filter; + +public class EventSelector { + + /** The event filter **/ + private final Filter filter; + + /** + * Additional topic segments to check after intitial + * Each segment starts with a '/' and is preceded by + * a single level wildcard, e.g. + * + * "foo/+/foobar/fizz/+/fizzbuzz/done" => + * ["/foobar/fizz/","/fizzbuzz/done"] + **/ + private final List<String> additionalSegments; + + /** + * True if there is a trailing multi-level wildcard + */ + private final boolean isMultiLevelWildcard; + + /** + * The initial section of topic to match, + * will only ever contain literals + * e.g. + * + * "*" => "" + * "foo/+/foobar" => "foo/" + */ + private final String initial; + + private final Predicate<String> topicMatcher; + + /** + * Create an event selector + * + * @param topic - if non null then assumed to be valid. If null then topic checking disabled + * @param filter + */ + public EventSelector(String topic, Filter filter) { + this.filter = filter; + + if(topic == null) { + // No topic matching + additionalSegments = Collections.emptyList(); + isMultiLevelWildcard = false; + initial = ""; + topicMatcher = s -> true; + } else { + // Do topic matching + if(topic.endsWith("*")) { + isMultiLevelWildcard = true; + topic = topic.substring(0, topic.length() - 1); + } else { + isMultiLevelWildcard = false; + } + + int singleLevelIdx = topic.indexOf('+'); + if(singleLevelIdx < 0) { + initial = topic; + additionalSegments = Collections.emptyList(); + } else { + initial = topic.substring(0, singleLevelIdx); + List<String> segments = new ArrayList<>(); + for(;;) { + int nextIdx = topic.indexOf('+', singleLevelIdx + 1); + if(nextIdx < 0) { + segments.add(topic.substring(singleLevelIdx + 1)); + break; + } else { + segments.add(topic.substring(singleLevelIdx + 1, nextIdx)); + singleLevelIdx = nextIdx; + } + } + additionalSegments = Collections.unmodifiableList(segments); + } + + if(additionalSegments.isEmpty()) { + if(isMultiLevelWildcard) { + topicMatcher = s -> s.startsWith(initial); + } else { + topicMatcher = initial::equals; + } + } else { + topicMatcher = this::topicMatch; + } + } + } + + public boolean matches(String topic, EventConverter event) { + // Must match the topic, and the filter if set + return topicMatcher.test(topic) && (filter == null || event.applyFilter(filter)); + } + + private boolean topicMatch(String topic) { + + if(topic.startsWith(initial)) { + int startIdx = initial.length(); + for(String segment : additionalSegments) { + // First, skip the single level wildcard + startIdx = topic.indexOf('/', startIdx); + if(startIdx < 0) { + startIdx = topic.length(); + } + if(topic.regionMatches(startIdx, segment, 0, segment.length())) { + // Check the next segment + startIdx += segment.length(); + } else { + // Doesn't match the segment + return false; + } + } + + if(startIdx == topic.length()) { + // We consumed the whole topic so this is a match + return true; + } else if(isMultiLevelWildcard && topic.charAt(startIdx - 1) == '/') { + // We consumed a whole number of tokens and are multi-level + return true; + } + } + + return false; + } + + /** + * Get the initial prefix before the first wildcard + * @return + */ + public String getInitial() { + return initial; + } +} diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImpl.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImpl.java index 5ce2ef8..d8c79c5 100644 --- a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImpl.java +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImpl.java @@ -34,9 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.NavigableMap; import java.util.Objects; -import java.util.TreeMap; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -69,13 +67,13 @@ public class TypedEventBusImpl implements TypedEventBus { * Map access and mutation must be synchronized on {@link #lock}. Values from * the map should be copied as the contents are not thread safe. */ - private final Map<String, Map<TypedEventHandler<?>, Filter>> topicsToTypedHandlers = new HashMap<>(); + private final Map<String, Map<TypedEventHandler<?>, EventSelector>> topicsToTypedHandlers = new HashMap<>(); /** * Map access and mutation must be synchronized on {@link #lock}. Values from * the map should be copied as the contents are not thread safe. */ - private final NavigableMap<String, Map<TypedEventHandler<?>, Filter>> wildcardTopicsToTypedHandlers = new TreeMap<>(); + private final Map<String, Map<TypedEventHandler<?>, EventSelector>> wildcardTopicsToTypedHandlers = new HashMap<>(); /** * Map access and mutation must be synchronized on {@link #lock}. Values from @@ -87,13 +85,13 @@ public class TypedEventBusImpl implements TypedEventBus { * Map access and mutation must be synchronized on {@link #lock}. Values from * the map should be copied as the contents are not thread safe. */ - private final NavigableMap<String, Map<UntypedEventHandler, Filter>> topicsToUntypedHandlers = new TreeMap<>(); + private final Map<String, Map<UntypedEventHandler, EventSelector>> topicsToUntypedHandlers = new HashMap<>(); /** * Map access and mutation must be synchronized on {@link #lock}. Values from * the map should be copied as the contents are not thread safe. */ - private final Map<String, Map<UntypedEventHandler, Filter>> wildcardTopicsToUntypedHandlers = new HashMap<>(); + private final Map<String, Map<UntypedEventHandler, EventSelector>> wildcardTopicsToUntypedHandlers = new HashMap<>(); /** * List access and mutation must be synchronized on {@link #lock}. @@ -127,9 +125,12 @@ public class TypedEventBusImpl implements TypedEventBus { private EventThread thread; private final Object threadLock = new Object(); + + private final boolean allowSingleLevelWildcards; public TypedEventBusImpl(TypedEventMonitorImpl monitorImpl, Map<String, ?> config) { this.monitorImpl = monitorImpl; + this.allowSingleLevelWildcards = Boolean.parseBoolean(String.valueOf(config.get("extended.wildcards.enabled"))); } void addTypedEventHandler(Bundle registeringBundle, TypedEventHandler<?> handler, Map<String, Object> properties) { @@ -209,7 +210,7 @@ public class TypedEventBusImpl implements TypedEventBus { doAddEventHandler(topicsToUntypedHandlers, wildcardTopicsToUntypedHandlers, knownUntypedHandlers, handler, null, properties); } - private <T> void doAddEventHandler(Map<String, Map<T, Filter>> map, Map<String, Map<T, Filter>> wildcardMap, + private <T> void doAddEventHandler(Map<String, Map<T, EventSelector>> map, Map<String, Map<T, EventSelector>> wildcardMap, Map<Long, T> idMap, T handler, String defaultTopic, Map<String, Object> properties) { Object prop = properties.get(TypedEventConstants.TYPED_EVENT_TOPICS); @@ -252,17 +253,20 @@ public class TypedEventBusImpl implements TypedEventBus { idMap.put(serviceId, handler); for(String s : topicList) { - Map<String, Map<T, Filter>> mapToUse; + Map<String, Map<T, EventSelector>> mapToUse; String topicToUse; + EventSelector selector; if(isWildcard(s)) { mapToUse = wildcardMap; - topicToUse = s.length() == 1 ? "" : s.substring(0, s.length() - 2); + selector = new EventSelector(s, f); + topicToUse = selector.getInitial(); } else { mapToUse = map; topicToUse = s; + selector = new EventSelector(null, f); } - Map<T, Filter> handlers = mapToUse.computeIfAbsent(topicToUse, x1 -> new HashMap<>()); - handlers.put(handler, f); + Map<T, EventSelector> handlers = mapToUse.computeIfAbsent(topicToUse, x1 -> new HashMap<>()); + handlers.put(handler, selector); } } } @@ -318,7 +322,7 @@ public class TypedEventBusImpl implements TypedEventBus { String key; if(isWildcard(s)) { handlers = wildcardMap; - key = s.length() == 1 ? "" : s.substring(0, s.length() - 2); + key = new EventSelector(s, null).getInitial(); } else { handlers = map; key = s; @@ -353,7 +357,7 @@ public class TypedEventBusImpl implements TypedEventBus { doUpdatedEventHandler(topicsToUntypedHandlers, wildcardTopicsToUntypedHandlers, knownUntypedHandlers, null, properties); } - private <T> void doUpdatedEventHandler(Map<String, Map<T, Filter>> map, Map<String, Map<T, Filter>> wildcardMap, Map<Long,T> idToHandler, String defaultTopic, + private <T> void doUpdatedEventHandler(Map<String, Map<T, EventSelector>> map, Map<String, Map<T, EventSelector>> wildcardMap, Map<Long,T> idToHandler, String defaultTopic, Map<String, Object> properties) { Long serviceId = getServiceId(properties); @@ -442,8 +446,8 @@ public class TypedEventBusImpl implements TypedEventBus { List<EventTask> wildcardDeliveries = new ArrayList<>(); String truncatedTopic = topic; do { - int idx = truncatedTopic.lastIndexOf('/'); - truncatedTopic = idx > 0 ? truncatedTopic.substring(0, idx) : ""; + int idx = truncatedTopic.lastIndexOf('/', truncatedTopic.length() - 2); + truncatedTopic = idx > 0 ? truncatedTopic.substring(0, idx + 1) : ""; wildcardDeliveries.addAll(toTypedEventTasks( wildcardTopicsToTypedHandlers.getOrDefault(truncatedTopic, emptyMap()), topic, convertibleEventData)); @@ -471,12 +475,11 @@ public class TypedEventBusImpl implements TypedEventBus { queue.addAll(deliveryTasks); } - private List<EventTask> toTypedEventTasks(Map<TypedEventHandler<?>, Filter> map, + private List<EventTask> toTypedEventTasks(Map<TypedEventHandler<?>, EventSelector> map, String topic, EventConverter convertibleEventData) { List<EventTask> list = new ArrayList<>(); - for(Entry<TypedEventHandler<?>, Filter> e : map.entrySet()) { - Filter f = e.getValue(); - if(f == null || convertibleEventData.applyFilter(f)) { + for(Entry<TypedEventHandler<?>, EventSelector> e : map.entrySet()) { + if(e.getValue().matches(topic, convertibleEventData)) { TypedEventHandler<?> handler = e.getKey(); list.add(new TypedEventTask(topic, convertibleEventData, handler, typedHandlersToTargetClasses.get(handler))); @@ -485,12 +488,11 @@ public class TypedEventBusImpl implements TypedEventBus { return list; } - private List<EventTask> toUntypedEventTasks(Map<UntypedEventHandler, Filter> map, + private List<EventTask> toUntypedEventTasks(Map<UntypedEventHandler, EventSelector> map, String topic, EventConverter convertibleEventData) { List<EventTask> list = new ArrayList<>(); - for(Entry<UntypedEventHandler, Filter> e : map.entrySet()) { - Filter f = e.getValue(); - if(f == null || convertibleEventData.applyFilter(f)) { + for(Entry<UntypedEventHandler, EventSelector> e : map.entrySet()) { + if(e.getValue().matches(topic, convertibleEventData)) { UntypedEventHandler handler = e.getKey(); list.add(new UntypedEventTask(topic, convertibleEventData, handler)); } @@ -498,14 +500,14 @@ public class TypedEventBusImpl implements TypedEventBus { return list; } - private static void checkTopicSyntax(String topic) { + private void checkTopicSyntax(String topic) { String msg = checkTopicSyntax(topic, false); if(msg != null) { throw new IllegalArgumentException(msg); } } - private static String checkTopicSyntax(String topic, boolean wildcardPermitted) { + private String checkTopicSyntax(String topic, boolean wildcardPermitted) { if(topic == null) { throw new IllegalArgumentException("The topic name is not permitted to be null"); @@ -514,9 +516,13 @@ public class TypedEventBusImpl implements TypedEventBus { boolean slashPermitted = false; for(int i = 0; i < topic.length(); i++) { int c = topic.codePointAt(i); + if(c >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { + // handle unicode characters greater than OxFFFF + i++; + } if('*' == c) { if(!wildcardPermitted) { - return "Wildcard topics may not be used for sending events"; + return "Multi-Level Wildcard topics may not be used for sending events"; } if(topic.length() != i + 1) { return "The wildcard * is only permitted at the end of the topic"; @@ -526,6 +532,27 @@ public class TypedEventBusImpl implements TypedEventBus { } continue; } + + if('+' == c) { + if(!allowSingleLevelWildcards) { + return "Single Level Wildcard topics are not part of Typed Events 1.0, and must be explicitly enabled using \"extended.wildcards.enabled\""; + } + if(!wildcardPermitted) { + return "Single Level Wildcard topics may not be used for sending events"; + } + if(i > 0 && topic.codePointAt(i - 1) != '/') { + return "The single level wildcard must be preceded by a / unless it is the first character in the topic string"; + } + if(topic.length() > i + 1) { + if(topic.codePointAt(i + 1) != '/') { + return "The single level wildcard must be followed by a / unless it is the last character in the topic string"; + } else { + // We have already checked the next '/' so skip it + i++; + } + } + continue; + } if('/' == c) { if(slashPermitted && i != (topic.length() - 1)) { @@ -541,8 +568,13 @@ public class TypedEventBusImpl implements TypedEventBus { return null; } + /** + * This method assumes that the topic is valid + * @param topic + * @return + */ private static boolean isWildcard(String topic) { - return topic.equals("*") || topic.endsWith("/*"); + return topic.indexOf('+') >= 0 || topic.indexOf('*') >= 0; } private class EventThread extends Thread { diff --git a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventSelectorTest.java b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventSelectorTest.java new file mode 100644 index 0000000..2869c8a --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventSelectorTest.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.aries.typedevent.bus.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.apache.aries.typedevent.bus.impl.EventConverterTest.NestedEventHolderNotAProperDTO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.osgi.framework.Filter; + +@ExtendWith(MockitoExtension.class) +public class EventSelectorTest { + + @Mock + Filter mockFilter; + + @Mock + EventConverter eventConverter; + + public static class DoublyNestedEventHolderWithIssues { + public NestedEventHolderNotAProperDTO event; + } + + @ParameterizedTest + @MethodSource("getTopicMatchingData") + public void testTopicMatching(String topic, String topicFilter, boolean expectedResult) { + assertEquals(expectedResult, new EventSelector(topicFilter, null).matches(topic, null)); + } + + /** + * Test topics and filters for matching checks + * @return + */ + static Stream<Arguments> getTopicMatchingData() { + return Arrays.asList( + // Basic + Arguments.of("foo", "foo", true), + Arguments.of("foo/bar", "foo", false), + Arguments.of("foo/bar", "foo/bar", true), + Arguments.of("foo/bark", "foo/bar", false), + Arguments.of("foo/bar", "foo/barb", false), + // Multi Level Wildcard + Arguments.of("foo", "*", true), + Arguments.of("foo", "foo/*", false), + Arguments.of("foo/bar", "*", true), + Arguments.of("foo/bar", "foo/*", true), + Arguments.of("foo/foobar", "foo/*", true), + Arguments.of("foo/bar/foobar", "foo/*", true), + Arguments.of("foo/bar/foobar", "foo/bar/*", true), + Arguments.of("foo/bark/foobar", "foo/bar/*", false), + // Single Level Wildcard + Arguments.of("foo", "+", true), + Arguments.of("foo", "foo/+", false), + Arguments.of("foo/bar", "+", false), + Arguments.of("foo/bar", "foo/+", true), + Arguments.of("foo/bar", "+/bar", true), + Arguments.of("foo/foobar", "foo/+", true), + Arguments.of("fool/foobar", "foo/+", false), + Arguments.of("foo/foobar", "+/+", true), + Arguments.of("foo/bar/foobar", "foo/+", false), + Arguments.of("foo/bar/foobar", "foo/+/foobar", true), + Arguments.of("foo/bar/foobark", "foo/+/foobar", false), + Arguments.of("foo/bar/foobar", "foo/+/+", true), + Arguments.of("foo/bar/foobar", "+/bar/+", true), + Arguments.of("foo/bark/foobar", "foo/bar/+", false), + // Mixture of wildcards + Arguments.of("foo", "+/*", false), + Arguments.of("foo/bar", "+/*", true), + Arguments.of("foo/bar/foobar", "+/*", true), + Arguments.of("foo/bar/foobar", "+/bar/*", true), + Arguments.of("foo/bar/foobar", "+/bar/foobar/*", false), + Arguments.of("foo/bar/foobar", "+/bar/+/*", false), + Arguments.of("foo/bar/foobar", "+/+/*", true), + Arguments.of("foo/bar/foobar/fizz", "+/bar/+/*", true), + Arguments.of("foo/bar/foobar/fizz", "foo/+/foobar/*", true), + Arguments.of("foo/bar/foobar/fizz", "foo/+/+/*", true), + Arguments.of("fool/bar/foobar/fizz", "foo/+/+/*", false) + ).stream(); + } + + @Test + public void testEventFilteringNoTopicMatch() { + assertFalse(new EventSelector("foo/bar", mockFilter).matches("fizz/buzz", eventConverter)); + Mockito.verifyNoInteractions(mockFilter, eventConverter); + } + + @Test + public void testEventFilteringFilterMatch() { + Mockito.when(eventConverter.applyFilter(mockFilter)).thenReturn(true); + assertTrue(new EventSelector("foo/bar", mockFilter).matches("foo/bar", eventConverter)); + Mockito.verify(eventConverter).applyFilter(mockFilter); + } + + @Test + public void testEventFilteringNoFilterMatch() { + Mockito.when(eventConverter.applyFilter(mockFilter)).thenReturn(false); + assertFalse(new EventSelector("foo/bar", mockFilter).matches("foo/bar", eventConverter)); + Mockito.verify(eventConverter).applyFilter(mockFilter); + } + +} diff --git a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImplTest.java b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImplTest.java index 017e1be..2113f7d 100644 --- a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImplTest.java +++ b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/TypedEventBusImplTest.java @@ -27,6 +27,11 @@ import static org.osgi.service.typedevent.TypedEventConstants.TYPED_EVENT_TOPICS import static org.osgi.service.typedevent.TypedEventConstants.TYPED_EVENT_TYPE; import static org.osgi.util.converter.Converters.standardConverter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; @@ -35,6 +40,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; @@ -50,8 +56,11 @@ public class TypedEventBusImplTest { private static final String SPECIAL_TEST_EVENT_TOPIC = SpecialTestEvent.class.getName().replace(".", "/"); private static final String TEST_EVENT_TOPIC = TestEvent.class.getName().replace(".", "/"); - private static final String TEST_EVENT_WILDCARD_TOPIC = "org/apache/aries/typedevent/*"; - private static final String BASE_WILDCARD_TOPIC = "*"; + private static final String TEST_EVENT_ML_WILDCARD_TOPIC = "org/apache/aries/typedevent/*"; + private static final String BASE_ML_WILDCARD_TOPIC = "*"; + + private static final String TEST_EVENT_SL_WILDCARD_TOPIC = "org/apache/+/typedevent/bus/impl/TypedEventBusImplTest$TestEvent"; + private static final String BASE_SL_WILDCARD_TOPIC = "+"; public static class TestEvent { public String message; @@ -86,7 +95,7 @@ public class TypedEventBusImplTest { private AutoCloseable mocks; @BeforeEach - public void start() throws ClassNotFoundException { + public void start(TestInfo info) throws ClassNotFoundException { mocks = MockitoAnnotations.openMocks(this); @@ -118,11 +127,24 @@ public class TypedEventBusImplTest { return null; }).when(unhandledHandler).notifyUnhandled(Mockito.anyString(), Mockito.any()); - monitorImpl = new TypedEventMonitorImpl(new HashMap<String, Object>()); + Map<String, Object> config; + if(info.getTestMethod() + .map(m -> m.isAnnotationPresent(AllowSingleLevelWildcard.class)) + .orElse(false)) { + config = Collections.singletonMap("extended.wildcards.enabled", true); + } else { + config = Collections.emptyMap(); + } + + monitorImpl = new TypedEventMonitorImpl(config); - impl = new TypedEventBusImpl(monitorImpl, new HashMap<String, Object>()); + impl = new TypedEventBusImpl(monitorImpl, config); impl.start(); } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public static @interface AllowSingleLevelWildcard { } @AfterEach public void stop() throws Exception { @@ -191,19 +213,19 @@ public class TypedEventBusImplTest { } /** - * Tests that events are delivered to Smart Behaviours based on type + * Tests that events are delivered with Wildcarding * * @throws InterruptedException */ @Test - public void testWildcardEventReceiving() throws InterruptedException { + public void testMultiLevelWildcardEventReceiving() throws InterruptedException { TestEvent event = new TestEvent(); event.message = "boo"; Map<String, Object> serviceProperties = new HashMap<>(); - serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_ML_WILDCARD_TOPIC); serviceProperties.put(TYPED_EVENT_TYPE, TestEvent.class.getName()); serviceProperties.put(SERVICE_ID, 42L); @@ -211,7 +233,7 @@ public class TypedEventBusImplTest { serviceProperties = new HashMap<>(); - serviceProperties.put(TYPED_EVENT_TOPICS, BASE_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TOPICS, BASE_ML_WILDCARD_TOPIC); serviceProperties.put(TYPED_EVENT_TYPE, TestEvent.class.getName()); serviceProperties.put(SERVICE_ID, 43L); @@ -219,14 +241,14 @@ public class TypedEventBusImplTest { serviceProperties = new HashMap<>(); - serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_ML_WILDCARD_TOPIC); serviceProperties.put(SERVICE_ID, 44L); impl.addUntypedEventHandler(untypedHandlerA, serviceProperties); serviceProperties = new HashMap<>(); - serviceProperties.put(TYPED_EVENT_TOPICS, BASE_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TOPICS, BASE_ML_WILDCARD_TOPIC); serviceProperties.put(SERVICE_ID, 45L); impl.addUntypedEventHandler(untypedHandlerB, serviceProperties); @@ -269,6 +291,100 @@ public class TypedEventBusImplTest { Mockito.verify(untypedHandlerB).notifyUntyped(Mockito.eq(TestEvent.class.getName().replace('.', '/')), Mockito.argThat(isUntypedTestEventWithMessage("boo"))); } + + /** + * Tests that events are delivered with Single Level wildcards + * + * @throws InterruptedException + */ + @AllowSingleLevelWildcard + @Test + public void testSingleLevelWildcardEventReceiving() throws InterruptedException { + + TestEvent event = new TestEvent(); + event.message = "boo"; + + Map<String, Object> serviceProperties = new HashMap<>(); + + serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_SL_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TYPE, TestEvent.class.getName()); + serviceProperties.put(SERVICE_ID, 42L); + + impl.addTypedEventHandler(registeringBundle, handlerA, serviceProperties); + + serviceProperties = new HashMap<>(); + + serviceProperties.put(TYPED_EVENT_TOPICS, BASE_SL_WILDCARD_TOPIC); + serviceProperties.put(TYPED_EVENT_TYPE, TestEvent.class.getName()); + serviceProperties.put(SERVICE_ID, 43L); + + impl.addTypedEventHandler(registeringBundle, handlerB, serviceProperties); + + serviceProperties = new HashMap<>(); + + serviceProperties.put(TYPED_EVENT_TOPICS, TEST_EVENT_SL_WILDCARD_TOPIC); + serviceProperties.put(SERVICE_ID, 44L); + + impl.addUntypedEventHandler(untypedHandlerA, serviceProperties); + + serviceProperties = new HashMap<>(); + + serviceProperties.put(TYPED_EVENT_TOPICS, BASE_SL_WILDCARD_TOPIC); + serviceProperties.put(SERVICE_ID, 45L); + + impl.addUntypedEventHandler(untypedHandlerB, serviceProperties); + + impl.deliver(event); + + assertTrue(semA.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(handlerA).notify(Mockito.eq(TestEvent.class.getName().replace('.', '/')), + Mockito.argThat(isTestEventWithMessage("boo"))); + + assertFalse(semB.tryAcquire(1, TimeUnit.SECONDS)); + + assertTrue(untypedSemA.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(untypedHandlerA).notifyUntyped(Mockito.eq(TestEvent.class.getName().replace('.', '/')), + Mockito.argThat(isUntypedTestEventWithMessage("boo"))); + + assertFalse(untypedSemB.tryAcquire(1, TimeUnit.SECONDS)); + + String topic = "org/apache/taurus/typedevent/bus/impl/TypedEventBusImplTest$TestEvent"; + impl.deliver(topic, event); + + assertTrue(semA.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(handlerA).notify(Mockito.eq(topic), + Mockito.argThat(isTestEventWithMessage("boo"))); + + assertFalse(semB.tryAcquire(1, TimeUnit.SECONDS)); + + assertTrue(untypedSemA.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(untypedHandlerA).notifyUntyped(Mockito.eq(topic), + Mockito.argThat(isUntypedTestEventWithMessage("boo"))); + + assertFalse(untypedSemB.tryAcquire(1, TimeUnit.SECONDS)); + + topic = "org"; + impl.deliver(topic, event); + + assertFalse(semA.tryAcquire(1, TimeUnit.SECONDS)); + + assertTrue(semB.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(handlerB).notify(Mockito.eq(topic), + Mockito.argThat(isTestEventWithMessage("boo"))); + + assertFalse(untypedSemA.tryAcquire(1, TimeUnit.SECONDS)); + + assertTrue(untypedSemB.tryAcquire(1, TimeUnit.SECONDS)); + + Mockito.verify(untypedHandlerB).notifyUntyped(Mockito.eq(topic), + Mockito.argThat(isUntypedTestEventWithMessage("boo"))); + + } public static class TestEventHandler implements TypedEventHandler<TestEvent> { diff --git a/org.apache.aries.typedevent.bus/test.bndrun b/org.apache.aries.typedevent.bus/test.bndrun index 08314a8..aa2948d 100644 --- a/org.apache.aries.typedevent.bus/test.bndrun +++ b/org.apache.aries.typedevent.bus/test.bndrun @@ -19,7 +19,7 @@ -runfw: org.apache.felix.framework --runee: JavaSE-11 +-runee: JavaSE-17 -runrequires: bnd.identity;id="org.apache.aries.typedevent.bus",\ bnd.identity;id="org.apache.aries.typedevent.bus-tests",\ @@ -39,8 +39,6 @@ junit-jupiter-api;version='[5.10.0,5.10.1)',\ junit-platform-commons;version='[1.10.0,1.10.1)',\ junit-jupiter-engine;version='[5.10.0,5.10.1)',\ - org.apache.aries.typedevent.bus;version='[0.0.2,0.0.3)',\ - org.apache.aries.typedevent.bus-tests;version='[0.0.2,0.0.3)',\ junit-platform-engine;version='[1.10.0,1.10.1)',\ junit-platform-launcher;version='[1.10.0,1.10.1)',\ org.objenesis;version='[3.3.0,3.3.1)',\ @@ -55,4 +53,7 @@ ch.qos.logback.core;version='[1.2.3,1.2.4)',\ org.apache.aries.component-dsl.component-dsl;version='[1.2.2,1.2.3)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ - slf4j.api;version='[1.7.30,1.7.31)' \ No newline at end of file + slf4j.api;version='[1.7.30,1.7.31)',\ + org.apache.aries.typedevent.bus;version='[0.0.3,0.0.4)',\ + org.apache.aries.typedevent.bus-tests;version='[0.0.3,0.0.4)',\ + org.mockito.junit-jupiter;version='[5.5.0,5.5.1)' \ No newline at end of file diff --git a/org.apache.aries.typedevent.remote/org.apache.aries.typedevent.remote.remoteservices/test.bndrun b/org.apache.aries.typedevent.remote/org.apache.aries.typedevent.remote.remoteservices/test.bndrun index 4a55dc9..fb30124 100644 --- a/org.apache.aries.typedevent.remote/org.apache.aries.typedevent.remote.remoteservices/test.bndrun +++ b/org.apache.aries.typedevent.remote/org.apache.aries.typedevent.remote.remoteservices/test.bndrun @@ -38,11 +38,6 @@ org.osgi.util.promise;version='[1.1.1,1.1.2)',\ org.osgi.util.pushstream;version='[1.0.1,1.0.2)',\ slf4j.api;version='[1.7.30,1.7.31)',\ - org.apache.aries.typedevent.bus;version='[0.0.2,0.0.3)',\ - org.apache.aries.typedevent.remote.api;version='[0.0.2,0.0.3)',\ - org.apache.aries.typedevent.remote.remoteservices;version='[0.0.2,0.0.3)',\ - org.apache.aries.typedevent.remote.remoteservices-tests;version='[0.0.2,0.0.3)',\ - org.apache.aries.typedevent.remote.spi;version='[0.0.2,0.0.3)',\ junit-jupiter-api;version='[5.10.0,5.10.1)',\ junit-jupiter-engine;version='[5.10.0,5.10.1)',\ junit-jupiter-params;version='[5.10.0,5.10.1)',\ @@ -56,4 +51,9 @@ org.objenesis;version='[3.3.0,3.3.1)',\ org.opentest4j;version='[1.3.0,1.3.1)',\ org.osgi.test.common;version='[1.2.1,1.2.2)',\ - org.osgi.test.junit5;version='[1.2.1,1.2.2)' + org.osgi.test.junit5;version='[1.2.1,1.2.2)',\ + org.apache.aries.typedevent.bus;version='[0.0.3,0.0.4)',\ + org.apache.aries.typedevent.remote.api;version='[0.0.3,0.0.4)',\ + org.apache.aries.typedevent.remote.remoteservices;version='[0.0.3,0.0.4)',\ + org.apache.aries.typedevent.remote.remoteservices-tests;version='[0.0.3,0.0.4)',\ + org.apache.aries.typedevent.remote.spi;version='[0.0.3,0.0.4)'
