This is an automated email from the ASF dual-hosted git repository. timothyjward pushed a commit to branch feature/custom-convert in repository https://gitbox.apache.org/repos/asf/aries-typedevent.git
commit 262c1c1861bf439b10bf2cc501f577ea47e8465b Author: Tim Ward <[email protected]> AuthorDate: Thu Dec 12 18:25:54 2024 +0000 Add support for an event conversion plugin Event Conversion is left up to the implementation, and Aries uses the OSGi converter. There are, however, users that want to use richer types than are supported by the specification. They should be able to register an override which lets them perform the conversion that they need. Signed-off-by: Tim Ward <[email protected]> --- .../aries/typedevent/bus/impl/EventConverter.java | 38 +++++-- .../apache/aries/typedevent/bus/impl/TypeData.java | 52 +++++++++ .../bus/impl/TypedEventBusActivator.java | 5 +- .../typedevent/bus/impl/TypedEventBusImpl.java | 74 +++++++----- .../aries/typedevent/bus/impl/TypedEventTask.java | 4 +- .../aries/typedevent/bus/spi/AriesTypedEvents.java | 45 ++++++++ .../typedevent/bus/spi/CustomEventConverter.java | 45 ++++++++ .../aries/typedevent/bus/spi/package-info.java | 18 +++ .../typedevent/bus/impl/EventConverterTest.java | 124 ++++++++++++++++++--- .../bus/osgi/EventDeliveryIntegrationTest.java | 43 +++++++ 10 files changed, 394 insertions(+), 54 deletions(-) diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventConverter.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventConverter.java index 93d0d6f..6df162b 100644 --- a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventConverter.java +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/EventConverter.java @@ -43,6 +43,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.apache.aries.typedevent.bus.spi.CustomEventConverter; import org.osgi.framework.Filter; import org.osgi.util.converter.Converter; import org.osgi.util.converter.ConverterFunction; @@ -188,24 +189,27 @@ public class EventConverter { } private final Object originalEvent; + private final CustomEventConverter custom; private Map<String, ?> untypedEventDataForFiltering; - private EventConverter(Object event) { + private EventConverter(Object event, CustomEventConverter custom) { this.originalEvent = event; + this.custom = custom; } - private EventConverter(Map<String, ?> untypedEvent) { + private EventConverter(Map<String, ?> untypedEvent, CustomEventConverter custom) { this.originalEvent = untypedEvent; + this.custom = custom; this.untypedEventDataForFiltering = untypedEvent; } - public static EventConverter forTypedEvent(Object event) { - return new EventConverter(event); + public static EventConverter forTypedEvent(Object event, CustomEventConverter custom) { + return new EventConverter(event, custom); } - public static EventConverter forUntypedEvent(Map<String, ?> event) { - return new EventConverter(event); + public static EventConverter forUntypedEvent(Map<String, ?> event, CustomEventConverter custom) { + return new EventConverter(event, custom); } public boolean applyFilter(Filter f) { @@ -221,16 +225,28 @@ public class EventConverter { public Map<String, ?> toUntypedEvent() { if (untypedEventDataForFiltering == null) { - untypedEventDataForFiltering = eventConverter.convert(originalEvent).sourceAsDTO().to(MAP_WITH_STRING_KEYS); + if(custom == null || + (untypedEventDataForFiltering = custom.toUntypedEvent(originalEvent)) == null ) { + untypedEventDataForFiltering = eventConverter.convert(originalEvent).sourceAsDTO().to(MAP_WITH_STRING_KEYS); + } } return untypedEventDataForFiltering; } - public <T> T toTypedEvent(Class<T> clazz) { - if (clazz.isInstance(originalEvent)) { - return clazz.cast(originalEvent); + @SuppressWarnings("unchecked") + public <T> T toTypedEvent(TypeData targetEventClass) { + Class<?> rawType = targetEventClass.getRawType(); + Type genericTarget = targetEventClass.getType(); + if (rawType.isInstance(originalEvent) && rawType == genericTarget) { + return (T) originalEvent; } else { - return eventConverter.convert(originalEvent).targetAsDTO().to(clazz); + if(custom != null) { + Object result = custom.toTypedEvent(originalEvent, rawType, genericTarget); + if(result != null) { + return (T) result; + } + } + return eventConverter.convert(originalEvent).targetAsDTO().to(genericTarget); } } diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypeData.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypeData.java new file mode 100644 index 0000000..af58df0 --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypeData.java @@ -0,0 +1,52 @@ +/* + * 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.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * The generic type information for the event data + */ +public final class TypeData { + + private final Class<?> rawType; + + private final Type type; + + public TypeData(Type type) { + super(); + this.type = type; + if(type instanceof Class) { + this.rawType = (Class<?>) type; + } else if (type instanceof ParameterizedType) { + this.rawType = (Class<?>) ((ParameterizedType) type).getRawType(); + } else { + throw new IllegalArgumentException("The type " + type + + " is not acceptable. Must be a raw Class or Parameterized Type"); + } + } + + public Class<?> getRawType() { + return rawType; + } + + public Type getType() { + return type; + } +} diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusActivator.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusActivator.java index 43a7b04..28468d0 100644 --- a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusActivator.java +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventBusActivator.java @@ -38,6 +38,7 @@ import java.util.stream.Collectors; import org.apache.aries.component.dsl.OSGi; import org.apache.aries.component.dsl.OSGiResult; +import org.apache.aries.typedevent.bus.spi.AriesTypedEvents; import org.osgi.annotation.bundle.Header; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; @@ -123,7 +124,9 @@ public class TypedEventBusActivator implements BundleActivator { getServiceProps(csr.getServiceReference())), handler -> tebi.removeUnhandledEventHandler(handler, getServiceProps(csr.getServiceReference())))), - register(TypedEventBus.class, tebi, serviceProps) + register(new String[] { + TypedEventBus.class.getName(), AriesTypedEvents.class.getName() + }, tebi, serviceProps) .flatMap(x -> nothing()))); 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 d8c79c5..75211b0 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 @@ -27,6 +27,7 @@ import static org.osgi.service.typedevent.TypedEventConstants.TYPED_EVENT_SPECIF import static org.osgi.util.converter.Converters.standardConverter; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,7 +39,10 @@ import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import org.apache.aries.typedevent.bus.spi.AriesTypedEvents; +import org.apache.aries.typedevent.bus.spi.CustomEventConverter; import org.osgi.annotation.bundle.Capability; import org.osgi.framework.Bundle; import org.osgi.framework.Constants; @@ -54,7 +58,7 @@ import org.osgi.util.converter.TypeReference; @Capability(namespace=SERVICE_NAMESPACE, attribute="objectClass:List<String>=org.osgi.service.typedevent.TypedEventBus", uses=TypedEventBus.class) @Capability(namespace=IMPLEMENTATION_NAMESPACE, name=TYPED_EVENT_IMPLEMENTATION, version=TYPED_EVENT_SPECIFICATION_VERSION, uses=TypedEventBus.class) -public class TypedEventBusImpl implements TypedEventBus { +public class TypedEventBusImpl implements TypedEventBus, AriesTypedEvents { private static final TypeReference<List<String>> LIST_OF_STRINGS = new TypeReference<List<String>>() { }; @@ -79,7 +83,7 @@ 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<TypedEventHandler<?>, Class<?>> typedHandlersToTargetClasses = new HashMap<>(); + private final Map<TypedEventHandler<?>, TypeData> typedHandlersToTargetClasses = new HashMap<>(); /** * Map access and mutation must be synchronized on {@link #lock}. Values from @@ -128,11 +132,25 @@ public class TypedEventBusImpl implements TypedEventBus { private final boolean allowSingleLevelWildcards; + /** + * Access and mutation must be synchronized on {@link #lock}. + */ + private CustomEventConverter customEventConverter; + public TypedEventBusImpl(TypedEventMonitorImpl monitorImpl, Map<String, ?> config) { this.monitorImpl = monitorImpl; this.allowSingleLevelWildcards = Boolean.parseBoolean(String.valueOf(config.get("extended.wildcards.enabled"))); } + public void registerGlobalEventConverter(CustomEventConverter converter, boolean force) { + synchronized (lock) { + if(customEventConverter != null && !force) { + throw new IllegalStateException("A custom converter is already set"); + } + customEventConverter = converter; + } + } + void addTypedEventHandler(Bundle registeringBundle, TypedEventHandler<?> handler, Map<String, Object> properties) { Class<?> clazz = discoverTypeForTypedHandler(registeringBundle, handler, properties); @@ -142,27 +160,27 @@ public class TypedEventBusImpl implements TypedEventBus { } private Class<?> discoverTypeForTypedHandler(Bundle registeringBundle, TypedEventHandler<?> handler, Map<String, Object> properties) { - Class<?> clazz = null; + Type genType = null; Object type = properties.get(TypedEventConstants.TYPED_EVENT_TYPE); if (type != null) { try { - clazz = registeringBundle.loadClass(String.valueOf(type)); + genType = registeringBundle.loadClass(String.valueOf(type)); } catch (ClassNotFoundException e) { // TODO Blow up e.printStackTrace(); } } else { Class<?> toCheck = handler.getClass(); - outer: while(clazz == null) { - clazz = findDirectlyImplemented(toCheck); + outer: while(genType == null) { + genType = findDirectlyImplemented(toCheck); - if(clazz != null) { + if(genType != null) { break outer; } - clazz = processInterfaceHierarchyForClass(toCheck); + genType = processInterfaceHierarchyForClass(toCheck); - if(clazz != null) { + if(genType != null) { break outer; } @@ -170,40 +188,42 @@ public class TypedEventBusImpl implements TypedEventBus { } } - if (clazz != null) { + if (genType != null) { + TypeData typeData = new TypeData(genType); synchronized (lock) { - typedHandlersToTargetClasses.put(handler, clazz); + typedHandlersToTargetClasses.put(handler, typeData); } + return typeData.getRawType(); } else { - // TODO Blow Up + // TODO log + throw new IllegalArgumentException("Unable to determine handled type for " + handler); } - return clazz; } - private Class<?> processInterfaceHierarchyForClass(Class<?> toCheck) { - Class<?> clazz = null; + private Type processInterfaceHierarchyForClass(Class<?> toCheck) { + Type type = null; for (Class<?> iface : toCheck.getInterfaces()) { - clazz = findDirectlyImplemented(iface); + type = findDirectlyImplemented(iface); - if(clazz != null) { + if(type != null) { break; } - clazz = processInterfaceHierarchyForClass(iface); + type = processInterfaceHierarchyForClass(iface); - if(clazz != null) { + if(type != null) { break; } } - return clazz; + return type; } - private Class<?> findDirectlyImplemented(Class<?> toCheck) { + private Type findDirectlyImplemented(Class<?> toCheck) { return Arrays.stream(toCheck.getGenericInterfaces()) .filter(ParameterizedType.class::isInstance) .map(ParameterizedType.class::cast) .filter(t -> TypedEventHandler.class.equals(t.getRawType())).map(t -> t.getActualTypeArguments()[0]) - .findFirst().map(Class.class::cast).orElse(null); + .findFirst().orElse(null); } void addUntypedEventHandler(UntypedEventHandler handler, Map<String, Object> properties) { @@ -422,21 +442,25 @@ public class TypedEventBusImpl implements TypedEventBus { public void deliver(String topic, Object event) { checkTopicSyntax(topic); Objects.requireNonNull(event, "The event object must not be null"); - deliver(topic, EventConverter.forTypedEvent(event)); + deliver(topic, event, EventConverter::forTypedEvent); } @Override public void deliverUntyped(String topic, Map<String, ?> eventData) { checkTopicSyntax(topic); Objects.requireNonNull(eventData, "The event object must not be null"); - deliver(topic, EventConverter.forUntypedEvent(eventData)); + deliver(topic, eventData, EventConverter::forUntypedEvent); } - private void deliver(String topic, EventConverter convertibleEventData) { + private <T> void deliver(String topic, T event, + BiFunction<T, CustomEventConverter, EventConverter> eventConversionFactory) { List<EventTask> deliveryTasks; + EventConverter convertibleEventData; synchronized (lock) { + convertibleEventData = eventConversionFactory.apply(event, customEventConverter); + List<EventTask> typedDeliveries = toTypedEventTasks( topicsToTypedHandlers.getOrDefault(topic, emptyMap()), topic, convertibleEventData); diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventTask.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventTask.java index e5a2459..40cd64c 100644 --- a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventTask.java +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/impl/TypedEventTask.java @@ -21,13 +21,13 @@ import org.osgi.service.typedevent.TypedEventHandler; class TypedEventTask extends EventTask { private final String topic; - private final Class<?> targetEventClass; + private final TypeData targetEventClass; private final EventConverter eventData; private final TypedEventHandler<Object> eventProcessor; @SuppressWarnings("unchecked") public TypedEventTask(String topic, EventConverter eventData, TypedEventHandler<?> eventProcessor, - Class<?> targetEventClass) { + TypeData targetEventClass) { super(); this.topic = topic; this.targetEventClass = targetEventClass; diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/AriesTypedEvents.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/AriesTypedEvents.java new file mode 100644 index 0000000..f17d491 --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/AriesTypedEvents.java @@ -0,0 +1,45 @@ +/* + * 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.spi; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.service.typedevent.TypedEventBus; + +@ProviderType +public interface AriesTypedEvents extends TypedEventBus { + + /** + * Register a global event converter for this event bus. Equivalent to + * {@link #registerGlobalEventConverter(CustomEventConverter, boolean)} passing + * <code>false</code> as the <em>force</em> flag. + * @param converter the converter to register + * @throws IllegalStateException if a converter has already been registered + */ + default public void registerGlobalEventConverter(CustomEventConverter converter) throws IllegalStateException { + registerGlobalEventConverter(converter, false); + } + + /** + * Register a global event converter for this event bus. + * @param converter - the converter to register + * @param force - if true then any existing converter will be replaced + * @throws IllegalStateException if <em>force</em> is <code>false</code> + * and a converter has already been registered + */ + public void registerGlobalEventConverter(CustomEventConverter converter, boolean force) throws IllegalStateException; + +} diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/CustomEventConverter.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/CustomEventConverter.java new file mode 100644 index 0000000..21a50f7 --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/CustomEventConverter.java @@ -0,0 +1,45 @@ +/* + * 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.spi; + +import java.lang.reflect.Type; +import java.util.Map; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A Custom Event Converter can be registered to provide customised + * conversion of event data. + */ +@ConsumerType +public interface CustomEventConverter { + /** + * Convert the supplied event to the target type + * @param event - the event to convert + * @param rawTarget - the target class + * @param genericTarget - the target type, including any generics information + * @return A converted event, or <code>null</code> if the event cannot be converted + */ + public <T> T toTypedEvent(Object event, Class<?> rawTarget, Type genericTarget); + + /** + * Convert the supplied event to an untyped event (nested maps of scalar types) + * @param event - the event to convert + * @return A converted event, or <code>null</code> if the event cannot be converted + */ + public Map<String, Object> toUntypedEvent(Object event); +} diff --git a/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/package-info.java b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/package-info.java new file mode 100644 index 0000000..2937eca --- /dev/null +++ b/org.apache.aries.typedevent.bus/src/main/java/org/apache/aries/typedevent/bus/spi/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ [email protected] +package org.apache.aries.typedevent.bus.spi; \ No newline at end of file diff --git a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventConverterTest.java b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventConverterTest.java index 98b4d19..089a3bf 100644 --- a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventConverterTest.java +++ b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/impl/EventConverterTest.java @@ -17,13 +17,19 @@ package org.apache.aries.typedevent.bus.impl; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Map; +import org.apache.aries.typedevent.bus.spi.CustomEventConverter; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.osgi.service.typedevent.TypedEventHandler; import org.osgi.util.converter.ConversionException; public class EventConverterTest { @@ -49,7 +55,15 @@ public class EventConverterTest { return holder; } } + + public static class ParameterizedEvent<T> { + public T parameterisedMessage; + } + + public static interface IntegerTestHandler extends TypedEventHandler<ParameterizedEvent<Integer>> {} + public static interface DoubleTestHandler extends TypedEventHandler<ParameterizedEvent<Double>> {} + public static class DoublyNestedEventHolderWithIssues { public NestedEventHolderNotAProperDTO event; } @@ -60,11 +74,12 @@ public class EventConverterTest { TestEvent te = new TestEvent(); te.message = "FOO"; - Map<String, ?> map = EventConverter.forTypedEvent(te).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(te, null).toUntypedEvent(); assertEquals("FOO", map.get("message")); - TestEvent testEvent = EventConverter.forUntypedEvent(map).toTypedEvent(TestEvent.class); + TestEvent testEvent = EventConverter.forUntypedEvent(map, null) + .toTypedEvent(new TypeData(TestEvent.class)); assertEquals(te.message, testEvent.message); @@ -79,13 +94,14 @@ public class EventConverterTest { NestedEventHolder holder = new NestedEventHolder(); holder.event = te; - Map<String, ?> map = EventConverter.forTypedEvent(holder).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(holder, null).toUntypedEvent(); @SuppressWarnings({ "unchecked" }) Map<String, Object> nested = (Map<String, Object>) map.get("event"); assertEquals("FOO", nested.get("message")); - NestedEventHolder testEvent = EventConverter.forUntypedEvent(map).toTypedEvent(NestedEventHolder.class); + NestedEventHolder testEvent = EventConverter.forUntypedEvent(map, null) + .toTypedEvent(new TypeData(NestedEventHolder.class)); assertEquals(te.message, testEvent.event.message); @@ -100,14 +116,15 @@ public class EventConverterTest { NestedEventHolder holder = new NestedEventHolder(); holder.event = te; - Map<String, ?> map = EventConverter.forTypedEvent(holder).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(holder, null).toUntypedEvent(); @SuppressWarnings("unchecked") Map<String, Object> nested = (Map<String, Object>) map.get("event"); assertEquals("FOO", nested.get("message")); try { - EventConverter.forUntypedEvent(map).toTypedEvent(DefaultVisibilityNestedEventHolder.class); + EventConverter.forUntypedEvent(map, null).toTypedEvent( + new TypeData(DefaultVisibilityNestedEventHolder.class)); fail("Should not succeed in creating a Default Visibility type"); } catch (ConversionException ce) { assertEquals(IllegalAccessException.class, ce.getCause().getClass()); @@ -123,14 +140,14 @@ public class EventConverterTest { NestedEventHolderNotAProperDTO holder = new NestedEventHolderNotAProperDTO(); holder.event = te; - Map<String, ?> map = EventConverter.forTypedEvent(holder).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(holder, null).toUntypedEvent(); @SuppressWarnings("unchecked") Map<String, Object> nested = (Map<String, Object>) map.get("event"); assertEquals("FOO", nested.get("message")); - NestedEventHolderNotAProperDTO testEvent = EventConverter.forUntypedEvent(map) - .toTypedEvent(NestedEventHolderNotAProperDTO.class); + NestedEventHolderNotAProperDTO testEvent = EventConverter.forUntypedEvent(map, null) + .toTypedEvent(new TypeData(NestedEventHolderNotAProperDTO.class)); assertEquals(te.message, testEvent.event.message); @@ -149,15 +166,15 @@ public class EventConverterTest { DoublyNestedEventHolderWithIssues doubleHolder = new DoublyNestedEventHolderWithIssues(); doubleHolder.event = holder; - Map<String, ?> map = EventConverter.forTypedEvent(doubleHolder).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(doubleHolder, null).toUntypedEvent(); Map<String, Object> nested = (Map<String, Object>) map.get("event"); assertTrue(nested.containsKey("event")); nested = (Map<String, Object>) nested.get("event"); assertEquals("FOO", nested.get("message")); - DoublyNestedEventHolderWithIssues testEvent = EventConverter.forUntypedEvent(map) - .toTypedEvent(DoublyNestedEventHolderWithIssues.class); + DoublyNestedEventHolderWithIssues testEvent = EventConverter.forUntypedEvent(map, null) + .toTypedEvent(new TypeData(DoublyNestedEventHolderWithIssues.class)); assertEquals(te.message, testEvent.event.event.message); @@ -172,17 +189,94 @@ public class EventConverterTest { NestedEventHolder holder = new NestedEventHolder(); holder.event = te; - Map<String, ?> map = EventConverter.forTypedEvent(holder).toUntypedEvent(); + Map<String, ?> map = EventConverter.forTypedEvent(holder, null).toUntypedEvent(); @SuppressWarnings("unchecked") Map<String, Object> nested = (Map<String, Object>) map.get("event"); assertEquals("FOO", nested.get("message")); - NestedEventHolderNotAProperDTO testEvent = EventConverter.forUntypedEvent(map) - .toTypedEvent(NestedEventHolderNotAProperDTO.class); + NestedEventHolderNotAProperDTO testEvent = EventConverter.forUntypedEvent(map, null) + .toTypedEvent(new TypeData(NestedEventHolderNotAProperDTO.class)); assertEquals(te.message, testEvent.event.message); } + @Test + public void testGenericFlattenAndReconstituteIntoADifferentType() { + + ParameterizedEvent<Integer> te = new ParameterizedEvent<>(); + te.parameterisedMessage = 42; + + Type integerType = ((ParameterizedType)IntegerTestHandler.class.getGenericInterfaces()[0]) + .getActualTypeArguments()[0]; + Type doubleType = ((ParameterizedType)DoubleTestHandler.class.getGenericInterfaces()[0]) + .getActualTypeArguments()[0]; + + EventConverter eventConverter = EventConverter.forTypedEvent(te, null); + + Map<String, ?> map = eventConverter.toUntypedEvent(); + assertEquals(42, map.get("parameterisedMessage")); + + ParameterizedEvent<Double> converted = eventConverter.toTypedEvent(new TypeData(doubleType)); + assertEquals(42d, converted.parameterisedMessage, 0.00001); + + ParameterizedEvent<Integer> testEvent = EventConverter.forUntypedEvent( + Map.of("parameterisedMessage", "17"), null) + .toTypedEvent(new TypeData(integerType)); + + assertEquals(17, testEvent.parameterisedMessage); + + } + + @Test + public void testCustomConverterRawTypes() { + + TestEvent te = new TestEvent(); + te.message = "FOO"; + CustomEventConverter cec = Mockito.mock(CustomEventConverter.class); + + Mockito.when(cec.toUntypedEvent(te)).thenReturn(Map.of("message", "BAR")); + Mockito.when(cec.toTypedEvent(te, String.class, String.class)).thenReturn("FOOBAR"); + Mockito.when(cec.toTypedEvent(te, TestEvent.class, TestEvent.class)).thenReturn("FIZZBUZZ"); + + EventConverter eventConverter = EventConverter.forTypedEvent(te, cec); + + Map<String, ?> map = eventConverter.toUntypedEvent(); + assertEquals("BAR", map.get("message")); + assertEquals("FOOBAR", eventConverter.toTypedEvent(new TypeData(String.class))); + + // Bypass the conversion if identity + assertSame(te, eventConverter.toTypedEvent(new TypeData(TestEvent.class))); + + } + + @Test + public void testCustomConverterWithGenerics() { + + ParameterizedEvent<Integer> te = new ParameterizedEvent<>(); + te.parameterisedMessage = 42; + + Type integerType = ((ParameterizedType)IntegerTestHandler.class.getGenericInterfaces()[0]) + .getActualTypeArguments()[0]; + Type doubleType = ((ParameterizedType)DoubleTestHandler.class.getGenericInterfaces()[0]) + .getActualTypeArguments()[0]; + + CustomEventConverter cec = Mockito.mock(CustomEventConverter.class); + + Mockito.when(cec.toUntypedEvent(te)).thenReturn(Map.of("parameterisedMessage", "21")); + Mockito.when(cec.toTypedEvent(te, ParameterizedEvent.class, integerType)).thenReturn(21d); + Mockito.when(cec.toTypedEvent(te, ParameterizedEvent.class, doubleType)).thenReturn(63d); + + EventConverter eventConverter = EventConverter.forTypedEvent(te, cec); + + Map<String, ?> map = eventConverter.toUntypedEvent(); + assertEquals("21", map.get("parameterisedMessage")); + + // Never bypass as we can't easily verify the generics + assertEquals(21d, eventConverter.toTypedEvent(new TypeData(integerType)), 0.00001); + assertEquals(63d, eventConverter.toTypedEvent(new TypeData(doubleType)), 0.00001); + + } + } diff --git a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/osgi/EventDeliveryIntegrationTest.java b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/osgi/EventDeliveryIntegrationTest.java index 96d0b65..923efc1 100644 --- a/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/osgi/EventDeliveryIntegrationTest.java +++ b/org.apache.aries.typedevent.bus/src/test/java/org/apache/aries/typedevent/bus/osgi/EventDeliveryIntegrationTest.java @@ -30,6 +30,8 @@ import org.apache.aries.typedevent.bus.common.TestEvent2; import org.apache.aries.typedevent.bus.common.TestEvent2.EventType; import org.apache.aries.typedevent.bus.common.TestEvent2Consumer; import org.apache.aries.typedevent.bus.common.TestEventConsumer; +import org.apache.aries.typedevent.bus.spi.AriesTypedEvents; +import org.apache.aries.typedevent.bus.spi.CustomEventConverter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,6 +75,9 @@ public class EventDeliveryIntegrationTest extends AbstractIntegrationTest { @Mock UntypedEventHandler untypedEventHandler, untypedEventHandler2; + + @Mock + CustomEventConverter customConverter; private AutoCloseable mocks; @@ -83,6 +88,7 @@ public class EventDeliveryIntegrationTest extends AbstractIntegrationTest { @AfterEach public void stop() throws Exception { + ((AriesTypedEvents) eventBus).registerGlobalEventConverter(null, true); mocks.close(); } @@ -325,4 +331,41 @@ public class EventDeliveryIntegrationTest extends AbstractIntegrationTest { } + /** + * Tests that events are delivered to untyped Event Handlers + * based on topic + * + * @throws InterruptedException + */ + @Test + public void testCustomEventReceiving() throws InterruptedException { + + TestEvent event = new TestEvent(); + event.message = "boo"; + + ((AriesTypedEvents) eventBus).registerGlobalEventConverter(customConverter); + + Mockito.when(customConverter.toUntypedEvent(event)).thenReturn(Map.of("message", "BOO")); + + Dictionary<String, Object> props = new Hashtable<>(); + props.put(TypedEventConstants.TYPED_EVENT_TOPICS, TEST_EVENT_TOPIC); + + regs.add(context.registerService(UntypedEventHandler.class, untypedEventHandler, props)); + + props = new Hashtable<>(); + + props.put(TypedEventConstants.TYPED_EVENT_TOPICS, TEST_EVENT_2_TOPIC); + + regs.add(context.registerService(UntypedEventHandler.class, untypedEventHandler2, props)); + + eventBus.deliver(event); + + Mockito.verify(untypedEventHandler, Mockito.timeout(1000)).notifyUntyped( + Mockito.eq(TEST_EVENT_TOPIC), Mockito.argThat(isUntypedTestEventWithMessage("BOO"))); + + Mockito.verify(untypedEventHandler2, Mockito.after(1000).never()).notifyUntyped( + Mockito.eq(TEST_EVENT_TOPIC), Mockito.argThat(isUntypedTestEventWithMessage("BOO"))); + + } + } \ No newline at end of file
