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)'


Reply via email to