Skip to content

Make it easier/more obvious to remove extensions from a cloud event #215

@johanhaleby

Description

@johanhaleby

I'm looking for a way to remove an extension from a CloudEvent. Something like this:

MyExtension myExtension = ..
CloudEvent cloudEventWithExtension =  new CloudEventBuilder().withExtension(myExtension).. .. .build();

var withoutExtension = CloudEventBuilder.v1(cloudEvent).removeExtension(myExtension).build();

or remove just a single extension property:

MyExtension myExtension = ..
CloudEvent cloudEventWithExtension =  new CloudEventBuilder().withExtension(myExtension).. .. .build();

var withoutExtension = CloudEventBuilder.v1(cloudEvent).removeExtension(MyExtension.PROPERTY_NAME1).build();

I actually don't know how to do this now. Here's an actual example from my project:

/**
 * A {@link CloudEvent} {@link Extension} that adds required extensions for Occurrent. These are:<br><br>
 *
 * <table>
 *     <tr><th>Key</th><th>Description</th></tr>
 *     <tr><td>{@value #STREAM_ID}</td><td>The id of a particular event stream</td></tr>
 *     <tr><td>{@value #STREAM_VERSION}</td><td>The version of an event in a particular event stream</td></tr>
 * </table>
 */
public class OccurrentCloudEventExtension implements Extension {
    public static final String STREAM_ID = "streamId";
    public static final String STREAM_VERSION = "streamVersion";

    static final Set<String> KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(STREAM_ID, STREAM_VERSION)));
    private String streamId;
    private long streamVersion;

    public OccurrentCloudEventExtension(String streamId, long streamVersion) {
        Objects.requireNonNull(streamId, "StreamId cannot be null");
        if (streamVersion < 1) {
            throw new IllegalArgumentException("Stream version cannot be less than 1");
        }
        this.streamId = streamId;
        this.streamVersion = streamVersion;
    }

    public static OccurrentCloudEventExtension occurrent(String streamId, long streamVersion) {
        return new OccurrentCloudEventExtension(streamId, streamVersion);
    }

    @Override
    public void readFrom(CloudEventExtensions extensions) {
        Object streamId = extensions.getExtension(STREAM_ID);
        if (streamId != null) {
            this.streamId = streamId.toString();
        }

        Object streamVersion = extensions.getExtension(STREAM_VERSION);
        if (streamVersion != null) {
            this.streamVersion = (long) streamVersion;
        }
    }

    @Override
    public Object getValue(String key) throws IllegalArgumentException {
        if (STREAM_ID.equals(key)) {
            return this.streamId;
        } else if (STREAM_VERSION.equals(key)) {
            return this.streamVersion;
        }
        throw new IllegalArgumentException(this.getClass().getSimpleName() + " doesn't expect the attribute key \"" + key + "\"");
    }

    @Override
    public Set<String> getKeys() {
        return KEYS;
    }
}

I've written a utility that's supposed to remove the extension:

/**
 * Utility class that removes all Occurrent extensions from a {@link CloudEvent}.
 */
public class OccurrentExtensionRemover {

    /**
     * Remove all occurrent extensions from a cloud event. This is useful, for example,
     * if you want to forward a {@link CloudEvent} that has been persisted into an Occurrent
     * event store but you don't want to occurrent extensions (metadata) to be included.
     *
     * @param cloudEvent The cloud event to remove Occurrent's {@code CloudEvent} extensions from.
     * @return A {@code CloudEvent} without Occurrents extension
     */
    public static CloudEvent removeOccurrentExtensions(CloudEvent cloudEvent) {
        Objects.requireNonNull(cloudEvent, CloudEvent.class.getSimpleName() + " cannot be null");
        io.cloudevents.core.v1.CloudEventBuilder b = CloudEventBuilder.v1(cloudEvent);
        OccurrentCloudEventExtension.KEYS.forEach(occurrentExtensionKey -> {
            b.setExtension(occurrentExtensionKey, (String) null);
        });
        return b.build();
    }
}

But this test fails:

class OccurrentExtensionRemoverTest {

    @Test
    void removes_all_occurrent_extensions() {
        // Given
        CloudEvent originalCloudEvent = new CloudEventBuilder()
                .withId("id")
                .withTime(ZonedDateTime.now())
                .withDataSchema(URI.create("urn:schema"))
                .withSource(URI.create("urn:test"))
                .withSubject("subject")
                .withType("type")
                .withData("text/plain", "hello".getBytes(UTF_8))
                .build();

        CloudEvent occurrentCloudEvent = new CloudEventBuilder(originalCloudEvent)
                .withExtension(new OccurrentCloudEventExtension("streamId", 1))
                .build();

        // When
        CloudEvent removedExtensionsFromOccurrentCloudEvent = OccurrentExtensionRemover.removeOccurrentExtensions(occurrentCloudEvent);

        // Then
        assertThat(removedExtensionsFromOccurrentCloudEvent).isEqualTo(originalCloudEvent);
    }
}

but this test fails:

org.opentest4j.AssertionFailedError: 
Expecting:
 <CloudEvent{id='id', source=urn:test, type='type', datacontenttype='text/plain', dataschema=urn:schema, subject='subject', time=2020-08-24T19:02:11.546+02:00[Europe/Stockholm], data=[104, 101, 108, 108, 111], extensions={streamId=null, streamVersion=null}}>
to be equal to:
 <CloudEvent{id='id', source=urn:test, type='type', datacontenttype='text/plain', dataschema=urn:schema, subject='subject', time=2020-08-24T19:02:11.546+02:00[Europe/Stockholm], data=[104, 101, 108, 108, 111], extensions={}}>
but was not.
Expected :CloudEvent{id='id', source=urn:test, type='type', datacontenttype='text/plain', dataschema=urn:schema, subject='subject', time=2020-08-24T19:02:11.546+02:00[Europe/Stockholm], data=[104, 101, 108, 108, 111], extensions={}}
Actual   :CloudEvent{id='id', source=urn:test, type='type', datacontenttype='text/plain', dataschema=urn:schema, subject='subject', time=2020-08-24T19:02:11.546+02:00[Europe/Stockholm], data=[104, 101, 108, 108, 111], extensions={streamId=null, streamVersion=null}}

Metadata

Metadata

Labels

bugSomething isn't workingenhancementNew feature or request

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions