diff --git a/pom.xml b/pom.xml index 2b1ae1a10..d55384cc5 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ docs config-sources converters/json + utils/events diff --git a/utils/events/README.md b/utils/events/README.md new file mode 100644 index 000000000..d47fe8f71 --- /dev/null +++ b/utils/events/README.md @@ -0,0 +1,163 @@ +# Config events + +Util library for config sources that fire events on changes. + +## Usage + +```xml + + + io.smallrye.config + smallrye-config-events + XXXX + + +``` + +## The event + +The CDI Event is a `ChangeEvent` and contains the following fields: + +* String key +* Optional (String) oldValue +* String newValue +* Type type +* String fromSource + +There are 3 types: + +* NEW - When you create a new key and value (i.e. the key does not exist anywhere in any config source) +* UPDATE - When you update a value of an existing key (i.e. the key and value exist somewhere in a config source) +* REMOVE - When you remove the value from the source (and that changed the overall config) + +### Observing events: + +You can listen to all or some of these events, filtering by `type` and/or `key` and/or `source`, example: + +```java + + // Getting all config event + public void all(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL: Received a config change event: {0}", changeEvent); + } + + // Get only new values + public void newValue(@Observes @TypeFilter(Type.NEW) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW: Received a config change event: {0}", changeEvent); + } + + // Get only override values + public void overrideValue(@Observes @TypeFilter(Type.UPDATE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE: Received a config change event: {0}", changeEvent); + } + + // Get only revert values + public void revertValue(@Observes @TypeFilter(Type.REMOVE) ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE: Received a config change event: {0}", changeEvent); + } + + // Getting all config event when key is some.key + public void allForKey(@Observes @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for key [some.key]: Received a config change event: {0}", changeEvent); + } + + // Getting all config event when key is some.key for new events + public void newForKey(@Observes @TypeFilter(Type.NEW) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "NEW for key [some.key]: Received a config change event: {0}", changeEvent); + } + + // Getting all config event when key is some.key for override events + public void overrideForKey(@Observes @TypeFilter(Type.UPDATE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for key [some.key]: Received a config change event: {0}", changeEvent); + } + + // Getting all config event when key is some.key for revert events + public void revertForKey(@Observes @TypeFilter(Type.REMOVE) @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "REMOVE for key [some.key]: Received a config change event: {0}", changeEvent); + } + + // Getting all config events for a certain source + public void allForSource(@Observes @SourceFilter("MemoryConfigSource") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource]: Received a config change event: {0}", changeEvent); + } + + // Getting all config events for a certain source + public void allForSourceAndKey(@Observes @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "ALL for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); + } + + // Getting all config events for a certain source + public void overrideForSourceAndKey(@Observes @TypeFilter(Type.UPDATE) @SourceFilter("MemoryConfigSource") @KeyFilter("some.key") ChangeEvent changeEvent){ + log.log(Level.SEVERE, "UPDATE for source [MemoryConfigSource] and for key [some.key]: Received a config change event: {0}", changeEvent); + } + +``` + +Note: You can filter by including the `@TypeFilter` and/or the `@KeyFilter` and/or the `@SourceFilter`. + + +### Pattern matching on field. + +You might want to listen for fields that match a certain regex. + +Example, listen to all keys that starts with `some.`: + +```java + + @RegexFilter("^some\\..+") + public void allForPatternMatchOnKey(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on key: Received a config change event: {0}", changeEvent); + } + +``` + +By default, it will match on `key`, however you also listen on another field, +for example, listen to all `oldValue` that starts with `some.`: + +```java + + @RegexFilter(onField = Field.oldValue, value = "^some\\..+") + public void allForPatternMatchOnOldValue(@Observes ChangeEvent changeEvent){ + log.log(Level.SEVERE, "Pattern match on old value: Received a config change event: {0}", changeEvent); + } + +``` + +You can Match on the following fields of the `ChangeEvent` object: + +* key +* oldValue +* newValue +* fromSource + +## Implementing this for your own Config source + +An example of a source that uses this is [Memory Config source](https://github.com/smallrye/smallrye-config/tree/master/extensions/sources/memory) + +`io.smallrye.config.events.ChangeEventNotifier` is a bean that makes it easy to detect changes and fire the appropriate events. + +To use it in your own source: + +* Get a snapshot of the properties before the change. +* Get a snapshot of the properties after the change. +* Call `detectChangesAndFire` method: + +Example: + +```java + + Map before = new HashMap<>(memoryConfigSource.getProperties()); + memoryConfigSource.getProperties().remove(key); + Map after = new HashMap<>(memoryConfigSource.getProperties()); + ChangeEventNotifier.getInstance().detectChangesAndFire(before, after,MemoryConfigSource.NAME) + +``` + +or if you know the change and do not need detection: + +```java + + memoryConfigSource.getProperties().remove(key); + ChangeEventNotifier.getInstance().fire(new ChangeEvent(Type.REMOVE,key,getOptionalOldValue(oldValue),null,MemoryConfigSource.NAME)); + +``` \ No newline at end of file diff --git a/utils/events/pom.xml b/utils/events/pom.xml new file mode 100644 index 000000000..375a7f76f --- /dev/null +++ b/utils/events/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + io.smallrye + smallrye-config-parent + 1.3.9-SNAPSHOT + ../../ + + + io.smallrye.config + smallrye-config-events + + jar + SmallRye: MicroProfile Config Events + A library to make it easy to add config events to config sources + + + + javax.enterprise + cdi-api + provided + + + javax.annotation + javax.annotation-api + provided + + + + + junit + junit + test + + + io.smallrye + smallrye-config + test + + + org.jboss.arquillian.container + arquillian-weld-embedded + test + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + test + + + org.jboss.weld + weld-core-impl + test + + + \ No newline at end of file diff --git a/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java b/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java new file mode 100644 index 000000000..895b70f9d --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java @@ -0,0 +1,52 @@ +package io.smallrye.config.events; + +import java.io.Serializable; +import java.util.Optional; + +/** + * an Event on a config element + * + * @author Phillip Kruger + */ +public class ChangeEvent implements Serializable { + + private final Type type; + private final String key; + private final Optional oldValue; + private final String newValue; + private final String fromSource; + + public ChangeEvent(Type type, String key, Optional oldValue, String newValue, String fromSource) { + this.type = type; + this.key = key; + this.oldValue = oldValue; + this.newValue = newValue; + this.fromSource = fromSource; + } + + public Type getType() { + return type; + } + + public String getKey() { + return key; + } + + public Optional getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public String getFromSource() { + return fromSource; + } + + @Override + public String toString() { + return "ChangeEvent{" + "type=" + type + ", key=" + key + ", oldValue=" + oldValue + ", newValue=" + newValue + + ", fromSource=" + fromSource + '}'; + } +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java b/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java new file mode 100644 index 000000000..e5fd2d647 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java @@ -0,0 +1,96 @@ +package io.smallrye.config.events; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; + +/** + * Easy way to fire a change event + * + * @author Phillip Kruger + * + * This gets used from Config sources that is not in the CDI Context. So we can not @Inject a bean. + * For some reason, CDI.current() is only working on Payara, and not on Thorntail and OpenLiberty, so this ugly footwork + * is to + * get around that. + */ +@ApplicationScoped +public class ChangeEventNotifier { + + @Inject + private Event broadcaster; + + private static ChangeEventNotifier INSTANCE; + + public void init(@Observes @Initialized(ApplicationScoped.class) Object init) { + INSTANCE = this; + } + + public static ChangeEventNotifier getInstance() { + // return CDI.current().select(ChangeEventNotifier.class).get(); + return INSTANCE; + } + + public void detectChangesAndFire(Map before, Map after, String fromSource) { + List changes = new ArrayList<>(); + if (!before.equals(after)) { + Set> beforeEntries = before.entrySet(); + for (Map.Entry beforeEntry : beforeEntries) { + String key = beforeEntry.getKey(); + String oldValue = beforeEntry.getValue(); + if (after.containsKey(key)) { + String newValue = after.get(key); + // Value can be null ! + if ((oldValue != null && newValue == null) || + (newValue != null && oldValue == null) || + (newValue != null && oldValue != null && !newValue.equals(oldValue))) { + // Update + changes.add(new ChangeEvent(Type.UPDATE, key, getOptionalOldValue(oldValue), newValue, fromSource)); + } + after.remove(key); + } else { + // Removed. + changes.add(new ChangeEvent(Type.REMOVE, key, getOptionalOldValue(oldValue), null, fromSource)); + } + } + Set> newEntries = after.entrySet(); + for (Map.Entry newEntry : newEntries) { + // New + changes.add(new ChangeEvent(Type.NEW, newEntry.getKey(), Optional.empty(), newEntry.getValue(), fromSource)); + } + } + if (!changes.isEmpty()) + fire(changes); + } + + public void fire(ChangeEvent changeEvent) { + List annotationList = new ArrayList<>(); + annotationList.add(new TypeFilter.TypeFilterLiteral(changeEvent.getType())); + annotationList.add(new KeyFilter.KeyFilterLiteral(changeEvent.getKey())); + annotationList.add(new SourceFilter.SourceFilterLiteral(changeEvent.getFromSource())); + + broadcaster.select(annotationList.toArray(new Annotation[annotationList.size()])).fire(changeEvent); + } + + public void fire(List changeEvents) { + for (ChangeEvent changeEvent : changeEvents) { + fire(changeEvent); + } + } + + public Optional getOptionalOldValue(String oldValue) { + if (oldValue == null || oldValue.isEmpty()) + return Optional.empty(); + return Optional.of(oldValue); + } + +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java b/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java new file mode 100644 index 000000000..94f9c4153 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java @@ -0,0 +1,36 @@ +package io.smallrye.config.events; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +/** + * Filter the event on the key + * + * @author Phillip Kruger + */ +@Qualifier +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface KeyFilter { + String value(); + + class KeyFilterLiteral extends AnnotationLiteral implements KeyFilter { + private final String key; + + KeyFilterLiteral(String key) { + this.key = key; + } + + @Override + public String value() { + return this.key; + } + } +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java b/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java new file mode 100644 index 000000000..dcbf7bd36 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java @@ -0,0 +1,36 @@ +package io.smallrye.config.events; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +/** + * Filter by a config source + * + * @author Phillip Kruger + */ +@Qualifier +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SourceFilter { + String value(); + + class SourceFilterLiteral extends AnnotationLiteral implements SourceFilter { + private final String name; + + SourceFilterLiteral(String name) { + this.name = name; + } + + @Override + public String value() { + return this.name; + } + } +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/Type.java b/utils/events/src/main/java/io/smallrye/config/events/Type.java new file mode 100644 index 000000000..6e886ab16 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/Type.java @@ -0,0 +1,7 @@ +package io.smallrye.config.events; + +public enum Type { + NEW, + REMOVE, + UPDATE +} \ No newline at end of file diff --git a/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java b/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java new file mode 100644 index 000000000..1efb4e85c --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java @@ -0,0 +1,36 @@ +package io.smallrye.config.events; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +/** + * filter by change type + * + * @author Phillip Kruger + */ +@Qualifier +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TypeFilter { + Type value(); + + class TypeFilterLiteral extends AnnotationLiteral implements TypeFilter { + private final Type type; + + TypeFilterLiteral(Type type) { + this.type = type; + } + + @Override + public Type value() { + return this.type; + } + } +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java b/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java new file mode 100644 index 000000000..3de4fd11f --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java @@ -0,0 +1,13 @@ +package io.smallrye.config.events.regex; + +/** + * a field to apply a regex on + * + * @author Phillip Kruger + */ +public enum Field { + key, + oldValue, + newValue, + fromSource +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java new file mode 100644 index 000000000..e1b6c8064 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java @@ -0,0 +1,27 @@ +package io.smallrye.config.events.regex; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.enterprise.util.Nonbinding; +import javax.interceptor.InterceptorBinding; + +/** + * an interceptor that match the value to a regular expression + * + * @author Phillip Kruger + */ +@Inherited +@InterceptorBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RegexFilter { + @Nonbinding + String value(); + + @Nonbinding + Field onField() default Field.key; +} diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java new file mode 100644 index 000000000..f6a7ddb34 --- /dev/null +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java @@ -0,0 +1,76 @@ +package io.smallrye.config.events.regex; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.smallrye.config.events.ChangeEvent; + +@RegexFilter(value = "") +@Interceptor +@Priority(100) +public class RegexFilterInterceptor { + private static final Logger log = Logger.getLogger(RegexFilterInterceptor.class.getName()); + + @AroundInvoke + public Object observer(InvocationContext ctx) throws Exception { + + RegexFilter regexFilterAnnotation = ctx.getMethod().getAnnotation(RegexFilter.class); + Field onField = regexFilterAnnotation.onField(); + String regex = regexFilterAnnotation.value(); + + Optional posibleChangeEvent = getChangeEvent(ctx); + + if (posibleChangeEvent.isPresent()) { + ChangeEvent changeEvent = posibleChangeEvent.get(); + String value = getValueToApplyRegexOn(changeEvent, onField); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(value); + boolean b = matcher.matches(); + if (!b) + return null; + } else { + log.log(Level.WARNING, "Can not find ChangeEvent parameter for method {0}. @RegexFilter is being ignored", + ctx.getMethod().getName()); + } + return ctx.proceed(); + } + + private String getValueToApplyRegexOn(ChangeEvent changeEvent, Field onField) { + String value = null; + switch (onField) { + case key: + value = changeEvent.getKey(); + break; + case fromSource: + value = changeEvent.getFromSource(); + break; + case newValue: + value = changeEvent.getNewValue(); + break; + case oldValue: + value = changeEvent.getOldValue().orElse(""); + } + + return value; + } + + private Optional getChangeEvent(InvocationContext ctx) { + Object[] parameters = ctx.getParameters(); + + for (Object parameter : parameters) { + if (parameter.getClass().equals(ChangeEvent.class)) { + ChangeEvent changeEvent = (ChangeEvent) parameter; + return Optional.of(changeEvent); + } + } + return Optional.empty(); + } +} diff --git a/utils/events/src/main/resources/META-INF/beans.xml b/utils/events/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ad2a3b6ec --- /dev/null +++ b/utils/events/src/main/resources/META-INF/beans.xml @@ -0,0 +1,5 @@ + + diff --git a/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java b/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java new file mode 100644 index 000000000..6b7fb6a13 --- /dev/null +++ b/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java @@ -0,0 +1,116 @@ +package io.smallrye.config.events; + +import java.io.File; +import java.util.Optional; + +import javax.enterprise.event.Observes; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.smallrye.config.events.regex.RegexFilter; +import io.smallrye.config.events.regex.RegexFilterInterceptor; + +/** + * Testing that the events fire correctly + * + * @author Phillip Kruger + */ +@RunWith(Arquillian.class) +public class ChangeEventNotifierTest { + + @Deployment + public static WebArchive createDeployment() { + final File[] smallryeConfig = Maven.resolver() + .loadPomFromFile("pom.xml") + .resolve("io.smallrye:smallrye-config") + .withoutTransitivity().asFile(); + + return ShrinkWrap.create(WebArchive.class, "ChangeEventNotifierTest.war") + .addPackage(ChangeEventNotifier.class.getPackage()) + .addPackage(RegexFilterInterceptor.class.getPackage()) + .addAsLibraries(smallryeConfig) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void testNewType() { + ChangeEvent changeEvent = new ChangeEvent(Type.NEW, "test.key", Optional.empty(), "test value", "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testUpdateType() { + ChangeEvent changeEvent = new ChangeEvent(Type.UPDATE, "test.key", Optional.of("old value"), "test value", "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testRemoveType() { + ChangeEvent changeEvent = new ChangeEvent(Type.REMOVE, "test.key", Optional.of("old value"), null, "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testCertainKey() { + ChangeEvent changeEvent = new ChangeEvent(Type.UPDATE, "some.key", Optional.of("old value"), "test value", "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testCertainKeyAndUpdate() { + ChangeEvent changeEvent = new ChangeEvent(Type.UPDATE, "some.key", Optional.of("old value"), "test value", "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testCertainSource() { + ChangeEvent changeEvent = new ChangeEvent(Type.UPDATE, "some.key", Optional.of("old value"), "test value", + "SomeConfigSource"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + @Test + public void testRegex() { + ChangeEvent changeEvent = new ChangeEvent(Type.NEW, "testcase.key", Optional.empty(), "test value", "TestCase"); + ChangeEventNotifier.getInstance().fire(changeEvent); + } + + public void listenForNew(@Observes @TypeFilter(Type.NEW) ChangeEvent changeEvent) { + Assert.assertEquals("Expecting new type", Type.NEW, changeEvent.getType()); + } + + public void listenForUpdate(@Observes @TypeFilter(Type.UPDATE) ChangeEvent changeEvent) { + Assert.assertEquals("Expecting update type", Type.UPDATE, changeEvent.getType()); + } + + public void listenForRemove(@Observes @TypeFilter(Type.REMOVE) ChangeEvent changeEvent) { + Assert.assertEquals("Expecting remove type", Type.REMOVE, changeEvent.getType()); + } + + public void listenForCertainKey(@Observes @KeyFilter("some.key") ChangeEvent changeEvent) { + Assert.assertEquals("Expecting certain key", "some.key", changeEvent.getKey()); + } + + public void listenForCertainKeyAndUpdate( + @Observes @TypeFilter(Type.UPDATE) @KeyFilter("some.key") ChangeEvent changeEvent) { + Assert.assertEquals("Expecting certain key", "some.key", changeEvent.getKey()); + Assert.assertEquals("Expecting update type", Type.UPDATE, changeEvent.getType()); + } + + public void listenForCertainSource(@Observes @SourceFilter("SomeConfigSource") ChangeEvent changeEvent) { + Assert.assertEquals("Expecting certain config source", "SomeConfigSource", changeEvent.getFromSource()); + } + + @RegexFilter("^testcase\\..+") + public void listenForKeyPattern(@Observes ChangeEvent changeEvent) { + Assert.assertTrue("Expecting key to start with certain value", changeEvent.getKey().startsWith("testcase")); + } +}