diff --git a/core/src/main/java/io/spine/core/Events.java b/core/src/main/java/io/spine/core/Events.java index 3ec4533c488..3a00f9c5f19 100644 --- a/core/src/main/java/io/spine/core/Events.java +++ b/core/src/main/java/io/spine/core/Events.java @@ -36,6 +36,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.spine.protobuf.AnyPacker.unpack; +import static io.spine.util.Exceptions.newIllegalStateException; import static io.spine.validate.Validate.checkNotEmptyOrBlank; /** @@ -217,6 +218,56 @@ public static boolean isExternal(EventContext context) { return context.getExternal(); } + /** + * Clears enrichments of the specified event. + * + *

Use this method to decrease a size of an event, if enrichments aren't important. + * + *

A result won't contain: + *

+ * + *

Enrichments will not be removed from second-level and deeper origins, + * because it's a heavy performance operation. + * + * @param event the event to clear enrichments + * @return the event without enrichments + */ + @Internal + public static Event clearEnrichments(Event event) { + final EventContext context = event.getContext(); + final EventContext.Builder resultContext = context.toBuilder() + .clearEnrichment(); + final EventContext.OriginCase originCase = resultContext.getOriginCase(); + switch (originCase) { + case EVENT_CONTEXT: + resultContext.setEventContext(context.getEventContext() + .toBuilder() + .clearEnrichment()); + break; + case REJECTION_CONTEXT: + resultContext.setRejectionContext(context.getRejectionContext() + .toBuilder() + .clearEnrichment()); + break; + case COMMAND_CONTEXT: + // Nothing to remove. + break; + case ORIGIN_NOT_SET: + // Does nothing because there is no origin for this event. + break; + default: + throw newIllegalStateException("Unsupported origin case is encountered: %s", + originCase); + } + final Event result = event.toBuilder() + .setContext(resultContext) + .build(); + return result; + } + /** * The stringifier of event IDs. */ diff --git a/core/src/main/proto/spine/core/enrichment.proto b/core/src/main/proto/spine/core/enrichment.proto index 016c67ffc67..78064114aae 100644 --- a/core/src/main/proto/spine/core/enrichment.proto +++ b/core/src/main/proto/spine/core/enrichment.proto @@ -34,7 +34,7 @@ import "google/protobuf/any.proto"; // Attributes with additional information (enrichment) for an outer message. // // A message can be enriched with one or more messages. For example, an `Event` can be enriched with -// information for easier building of user interface projections. A `Rejection` can be eriched with +// information for easier building of user interface projections. A `Rejection` can be enriched with // additional information on why the command was rejected. // // If message enrichment should not be performed (e.g. because of performance or security diff --git a/ext.gradle b/ext.gradle index fd53ed27c1b..ce21c93cf63 100644 --- a/ext.gradle +++ b/ext.gradle @@ -25,7 +25,7 @@ * as we want to manage the versions in a single source. */ -def final SPINE_VERSION = '0.9.77-SNAPSHOT' +def final SPINE_VERSION = '0.9.78-SNAPSHOT' ext { // The version of the modules in this project. diff --git a/server/src/main/java/io/spine/server/aggregate/AggregateStorage.java b/server/src/main/java/io/spine/server/aggregate/AggregateStorage.java index ebaa7c74546..165547112e9 100644 --- a/server/src/main/java/io/spine/server/aggregate/AggregateStorage.java +++ b/server/src/main/java/io/spine/server/aggregate/AggregateStorage.java @@ -38,6 +38,7 @@ import static com.google.common.collect.Lists.newLinkedList; import static com.google.protobuf.TextFormat.shortDebugString; import static com.google.protobuf.util.Timestamps.checkValid; +import static io.spine.core.Events.clearEnrichments; import static io.spine.util.Exceptions.newIllegalStateException; import static io.spine.validate.Validate.checkNotEmptyOrBlank; @@ -156,13 +157,17 @@ public void write(I id, AggregateStateRecord events) { /** * Writes an event to the storage by an aggregate ID. * + *

Before the storing, {@linkplain io.spine.core.Events#clearEnrichments(Event) enrichments} + * will be removed from the event. + * * @param id the aggregate ID * @param event the event to write */ void writeEvent(I id, Event event) { checkNotClosedAndArguments(id, event); - final AggregateEventRecord record = toStorageRecord(event); + final Event eventWithoutEnrichments = clearEnrichments(event); + final AggregateEventRecord record = toStorageRecord(eventWithoutEnrichments); writeRecord(id, record); } diff --git a/server/src/main/java/io/spine/server/event/EEntity.java b/server/src/main/java/io/spine/server/event/EEntity.java index ff285dd2145..a94a5a01046 100644 --- a/server/src/main/java/io/spine/server/event/EEntity.java +++ b/server/src/main/java/io/spine/server/event/EEntity.java @@ -23,7 +23,6 @@ import com.google.protobuf.Timestamp; import io.spine.annotation.Internal; import io.spine.core.Event; -import io.spine.core.EventContext; import io.spine.core.EventEnvelope; import io.spine.core.EventId; import io.spine.core.Events; @@ -34,10 +33,12 @@ import javax.annotation.Nullable; import java.util.Comparator; -import static io.spine.util.Exceptions.newIllegalStateException; +import static io.spine.core.Events.clearEnrichments; /** - * Stores an event. + * An entity for storing an event. + * + *

An underlying event doesn't contain {@linkplain Events#clearEnrichments(Event) enrichments}. * * @author Alexander Yevsyukov * @author Dmytro Dashenkov @@ -87,8 +88,8 @@ public int compare(EEntity e1, EEntity e2) { EEntity(Event event) { this(event.getId()); - final Event compactedEvent = compact(event); - updateState(compactedEvent); + final Event eventWithoutEnrichments = clearEnrichments(event); + updateState(eventWithoutEnrichments); } /** @@ -128,49 +129,4 @@ public String getType() { } return typeName.value(); } - - /** - * Obtains the compacted version of the event. - * - *

A compacted version doesn't contain: - *

- * - * @param event the event to compact - * @return the compacted event - */ - private static Event compact(Event event) { - final EventContext context = event.getContext(); - final EventContext.Builder resultContext = context.toBuilder() - .clearEnrichment(); - final EventContext.OriginCase originCase = resultContext.getOriginCase(); - switch (originCase) { - case EVENT_CONTEXT: - resultContext.setEventContext(context.getEventContext() - .toBuilder() - .clearOrigin() - .clearEnrichment()); - break; - case REJECTION_CONTEXT: - resultContext.setRejectionContext(context.getRejectionContext() - .toBuilder() - .clearEnrichment()); - break; - case COMMAND_CONTEXT: - // Does nothing. - break; - case ORIGIN_NOT_SET: - // Does nothing because there is no origin for this event. - break; - default: - throw newIllegalStateException("Unsupported origin case encountered: %s", - originCase); - } - return event.toBuilder() - .setContext(resultContext) - .build(); - } } diff --git a/server/src/test/java/io/spine/server/aggregate/AggregateStorageShould.java b/server/src/test/java/io/spine/server/aggregate/AggregateStorageShould.java index 28e5c4dbde5..0ffaa90b372 100644 --- a/server/src/test/java/io/spine/server/aggregate/AggregateStorageShould.java +++ b/server/src/test/java/io/spine/server/aggregate/AggregateStorageShould.java @@ -27,6 +27,8 @@ import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; import io.spine.core.Event; +import io.spine.core.EventContext; +import io.spine.core.RejectionContext; import io.spine.core.Version; import io.spine.server.aggregate.given.Given.StorageRecord; import io.spine.server.command.TestEventFactory; @@ -52,10 +54,12 @@ import static io.spine.Identifier.newUuid; import static io.spine.core.Versions.increment; import static io.spine.core.Versions.zero; +import static io.spine.core.given.GivenEnrichment.withOneAttribute; import static io.spine.server.aggregate.given.Given.StorageRecords.sequenceFor; import static io.spine.server.command.TestEventFactory.newInstance; import static io.spine.time.Durations2.seconds; import static io.spine.time.Time.getCurrentTime; +import static io.spine.validate.Validate.isDefault; import static java.lang.Integer.MAX_VALUE; import static java.util.Collections.reverse; import static org.junit.Assert.assertEquals; @@ -345,6 +349,65 @@ public void continue_history_reading_if_snapshot_was_not_found_in_first_batch() assertEquals(eventCountAfterSnapshot, stateRecord.getEventCount()); } + @Test + public void not_store_enrichment_for_EventContext() { + final EventContext enrichedContext = EventContext.newBuilder() + .setEnrichment(withOneAttribute()) + .build(); + final Event event = Event.newBuilder() + .setContext(enrichedContext) + .setMessage(Any.getDefaultInstance()) + .build(); + storage.writeEvent(id, event); + final EventContext loadedContext = storage.read(newReadRequest(id)) + .get() + .getEvent(0) + .getContext(); + assertTrue(isDefault(loadedContext.getEnrichment())); + } + + @Test + public void not_store_enrichment_for_origin_of_RejectionContext_type() { + final RejectionContext origin = RejectionContext.newBuilder() + .setEnrichment(withOneAttribute()) + .build(); + final EventContext context = EventContext.newBuilder() + .setRejectionContext(origin) + .build(); + final Event event = Event.newBuilder() + .setContext(context) + .setMessage(Any.getDefaultInstance()) + .build(); + storage.writeEvent(id, event); + final RejectionContext loadedOrigin = storage.read(newReadRequest(id)) + .get() + .getEvent(0) + .getContext() + .getRejectionContext(); + assertTrue(isDefault(loadedOrigin.getEnrichment())); + } + + @Test + public void not_store_enrichment_for_origin_of_EventContext_type() { + final EventContext origin = EventContext.newBuilder() + .setEnrichment(withOneAttribute()) + .build(); + final EventContext context = EventContext.newBuilder() + .setEventContext(origin) + .build(); + final Event event = Event.newBuilder() + .setContext(context) + .setMessage(Any.getDefaultInstance()) + .build(); + storage.writeEvent(id, event); + final EventContext loadedOrigin = storage.read(newReadRequest(id)) + .get() + .getEvent(0) + .getContext() + .getEventContext(); + assertTrue(isDefault(loadedOrigin.getEnrichment())); + } + @Test(expected = IllegalStateException.class) public void throw_exception_if_try_to_write_event_count_to_closed_storage() { close(storage); @@ -402,18 +465,18 @@ protected void writeAll(ProjectId id, Iterable records) { } private Iterator historyBackward() { - final AggregateReadRequest readRequest = new AggregateReadRequest<>(id, MAX_VALUE); + final AggregateReadRequest readRequest = newReadRequest(id); return storage.historyBackward(readRequest); } protected static final Function TO_EVENT = new Function() { - @Nullable // return null because an exception won't be propagated in this case - @Override - public Event apply(@Nullable AggregateEventRecord input) { - return (input == null) ? null : input.getEvent(); - } - }; + @Nullable // return null because an exception won't be propagated in this case + @Override + public Event apply(@Nullable AggregateEventRecord input) { + return (input == null) ? null : input.getEvent(); + } + }; private static Snapshot newSnapshot(Timestamp time) { return Snapshot.newBuilder() diff --git a/server/src/test/java/io/spine/server/event/EventStoreShould.java b/server/src/test/java/io/spine/server/event/EventStoreShould.java index 031a4d43d58..3a019624556 100644 --- a/server/src/test/java/io/spine/server/event/EventStoreShould.java +++ b/server/src/test/java/io/spine/server/event/EventStoreShould.java @@ -22,14 +22,11 @@ import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.MoreExecutors; -import com.google.protobuf.Any; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import io.grpc.stub.StreamObserver; import io.spine.core.ActorContext; import io.spine.core.CommandContext; -import io.spine.core.Enrichment; -import io.spine.core.Enrichment.Container; import io.spine.core.Event; import io.spine.core.EventContext; import io.spine.core.RejectionContext; @@ -51,11 +48,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import static com.google.common.collect.Sets.newConcurrentHashSet; -import static com.google.protobuf.Any.pack; import static com.google.protobuf.util.Timestamps.add; import static com.google.protobuf.util.Timestamps.subtract; +import static io.spine.core.given.GivenEnrichment.withOneAttribute; import static io.spine.grpc.StreamObservers.memoizingObserver; -import static io.spine.protobuf.TypeConverter.toMessage; import static io.spine.test.Verify.assertContainsAll; import static io.spine.test.Verify.assertSize; import static io.spine.time.Time.getCurrentTime; @@ -242,7 +238,7 @@ public void not_store_enrichment_for_EventContext() { final Event enriched = event.toBuilder() .setContext(event.getContext() .toBuilder() - .setEnrichment(newEnrichment())) + .setEnrichment(withOneAttribute())) .build(); eventStore.append(enriched); final MemoizingObserver observer = memoizingObserver(); @@ -256,7 +252,7 @@ public void not_store_enrichment_for_EventContext() { @Test public void not_store_enrichment_for_origin_of_RejectionContext_type() { final RejectionContext originContext = RejectionContext.newBuilder() - .setEnrichment(newEnrichment()) + .setEnrichment(withOneAttribute()) .build(); final Event event = projectCreated(Time.getCurrentTime()); final Event enriched = event.toBuilder() @@ -277,7 +273,7 @@ public void not_store_enrichment_for_origin_of_RejectionContext_type() { @Test public void not_store_enrichment_for_origin_of_EventContext_type() { final EventContext.Builder originContext = EventContext.newBuilder() - .setEnrichment(newEnrichment()); + .setEnrichment(withOneAttribute()); final Event event = projectCreated(Time.getCurrentTime()); final Event enriched = event.toBuilder() .setContext(event.getContext() @@ -294,29 +290,6 @@ public void not_store_enrichment_for_origin_of_EventContext_type() { assertTrue(isDefault(loadedOriginContext.getEnrichment())); } - @Test - public void not_store_nested_origins_for_EventContext_origin() { - final EventContext.Builder context = EventContext.newBuilder() - .setEnrichment(newEnrichment()); - final EventContext originContext = EventContext.newBuilder() - .setEventContext(context) - .build(); - final Event event = projectCreated(Time.getCurrentTime()); - final Event enriched = event.toBuilder() - .setContext(event.getContext() - .toBuilder() - .setEventContext(originContext)) - .build(); - eventStore.append(enriched); - final MemoizingObserver observer = memoizingObserver(); - eventStore.read(EventStreamQuery.getDefaultInstance(), observer); - final EventContext loadedOriginContext = observer.responses() - .get(0) - .getContext() - .getEventContext(); - assertTrue(isDefault(loadedOriginContext.getEventContext())); - } - /* * Test environment *********************/ @@ -337,16 +310,6 @@ private static void assertDone(AtomicBoolean done) { } } - private static Enrichment newEnrichment() { - final String key = "enrichment key"; - final Any value = pack(toMessage("enrichment value")); - return Enrichment.newBuilder() - .setContainer(Container.newBuilder() - .putItems(key, value) - .build()) - .build(); - } - private static class ResponseObserver implements StreamObserver { private final Collection resultStorage; diff --git a/testutil-core/src/main/java/io/spine/core/given/GivenEnrichment.java b/testutil-core/src/main/java/io/spine/core/given/GivenEnrichment.java new file mode 100644 index 00000000000..aefb4231f2f --- /dev/null +++ b/testutil-core/src/main/java/io/spine/core/given/GivenEnrichment.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.core.given; + +import com.google.protobuf.Any; +import io.spine.core.Enrichment; + +import static com.google.protobuf.Any.pack; +import static io.spine.Identifier.newUuid; +import static io.spine.protobuf.TypeConverter.toMessage; + +/** + * Factory methods to create {@code Enrichment} instances in test purposes. + * + * @author Dmytro Grankin + */ +public class GivenEnrichment { + + /** Prevents instantiation of this utility class. */ + private GivenEnrichment() {} + + /** + * Creates a new {@link Enrichment} with one {@linkplain Enrichment#getContainer() attribute}. + * + *

An enrichment attribute is invalid and has random values. + * + * @return a new enrichment instance + */ + public static Enrichment withOneAttribute() { + final String key = newUuid(); + final Any value = pack(toMessage(newUuid())); + final Enrichment result = Enrichment.newBuilder() + .setContainer(Enrichment.Container.newBuilder() + .putItems(key, value) + .build()) + .build(); + return result; + } +} diff --git a/testutil-core/src/test/java/io/spine/core/given/GivenEnrichmentShould.java b/testutil-core/src/test/java/io/spine/core/given/GivenEnrichmentShould.java new file mode 100644 index 00000000000..ac7bba8a12d --- /dev/null +++ b/testutil-core/src/test/java/io/spine/core/given/GivenEnrichmentShould.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.core.given; + +import com.google.protobuf.Any; +import io.spine.core.Enrichment; +import org.junit.Test; + +import java.util.Map; + +import static io.spine.core.given.GivenEnrichment.withOneAttribute; +import static io.spine.test.Tests.assertHasPrivateParameterlessCtor; +import static io.spine.test.Verify.assertSize; + +/** + * @author Dmytro Grankin + */ +public class GivenEnrichmentShould { + + @Test + public void have_private_utility_ctor() { + assertHasPrivateParameterlessCtor(GivenEnrichment.class); + } + + @Test + public void create_enrichment_with_one_attribute() { + final Enrichment enrichment = withOneAttribute(); + final Map enrichmentAttributes = enrichment.getContainer() + .getItems(); + assertSize(1, enrichmentAttributes); + } +}