Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Donation from MicroProfile-ext: Config Events #152

Merged
merged 3 commits into from
Aug 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<module>docs</module>
<module>config-sources</module>
<module>converters/json</module>
<module>utils/events</module>
</modules>

<dependencyManagement>
Expand Down
163 changes: 163 additions & 0 deletions utils/events/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Config events

Util library for config sources that fire events on changes.

## Usage

```xml

<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-events</artifactId>
<version>XXXX</version>
</dependency>

```

## 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<String,String> before = new HashMap<>(memoryConfigSource.getProperties());
memoryConfigSource.getProperties().remove(key);
Map<String,String> 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));

```
63 changes: 63 additions & 0 deletions utils/events/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-config-parent</artifactId>
<version>1.3.9-SNAPSHOT</version>
<relativePath>../../</relativePath>
</parent>

<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-events</artifactId>

<packaging>jar</packaging>
<name>SmallRye: MicroProfile Config Events</name>
<description>A library to make it easy to add config events to config sources</description>

<dependencies>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<scope>provided</scope>
</dependency>

<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-config</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-weld-embedded</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.smallrye.config.events;

import java.io.Serializable;
import java.util.Optional;

/**
* an Event on a config element
*
* @author <a href="mailto:phillip.kruger@redhat.com">Phillip Kruger</a>
*/
public class ChangeEvent implements Serializable {

private final Type type;
private final String key;
private final Optional<String> oldValue;
private final String newValue;
private final String fromSource;

public ChangeEvent(Type type, String key, Optional<String> 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<String> 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 + '}';
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:phillip.kruger@redhat.com">Phillip Kruger</a>
*
* 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<ChangeEvent> 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<String, String> before, Map<String, String> after, String fromSource) {
List<ChangeEvent> changes = new ArrayList<>();
if (!before.equals(after)) {
Set<Map.Entry<String, String>> beforeEntries = before.entrySet();
for (Map.Entry<String, String> 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<Map.Entry<String, String>> newEntries = after.entrySet();
for (Map.Entry<String, String> 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<Annotation> 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<ChangeEvent> changeEvents) {
for (ChangeEvent changeEvent : changeEvents) {
fire(changeEvent);
}
}

public Optional<String> getOptionalOldValue(String oldValue) {
if (oldValue == null || oldValue.isEmpty())
return Optional.empty();
return Optional.of(oldValue);
}

}
Loading