From 4b440a2feecd33e1a9807e4780cbe94751f6c8b8 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:27:04 +0300 Subject: [PATCH 01/60] Add `when_updated` field to the `MarketSharesUpdated` event. --- .../main/proto/spine_examples/shareaware/market/events.proto | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model/src/main/proto/spine_examples/shareaware/market/events.proto b/model/src/main/proto/spine_examples/shareaware/market/events.proto index db9dbb5b..e3e0e525 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/events.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/events.proto @@ -38,6 +38,7 @@ option java_multiple_files = true; import "spine_examples/shareaware/identifiers.proto"; import "spine/money/money.proto"; import "spine_examples/shareaware/share.proto"; +import "google/protobuf/timestamp.proto"; // Shares have been obtained from the market. message SharesObtained { @@ -96,4 +97,7 @@ message MarketSharesUpdated { // Updated shares. repeated Share share = 2 [(required) = true]; + + // Time when shares were updated. + google.protobuf.Timestamp when_updated = 3 [(required) = true]; } From 16e968946a8f13ca5feafa547ee1e5d3ab3bf59d Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:27:53 +0300 Subject: [PATCH 02/60] Define ID for the `SharePriceMovement` projection. --- .../proto/spine_examples/shareaware/identifiers.proto | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/model/src/main/proto/spine_examples/shareaware/identifiers.proto b/model/src/main/proto/spine_examples/shareaware/identifiers.proto index 034ecc16..0964670e 100644 --- a/model/src/main/proto/spine_examples/shareaware/identifiers.proto +++ b/model/src/main/proto/spine_examples/shareaware/identifiers.proto @@ -36,6 +36,8 @@ option java_outer_classname = "IdentifiersProto"; option java_multiple_files = true; import "spine/core/user_id.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; // Identifies a watchlist. message WatchlistId { @@ -119,3 +121,12 @@ message ReplenishmentOperationId { SaleId sale = 2; } } + +message SharePriceMovementId { + + ShareId share = 1 [(required) = true, (column) = true]; + + google.protobuf.Duration time_range = 2 [(required) = true]; + + google.protobuf.Timestamp when_created = 3 [(required) = true]; +} From 0d37be0429e47cbc2689315a163aa60d29473b60 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:28:15 +0300 Subject: [PATCH 03/60] Define the `SharePriceMovement` projection. --- .../market/share_price_movement.proto | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto new file mode 100644 index 00000000..083dd284 --- /dev/null +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -0,0 +1,55 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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. + */ + +syntax = "proto3"; + +package spine_examples.shareaware.market; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.shareaware.spine.io"; +option java_package = "io.spine.examples.shareaware.market"; +option java_outer_classname = "SharesPriceProgressionProto"; +option java_multiple_files = true; + +import "spine_examples/shareaware/identifiers.proto"; +import "google/protobuf/timestamp.proto"; +import "spine/money/money.proto"; + +message SharePriceMovement { + option (entity) = {kind: PROJECTION}; + + SharePriceMovementId id = 1 [(column) = true]; + + repeated MovementPoint movement_point = 2 ; +} + +message MovementPoint { + + spine.money.Money price = 1 [(required) = true]; + + google.protobuf.Timestamp time = 2 [(required) = true]; +} From 96731259b49578a16d6cad923877b8c0a80af9d4 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:34:10 +0300 Subject: [PATCH 04/60] Set the `when_updated` field value in the `MarketDataProvider`. --- .../examples/shareaware/server/market/MarketDataProvider.java | 2 ++ .../examples/shareaware/server/market/given/MarketTestEnv.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/MarketDataProvider.java b/server/src/main/java/io/spine/examples/shareaware/server/market/MarketDataProvider.java index e598af1e..41cdc5ae 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/MarketDataProvider.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/MarketDataProvider.java @@ -39,6 +39,7 @@ import java.util.function.Consumer; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static io.spine.base.Time.currentTime; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -159,6 +160,7 @@ private MarketSharesUpdated emitEvent() { .newBuilder() .setMarket(MarketProcess.ID) .addAllShare(updatedShares) + .setWhenUpdated(currentTime()) .vBuild(); marketContext.emittedEvent(event, actor); return event; diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java index 7bffd1c6..5d0b9b6e 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java @@ -26,6 +26,7 @@ package io.spine.examples.shareaware.server.market.given; +import io.spine.base.Time; import io.spine.core.UserId; import io.spine.examples.shareaware.MarketId; import io.spine.examples.shareaware.PurchaseId; @@ -144,6 +145,7 @@ public static MarketSharesUpdated marketSharesUpdated() { .setMarket(MarketProcess.ID) .addShare(tesla()) .addShare(apple()) + .setWhenUpdated(Time.currentTime()) .vBuild(); } From 636f8f38d16a866f8ce9905f63ad5c5548ec59fb Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:34:50 +0300 Subject: [PATCH 05/60] Implement the `ProjectionReader`. --- .../shareaware/server/ProjectionReader.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt diff --git a/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt b/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt new file mode 100644 index 00000000..60f378b4 --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server + +import com.google.common.base.Preconditions +import com.google.common.collect.ImmutableList +import io.spine.base.EntityState +import io.spine.client.ActorRequestFactory +import io.spine.client.EntityStateWithVersion +import io.spine.client.Filter +import io.spine.client.Query +import io.spine.client.QueryResponse +import io.spine.core.ActorContext +import io.spine.grpc.MemoizingObserver +import io.spine.protobuf.AnyPacker +import io.spine.server.stand.Stand + +public class ProjectionReader ( + private val stand: Stand, + private val stateClass: Class +) { + + /** + * Reads projections that match the filter on behalf of the actor from the context. + */ + public fun read(ctx: ActorContext, vararg filters: Filter): ImmutableList { + Preconditions.checkNotNull(ctx) + val queryFactory = ActorRequestFactory + .fromContext(ctx) + .query() + val query = queryFactory + .select(stateClass) + .where(*filters) + .build() + return executeAndUnpackResponse(query) + } + + public fun readAll(ctx: ActorContext): ImmutableList { + Preconditions.checkNotNull(ctx) + val queryFactory = ActorRequestFactory + .fromContext(ctx) + .query() + val query = queryFactory + .select(stateClass) + .build() + return executeAndUnpackResponse(query) + } + + private fun executeAndUnpackResponse(query: Query): ImmutableList { + val observer = MemoizingObserver() + stand.execute(query, observer) + val response = observer.firstResponse() + return response + .messageList + .stream() + .map { state: EntityStateWithVersion -> + AnyPacker.unpack( + state.state, + stateClass + ) + } + .collect(ImmutableList.toImmutableList()) + } +} From f631abbf718085c83c5470737cdc5e81a0de862a Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:35:35 +0300 Subject: [PATCH 06/60] Implement the `SharePriceMovementProjection` class. --- .../market/SharePriceMovementProjection.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt new file mode 100644 index 00000000..2c60e400 --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market + +import io.spine.core.External +import io.spine.core.Subscribe +import io.spine.examples.shareaware.SharePriceMovementId +import io.spine.examples.shareaware.market.MovementPoint +import io.spine.examples.shareaware.market.SharePriceMovement +import io.spine.examples.shareaware.market.event.MarketSharesUpdated +import io.spine.examples.shareaware.share.Share +import io.spine.server.projection.Projection + +public class SharePriceMovementProjection : + Projection() { + + @Subscribe + public fun on(@External e: MarketSharesUpdated) { + val share = e.shareList.find { share: Share? -> share?.id == builder().id.share } + val point: MovementPoint = MovementPoint + .newBuilder() + .setPrice(share!!.price) + .setTime(e.whenUpdated) + .vBuild() + builder().addMovementPoint(point) + } +} From 733abcec44fd241721c1721ac9466b3309a9d699 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:37:52 +0300 Subject: [PATCH 07/60] Implement the repository for the `SharePriceMovementProjection`. --- .../market/SharePriceMovementRepository.kt | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt new file mode 100644 index 00000000..6ce85682 --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market + +import com.google.common.collect.ImmutableSet +import com.google.protobuf.Duration +import com.google.protobuf.Timestamp +import io.spine.base.Time.currentTime +import io.spine.client.Filters.eq +import io.spine.core.ActorContext +import io.spine.examples.shareaware.ShareId +import io.spine.examples.shareaware.SharePriceMovementId +import io.spine.examples.shareaware.market.SharePriceMovement +import io.spine.examples.shareaware.market.event.MarketSharesUpdated +import io.spine.examples.shareaware.server.ProjectionReader +import io.spine.examples.shareaware.share.Share +import io.spine.server.projection.ProjectionRepository +import io.spine.server.route.EventRouting + +public class SharePriceMovementRepository : + ProjectionRepository() { + + override fun setupEventRouting(routing: EventRouting) { + super.setupEventRouting(routing) + routing.route(MarketSharesUpdated::class.java) { event, context -> + updateMovements(event, context.actorContext()) + } + } + + private fun updateMovements( + event: MarketSharesUpdated, context: ActorContext + ): ImmutableSet { + val reader = ProjectionReader( + context().stand(), + SharePriceMovement::class.java + ) + var activeProjections = setOf() + event.shareList.forEach { share: Share -> + val shareField = SharePriceMovement.Field.id().share() + val priceMovements = reader.read(context, eq(shareField, share.id)) + if (priceMovements.isNotEmpty()) { + var activeProjection = priceMovements.find { + it.id.timeRange.greaterThen(currentTime().minus(it.id.whenCreated)) + }?.id + if (activeProjection == null) { + activeProjection = SharePriceMovementId + .newBuilder() + .buildWith(share.id) + } + activeProjections = activeProjections.plus(activeProjection) + } else { + val id = SharePriceMovementId + .newBuilder() + .buildWith(share.id) + activeProjections = activeProjections.plus(id) + } + } + return ImmutableSet.copyOf(activeProjections) + } + + private fun SharePriceMovementId.Builder.buildWith(share: ShareId): SharePriceMovementId { + return this + .setShare(share) + .setTimeRange( + Duration.newBuilder() + .setSeconds(60) + .build() + ) + .setWhenCreated(currentTime()) + .vBuild(); + } + + private fun Timestamp.minus(timestamp: Timestamp): Duration { + val duration = Duration.newBuilder(); + + duration.seconds = this.seconds - timestamp.seconds; + duration.nanos = this.nanos - timestamp.nanos; + + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1; + duration.nanos -= 1000000000; + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1; + duration.nanos += 1000000000; + } + val build = duration.build() + println("Duration $build") + return build + } + + private fun Duration.greaterThen(duration: Duration): Boolean { + if (this.seconds > duration.seconds) { + return true + } + if (this.seconds <= duration.seconds) { + return false + } + return this.nanos > duration.nanos + } +} From 429c2ae15a38c3bab450c764fcd53bdc518df7ab Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 7 Jul 2023 13:38:39 +0300 Subject: [PATCH 08/60] Add `SharePriceMovementRepository` to the `TradingContext`. --- .../io/spine/examples/shareaware/server/TradingContext.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java b/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java index 521df2f5..c2aaae53 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java @@ -32,6 +32,7 @@ import io.spine.examples.shareaware.server.investment.SharesSaleRepository; import io.spine.examples.shareaware.server.market.AvailableMarketSharesRepository; import io.spine.examples.shareaware.server.market.MarketProcess; +import io.spine.examples.shareaware.server.market.SharePriceMovementRepository; import io.spine.examples.shareaware.server.paymentgateway.PaymentGatewayProcess; import io.spine.examples.shareaware.server.wallet.WalletAggregate; import io.spine.examples.shareaware.server.wallet.WalletBalanceRepository; @@ -75,6 +76,7 @@ public static BoundedContextBuilder newBuilder() { .add(new SharesPurchaseRepository()) .add(new SharesSaleRepository()) .add(new InvestmentViewRepository()) - .add(new AvailableMarketSharesRepository()); + .add(new AvailableMarketSharesRepository()) + .add(new SharePriceMovementRepository()); } } From b200283a8402bcb67036504e2e8081be103e44d0 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Sun, 9 Jul 2023 23:39:13 +0300 Subject: [PATCH 09/60] Add `share` field to `SharePriceMovement` projection. --- .../shareaware/identifiers.proto | 2 +- .../market/share_price_movement.proto | 6 +- .../market/SharePriceMovementProjection.kt | 6 +- .../market/SharePriceMovementRepository.kt | 64 +++++++++++-------- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/identifiers.proto b/model/src/main/proto/spine_examples/shareaware/identifiers.proto index 0964670e..d294699e 100644 --- a/model/src/main/proto/spine_examples/shareaware/identifiers.proto +++ b/model/src/main/proto/spine_examples/shareaware/identifiers.proto @@ -124,7 +124,7 @@ message ReplenishmentOperationId { message SharePriceMovementId { - ShareId share = 1 [(required) = true, (column) = true]; + ShareId share = 1; google.protobuf.Duration time_range = 2 [(required) = true]; diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 083dd284..18272893 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -42,9 +42,11 @@ import "spine/money/money.proto"; message SharePriceMovement { option (entity) = {kind: PROJECTION}; - SharePriceMovementId id = 1 [(column) = true]; + SharePriceMovementId id = 1; - repeated MovementPoint movement_point = 2 ; + ShareId share = 2 [(required) = true, (column) = true]; + + repeated MovementPoint movement_point = 3 ; } message MovementPoint { diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt index 2c60e400..722d41d4 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt @@ -40,12 +40,14 @@ public class SharePriceMovementProjection : @Subscribe public fun on(@External e: MarketSharesUpdated) { - val share = e.shareList.find { share: Share? -> share?.id == builder().id.share } + val share = e.shareList.find { share: Share -> share.id == builder().id.share } val point: MovementPoint = MovementPoint .newBuilder() .setPrice(share!!.price) .setTime(e.whenUpdated) .vBuild() - builder().addMovementPoint(point) + builder() + .setShare(builder().id.share) + .addMovementPoint(point) } } diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt index 6ce85682..ec5208e4 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt @@ -47,49 +47,59 @@ public class SharePriceMovementRepository : override fun setupEventRouting(routing: EventRouting) { super.setupEventRouting(routing) routing.route(MarketSharesUpdated::class.java) { event, context -> - updateMovements(event, context.actorContext()) + routeToSharePriceMovements(event, context.actorContext()) } } - private fun updateMovements( + private fun routeToSharePriceMovements( event: MarketSharesUpdated, context: ActorContext ): ImmutableSet { val reader = ProjectionReader( context().stand(), SharePriceMovement::class.java ) - var activeProjections = setOf() + var activePriceMovementsIds = setOf() event.shareList.forEach { share: Share -> - val shareField = SharePriceMovement.Field.id().share() - val priceMovements = reader.read(context, eq(shareField, share.id)) - if (priceMovements.isNotEmpty()) { - var activeProjection = priceMovements.find { - it.id.timeRange.greaterThen(currentTime().minus(it.id.whenCreated)) - }?.id - if (activeProjection == null) { - activeProjection = SharePriceMovementId - .newBuilder() - .buildWith(share.id) - } - activeProjections = activeProjections.plus(activeProjection) + val shareField = SharePriceMovement.Field.share() + val sharePriceMovements = reader.read(context, eq(shareField, share.id)) + if (sharePriceMovements.isNotEmpty()) { + val activePriceMovementId = findActiveOrCreate(sharePriceMovements) + activePriceMovementsIds = activePriceMovementsIds.plus(activePriceMovementId) } else { - val id = SharePriceMovementId - .newBuilder() - .buildWith(share.id) - activeProjections = activeProjections.plus(id) + val id = createNewSharePriceMovementId(share.id) + activePriceMovementsIds = activePriceMovementsIds.plus(id) } } - return ImmutableSet.copyOf(activeProjections) + return ImmutableSet.copyOf(activePriceMovementsIds) + } + + private fun findActiveOrCreate( + sharePriceMovements: List + ): SharePriceMovementId { + var activeProjectionId = sharePriceMovements.find { priceMovement -> + val timeFromCreation = currentTime().minus(priceMovement.id.whenCreated) + priceMovement.id.timeRange.greaterThen(timeFromCreation) + }?.id + if (activeProjectionId == null) { + activeProjectionId = createNewSharePriceMovementId(sharePriceMovements[0].share) + } + return activeProjectionId + } + + private fun createNewSharePriceMovementId(share: ShareId): SharePriceMovementId { + return SharePriceMovementId + .newBuilder() + .buildWith(share) } private fun SharePriceMovementId.Builder.buildWith(share: ShareId): SharePriceMovementId { + val duration = Duration + .newBuilder() + .setSeconds(60) + .build() return this .setShare(share) - .setTimeRange( - Duration.newBuilder() - .setSeconds(60) - .build() - ) + .setTimeRange(duration) .setWhenCreated(currentTime()) .vBuild(); } @@ -107,9 +117,7 @@ public class SharePriceMovementRepository : duration.seconds -= 1; duration.nanos += 1000000000; } - val build = duration.build() - println("Duration $build") - return build + return duration.build() } private fun Duration.greaterThen(duration: Duration): Boolean { From 42069ce4b0d1f2217598e83c91f31470745e3ba9 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Sun, 9 Jul 2023 23:45:10 +0300 Subject: [PATCH 10/60] Extract extensions for time-related types to a separate file. --- .../market/SharePriceMovementRepository.kt | 31 +--------- .../shareaware/server/market/TimeExts.kt | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt index ec5208e4..b13a5e6c 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt @@ -28,7 +28,6 @@ package io.spine.examples.shareaware.server.market import com.google.common.collect.ImmutableSet import com.google.protobuf.Duration -import com.google.protobuf.Timestamp import io.spine.base.Time.currentTime import io.spine.client.Filters.eq import io.spine.core.ActorContext @@ -59,8 +58,8 @@ public class SharePriceMovementRepository : SharePriceMovement::class.java ) var activePriceMovementsIds = setOf() + val shareField = SharePriceMovement.Field.share() event.shareList.forEach { share: Share -> - val shareField = SharePriceMovement.Field.share() val sharePriceMovements = reader.read(context, eq(shareField, share.id)) if (sharePriceMovements.isNotEmpty()) { val activePriceMovementId = findActiveOrCreate(sharePriceMovements) @@ -101,32 +100,6 @@ public class SharePriceMovementRepository : .setShare(share) .setTimeRange(duration) .setWhenCreated(currentTime()) - .vBuild(); - } - - private fun Timestamp.minus(timestamp: Timestamp): Duration { - val duration = Duration.newBuilder(); - - duration.seconds = this.seconds - timestamp.seconds; - duration.nanos = this.nanos - timestamp.nanos; - - if (duration.seconds < 0 && duration.nanos > 0) { - duration.seconds += 1; - duration.nanos -= 1000000000; - } else if (duration.seconds > 0 && duration.nanos < 0) { - duration.seconds -= 1; - duration.nanos += 1000000000; - } - return duration.build() - } - - private fun Duration.greaterThen(duration: Duration): Boolean { - if (this.seconds > duration.seconds) { - return true - } - if (this.seconds <= duration.seconds) { - return false - } - return this.nanos > duration.nanos + .vBuild() } } diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt new file mode 100644 index 00000000..c091c929 --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market + +import com.google.protobuf.Duration +import com.google.protobuf.Timestamp + +public fun Timestamp.minus(timestamp: Timestamp): Duration { + val duration = Duration.newBuilder() + + duration.seconds = this.seconds - timestamp.seconds + duration.nanos = this.nanos - timestamp.nanos + + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1 + duration.nanos -= 1000000000 + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1 + duration.nanos += 1000000000 + } + return duration.build() +} + +public fun Duration.greaterThen(duration: Duration): Boolean { + if (this.seconds > duration.seconds) { + return true + } + if (this.seconds <= duration.seconds) { + return false + } + return this.nanos > duration.nanos +} From 40f9db9c3796a3091b67f8c4abb1ebfe728061f3 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Sun, 9 Jul 2023 23:55:18 +0300 Subject: [PATCH 11/60] Rename the `time_range` field to the `activity_time` in the `SharePriceMovementId`. --- .../main/proto/spine_examples/shareaware/identifiers.proto | 2 +- .../shareaware/server/market/SharePriceMovementRepository.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/identifiers.proto b/model/src/main/proto/spine_examples/shareaware/identifiers.proto index d294699e..e2bf2dfb 100644 --- a/model/src/main/proto/spine_examples/shareaware/identifiers.proto +++ b/model/src/main/proto/spine_examples/shareaware/identifiers.proto @@ -126,7 +126,7 @@ message SharePriceMovementId { ShareId share = 1; - google.protobuf.Duration time_range = 2 [(required) = true]; + google.protobuf.Duration activity_time = 2 [(required) = true]; google.protobuf.Timestamp when_created = 3 [(required) = true]; } diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt index b13a5e6c..595ef79a 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt @@ -77,7 +77,7 @@ public class SharePriceMovementRepository : ): SharePriceMovementId { var activeProjectionId = sharePriceMovements.find { priceMovement -> val timeFromCreation = currentTime().minus(priceMovement.id.whenCreated) - priceMovement.id.timeRange.greaterThen(timeFromCreation) + priceMovement.id.activityTime.greaterThen(timeFromCreation) }?.id if (activeProjectionId == null) { activeProjectionId = createNewSharePriceMovementId(sharePriceMovements[0].share) @@ -98,7 +98,7 @@ public class SharePriceMovementRepository : .build() return this .setShare(share) - .setTimeRange(duration) + .setActivityTime(duration) .setWhenCreated(currentTime()) .vBuild() } From 282880cfd971c39397b68e978e0c0426381138cf Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 10 Jul 2023 00:03:22 +0300 Subject: [PATCH 12/60] Rename the `SharePriceMovement` to the `SharePriceMovementPerMinute`. --- .../shareaware/market/share_price_movement.proto | 2 +- .../shareaware/server/TradingContext.java | 4 ++-- ... => SharePriceMovementPerMinuteProjection.kt} | 8 +++++--- ... => SharePriceMovementPerMinuteRepository.kt} | 16 +++++++++------- 4 files changed, 17 insertions(+), 13 deletions(-) rename server/src/main/java/io/spine/examples/shareaware/server/market/{SharePriceMovementProjection.kt => SharePriceMovementPerMinuteProjection.kt} (89%) rename server/src/main/java/io/spine/examples/shareaware/server/market/{SharePriceMovementRepository.kt => SharePriceMovementPerMinuteRepository.kt} (89%) diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 18272893..38690f58 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -39,7 +39,7 @@ import "spine_examples/shareaware/identifiers.proto"; import "google/protobuf/timestamp.proto"; import "spine/money/money.proto"; -message SharePriceMovement { +message SharePriceMovementPerMinute { option (entity) = {kind: PROJECTION}; SharePriceMovementId id = 1; diff --git a/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java b/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java index c2aaae53..9297d0fc 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/TradingContext.java @@ -32,7 +32,7 @@ import io.spine.examples.shareaware.server.investment.SharesSaleRepository; import io.spine.examples.shareaware.server.market.AvailableMarketSharesRepository; import io.spine.examples.shareaware.server.market.MarketProcess; -import io.spine.examples.shareaware.server.market.SharePriceMovementRepository; +import io.spine.examples.shareaware.server.market.SharePriceMovementPerMinuteRepository; import io.spine.examples.shareaware.server.paymentgateway.PaymentGatewayProcess; import io.spine.examples.shareaware.server.wallet.WalletAggregate; import io.spine.examples.shareaware.server.wallet.WalletBalanceRepository; @@ -77,6 +77,6 @@ public static BoundedContextBuilder newBuilder() { .add(new SharesSaleRepository()) .add(new InvestmentViewRepository()) .add(new AvailableMarketSharesRepository()) - .add(new SharePriceMovementRepository()); + .add(new SharePriceMovementPerMinuteRepository()); } } diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt similarity index 89% rename from server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt rename to server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt index 722d41d4..eadd501b 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjection.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt @@ -30,13 +30,15 @@ import io.spine.core.External import io.spine.core.Subscribe import io.spine.examples.shareaware.SharePriceMovementId import io.spine.examples.shareaware.market.MovementPoint -import io.spine.examples.shareaware.market.SharePriceMovement +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.examples.shareaware.market.event.MarketSharesUpdated import io.spine.examples.shareaware.share.Share import io.spine.server.projection.Projection -public class SharePriceMovementProjection : - Projection() { +public class SharePriceMovementPerMinuteProjection : + Projection() { @Subscribe public fun on(@External e: MarketSharesUpdated) { diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt similarity index 89% rename from server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt rename to server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt index 595ef79a..afd79aa4 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt @@ -33,15 +33,17 @@ import io.spine.client.Filters.eq import io.spine.core.ActorContext import io.spine.examples.shareaware.ShareId import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.SharePriceMovement +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.examples.shareaware.market.event.MarketSharesUpdated import io.spine.examples.shareaware.server.ProjectionReader import io.spine.examples.shareaware.share.Share import io.spine.server.projection.ProjectionRepository import io.spine.server.route.EventRouting -public class SharePriceMovementRepository : - ProjectionRepository() { +public class SharePriceMovementPerMinuteRepository : + ProjectionRepository() { override fun setupEventRouting(routing: EventRouting) { super.setupEventRouting(routing) @@ -53,12 +55,12 @@ public class SharePriceMovementRepository : private fun routeToSharePriceMovements( event: MarketSharesUpdated, context: ActorContext ): ImmutableSet { - val reader = ProjectionReader( + val reader = ProjectionReader( context().stand(), - SharePriceMovement::class.java + SharePriceMovementPerMinute::class.java ) var activePriceMovementsIds = setOf() - val shareField = SharePriceMovement.Field.share() + val shareField = SharePriceMovementPerMinute.Field.share() event.shareList.forEach { share: Share -> val sharePriceMovements = reader.read(context, eq(shareField, share.id)) if (sharePriceMovements.isNotEmpty()) { @@ -73,7 +75,7 @@ public class SharePriceMovementRepository : } private fun findActiveOrCreate( - sharePriceMovements: List + sharePriceMovements: List ): SharePriceMovementId { var activeProjectionId = sharePriceMovements.find { priceMovement -> val timeFromCreation = currentTime().minus(priceMovement.id.whenCreated) From 261758b24dff4ae7e93a58f829f5fdf0ee8ee0a1 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 10 Jul 2023 13:14:20 +0300 Subject: [PATCH 13/60] Implement `isActive` extension for the `SharePriceMovementPerMinute` projection. --- .../SharePriceMovementPerMinuteRepository.kt | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt index afd79aa4..c7a04fe0 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt @@ -27,6 +27,7 @@ package io.spine.examples.shareaware.server.market import com.google.common.collect.ImmutableSet +import com.google.common.collect.ImmutableSet.toImmutableSet import com.google.protobuf.Duration import io.spine.base.Time.currentTime import io.spine.client.Filters.eq @@ -45,46 +46,40 @@ public class SharePriceMovementPerMinuteRepository : SharePriceMovementPerMinuteProjection, SharePriceMovementPerMinute>() { + private val sharePriceMovementActivityTime: Long = 60 + override fun setupEventRouting(routing: EventRouting) { super.setupEventRouting(routing) routing.route(MarketSharesUpdated::class.java) { event, context -> - routeToSharePriceMovements(event, context.actorContext()) + toSharePriceMovements(event, context.actorContext()) } } - private fun routeToSharePriceMovements( + private fun toSharePriceMovements( event: MarketSharesUpdated, context: ActorContext ): ImmutableSet { val reader = ProjectionReader( context().stand(), SharePriceMovementPerMinute::class.java ) - var activePriceMovementsIds = setOf() val shareField = SharePriceMovementPerMinute.Field.share() - event.shareList.forEach { share: Share -> + return event.shareList.stream().map { share: Share -> val sharePriceMovements = reader.read(context, eq(shareField, share.id)) if (sharePriceMovements.isNotEmpty()) { - val activePriceMovementId = findActiveOrCreate(sharePriceMovements) - activePriceMovementsIds = activePriceMovementsIds.plus(activePriceMovementId) + findActiveOrCreate(sharePriceMovements) } else { - val id = createNewSharePriceMovementId(share.id) - activePriceMovementsIds = activePriceMovementsIds.plus(id) + createNewSharePriceMovementId(share.id) } - } - return ImmutableSet.copyOf(activePriceMovementsIds) + }.collect(toImmutableSet()) } private fun findActiveOrCreate( sharePriceMovements: List ): SharePriceMovementId { - var activeProjectionId = sharePriceMovements.find { priceMovement -> - val timeFromCreation = currentTime().minus(priceMovement.id.whenCreated) - priceMovement.id.activityTime.greaterThen(timeFromCreation) - }?.id - if (activeProjectionId == null) { - activeProjectionId = createNewSharePriceMovementId(sharePriceMovements[0].share) - } - return activeProjectionId + val activeProjection = sharePriceMovements + .find { priceMovement -> priceMovement.isActive() } + ?: return createNewSharePriceMovementId(sharePriceMovements[0].share) + return activeProjection.id } private fun createNewSharePriceMovementId(share: ShareId): SharePriceMovementId { @@ -93,10 +88,15 @@ public class SharePriceMovementPerMinuteRepository : .buildWith(share) } + private fun SharePriceMovementPerMinute.isActive(): Boolean { + val timeFromCreation = currentTime().minus(this.id.whenCreated) + return this.id.activityTime.greaterThen(timeFromCreation) + } + private fun SharePriceMovementId.Builder.buildWith(share: ShareId): SharePriceMovementId { val duration = Duration .newBuilder() - .setSeconds(60) + .setSeconds(sharePriceMovementActivityTime) .build() return this .setShare(share) From d4c73f51818c9e3e269d234bb40f3ac37466b714 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 12:40:29 +0300 Subject: [PATCH 14/60] Add tests for the `SharePriceMovement` projection. --- .../shareaware/server/given/GivenShare.java | 13 +- .../SharePriceMovementProjectionTest.kt | 126 ++++++++++++++++++ .../server/market/given/MarketTestEnv.java | 27 ++-- .../given/SharesPriceMovementTestEnv.kt | 106 +++++++++++++++ 4 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt create mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt diff --git a/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java b/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java index e46e1a53..9a35c123 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java @@ -28,6 +28,7 @@ import io.spine.examples.shareaware.share.Share; import io.spine.examples.shareaware.ShareId; +import io.spine.money.Money; import static io.spine.examples.shareaware.given.GivenMoney.*; @@ -46,20 +47,28 @@ private GivenShare() { } public static Share tesla() { + return tesla(usd(20)); + } + + public static Share tesla(Money price) { return Share .newBuilder() .setId(teslaId) - .setPrice(usd(20)) + .setPrice(price) .setCompanyName("Tesla") .setCompanyLogo("testURL") .vBuild(); } public static Share apple() { + return apple(usd(20)); + } + + public static Share apple(Money price) { return Share .newBuilder() .setId(appleId) - .setPrice(usd(20)) + .setPrice(price) .setCompanyName("Apple") .setCompanyLogo("testURL") .vBuild(); diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt new file mode 100644 index 00000000..a2d0ca87 --- /dev/null +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market + +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.ProtoTruth +import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly +import io.spine.client.Filters.eq +import io.spine.examples.shareaware.SharePriceMovementId +import io.spine.examples.shareaware.given.GivenMoney.usd +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute +import io.spine.examples.shareaware.server.ProjectionReader +import io.spine.examples.shareaware.server.given.GivenShare.tesla +import io.spine.examples.shareaware.server.market.given.MarketTestEnv.marketSharesUpdated +import io.spine.examples.shareaware.server.market.given.ProjectionActivityTime +import io.spine.examples.shareaware.server.market.given.ShareFieldInProjection +import io.spine.examples.shareaware.server.market.given.actorContext +import io.spine.examples.shareaware.server.market.given.sharePriceMovementPerMinute +import io.spine.server.BoundedContext +import io.spine.server.BoundedContextBuilder +import io.spine.server.integration.ThirdPartyContext +import io.spine.testing.core.given.GivenUserId.newUuid +import java.time.Duration.ofSeconds +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`SharePriceMovement` projection should") +class SharePriceMovementProjectionTest { + + private lateinit var context: BoundedContext + + private lateinit var repository: SharePriceMovementPerMinuteRepository + + private lateinit var marketData: ThirdPartyContext + + private lateinit var reader: ProjectionReader + + @BeforeEach + fun setUp() { + repository = SharePriceMovementPerMinuteRepository() + context = BoundedContextBuilder + .assumingTests() + .add(repository) + .build() + marketData = ThirdPartyContext.singleTenant("MarketData") + reader = ProjectionReader(context.stand(), SharePriceMovementPerMinute::class.java) + } + + @Test + @DisplayName("accept the events only for the activity time") + fun createProjections() { + val shareId = tesla().id + + marketData.emittedEvent(marketSharesUpdated(), newUuid()) + sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) + val projectionsAfterFirstEmit = reader.read( + actorContext(), + eq(ShareFieldInProjection, shareId) + ) + assertThat(projectionsAfterFirstEmit.size).isEqualTo(1) + + marketData.emittedEvent(marketSharesUpdated(), newUuid()) + sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) + val projectionsAfterSecondEmit = reader.read( + actorContext(), + eq(ShareFieldInProjection, shareId) + ) + assertThat(projectionsAfterSecondEmit.size).isEqualTo(2) + + assertThat(projectionsAfterSecondEmit[0]).isNotEqualTo(projectionsAfterSecondEmit[1]) + assertThat(projectionsAfterSecondEmit[0].movementPointCount).isEqualTo(1) + assertThat(projectionsAfterSecondEmit[1].movementPointCount).isEqualTo(1) + } + + @Test + @DisplayName("construct the `MovementPoint`s from the `MarketSharesUpdate` event") + fun state() { + val shareId = tesla().id + val shareWithLowerPrice = tesla(usd(10)) + val shareWithHigherPrice = tesla(usd(20)) + val eventWithLowerPrice = marketSharesUpdated(shareWithLowerPrice) + val eventWithHigherPrice = marketSharesUpdated(shareWithHigherPrice) + + marketData.emittedEvent(eventWithLowerPrice, newUuid()) + marketData.emittedEvent(eventWithHigherPrice, newUuid()) + sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) + val projection = reader.read(actorContext(), eq(ShareFieldInProjection, shareId))[0] + val expectedProjection = sharePriceMovementPerMinute( + shareId, + shareWithLowerPrice.price, + eventWithLowerPrice.whenUpdated, + shareWithHigherPrice.price, + eventWithHigherPrice.whenUpdated + ) + + ProtoTruth.assertThat(projection) + .comparingExpectedFieldsOnly() + .isEqualTo(expectedProjection) + } +} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java index 5d0b9b6e..58f2a481 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java @@ -27,12 +27,11 @@ package io.spine.examples.shareaware.server.market.given; import io.spine.base.Time; -import io.spine.core.UserId; import io.spine.examples.shareaware.MarketId; import io.spine.examples.shareaware.PurchaseId; import io.spine.examples.shareaware.SaleId; -import io.spine.examples.shareaware.share.Share; import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.given.GivenMoney; import io.spine.examples.shareaware.market.AvailableMarketShares; import io.spine.examples.shareaware.market.Market; import io.spine.examples.shareaware.market.command.CloseMarket; @@ -42,13 +41,15 @@ import io.spine.examples.shareaware.market.event.MarketClosed; import io.spine.examples.shareaware.market.event.MarketOpened; import io.spine.examples.shareaware.market.event.MarketSharesUpdated; -import io.spine.examples.shareaware.market.rejection.Rejections.SharesCannotBeSoldOnMarket; import io.spine.examples.shareaware.market.rejection.Rejections.SharesCannotBeObtained; -import io.spine.examples.shareaware.given.GivenMoney; +import io.spine.examples.shareaware.market.rejection.Rejections.SharesCannotBeSoldOnMarket; import io.spine.examples.shareaware.server.market.MarketProcess; +import io.spine.examples.shareaware.share.Share; + +import java.util.Arrays; -import static io.spine.base.Identifier.*; -import static io.spine.examples.shareaware.server.given.GivenShare.*; +import static io.spine.examples.shareaware.server.given.GivenShare.apple; +import static io.spine.examples.shareaware.server.given.GivenShare.tesla; public final class MarketTestEnv { @@ -140,13 +141,17 @@ public static SellSharesOnMarket sellSharesOnMarket() { } public static MarketSharesUpdated marketSharesUpdated() { - return MarketSharesUpdated + return marketSharesUpdated(tesla(), apple()); + } + + public static MarketSharesUpdated marketSharesUpdated(Share... shares) { + MarketSharesUpdated.Builder builder = MarketSharesUpdated .newBuilder() .setMarket(MarketProcess.ID) - .addShare(tesla()) - .addShare(apple()) - .setWhenUpdated(Time.currentTime()) - .vBuild(); + .setWhenUpdated(Time.currentTime()); + Arrays.stream(shares) + .forEach(builder::addShare); + return builder.vBuild(); } public static AvailableMarketShares diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt new file mode 100644 index 00000000..95decbdd --- /dev/null +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market.given + +import com.google.protobuf.Duration +import com.google.protobuf.Timestamp +import io.spine.base.EntityColumn +import io.spine.base.Time.currentTime +import io.spine.core.ActorContext +import io.spine.core.TenantId +import io.spine.examples.shareaware.ShareId +import io.spine.examples.shareaware.SharePriceMovementId +import io.spine.examples.shareaware.market.MovementPoint +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute +import io.spine.money.Money +import io.spine.testing.core.given.GivenUserId.newUuid + +const val ProjectionActivityTime: Long = 60 + +val ShareFieldInProjection: EntityColumn = SharePriceMovementPerMinute.Column.share() + +fun sharePriceMovementPerMinute( + share: ShareId, + firstPrice: Money, + whenFirstPrice: Timestamp, + secondPrice: Money, + whenSecondPrice: Timestamp +): SharePriceMovementPerMinute { + val firstPoint = MovementPoint + .newBuilder() + .buildWith(firstPrice, whenFirstPrice) + val secondPoint = MovementPoint + .newBuilder() + .buildWith(secondPrice, whenSecondPrice) + val activityTime = Duration + .newBuilder() + .setSeconds(ProjectionActivityTime) + .build() + val sharePriceMovementId = SharePriceMovementId + .newBuilder() + .buildWith(share, activityTime) + return SharePriceMovementPerMinute + .newBuilder() + .setId(sharePriceMovementId) + .addMovementPoint(firstPoint) + .addMovementPoint(secondPoint) + .setShare(share) + .buildPartial() +} + +fun actorContext(): ActorContext { + val tenantId = TenantId + .newBuilder() + .setValue("SharePriceMovementTest") + .build() + return ActorContext + .newBuilder() + .setActor(newUuid()) + .setTenantId(tenantId) + .setTimestamp(currentTime()) + .vBuild() +} + +private fun MovementPoint.Builder.buildWith( + price: Money, + time: Timestamp +): MovementPoint { + return this + .setPrice(price) + .setTime(time) + .vBuild() +} + +private fun SharePriceMovementId.Builder.buildWith( + share: ShareId, + activityTime: Duration +): SharePriceMovementId { + return this + .setShare(share) + .setActivityTime(activityTime) + .buildPartial() +} From fb3b8d5c2a6e6d0bb528462ea0b51ecdde00007a Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:26:38 +0300 Subject: [PATCH 15/60] Add documentation to the `SharePriceMovementId`. --- .../main/proto/spine_examples/shareaware/identifiers.proto | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model/src/main/proto/spine_examples/shareaware/identifiers.proto b/model/src/main/proto/spine_examples/shareaware/identifiers.proto index e2bf2dfb..1e5f0c60 100644 --- a/model/src/main/proto/spine_examples/shareaware/identifiers.proto +++ b/model/src/main/proto/spine_examples/shareaware/identifiers.proto @@ -122,11 +122,15 @@ message ReplenishmentOperationId { } } +// Identifies the share price movement view. message SharePriceMovementId { + // The ID of the share which price movement to identify. ShareId share = 1; + // The time duration while the view will collect information about share price movements. google.protobuf.Duration activity_time = 2 [(required) = true]; + // The time when the share price movement view was created. google.protobuf.Timestamp when_created = 3 [(required) = true]; } From fd29c4c7c1ff91232ce2769a23991dfcccd39223 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:27:26 +0300 Subject: [PATCH 16/60] Write documentation for the `SharePriceMovementPerMinute`. --- .../shareaware/market/share_price_movement.proto | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 38690f58..f88559b2 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -32,26 +32,33 @@ import "spine/options.proto"; option (type_url_prefix) = "type.shareaware.spine.io"; option java_package = "io.spine.examples.shareaware.market"; -option java_outer_classname = "SharesPriceProgressionProto"; +option java_outer_classname = "SharesPriceMovementPerMinuteProto"; option java_multiple_files = true; import "spine_examples/shareaware/identifiers.proto"; import "google/protobuf/timestamp.proto"; import "spine/money/money.proto"; +// Displays the share price movement per minute on a timeline. message SharePriceMovementPerMinute { option (entity) = {kind: PROJECTION}; + // The ID of the share price movement. SharePriceMovementId id = 1; + // The share ID price movement of which to display. ShareId share = 2 [(required) = true, (column) = true]; + // The list of the price-to-time points. repeated MovementPoint movement_point = 3 ; } +// The share price at a particular time. message MovementPoint { + // The share price. spine.money.Money price = 1 [(required) = true]; + // The time when the share price was set. google.protobuf.Timestamp time = 2 [(required) = true]; } From fca923f14d5333c5dcf10b948a1dc297cde8b989 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:28:08 +0300 Subject: [PATCH 17/60] Write documentation for the `ProjectionReader` class. --- .../shareaware/server/ProjectionReader.kt | 18 ++++++------------ .../market/SharePriceMovementProjectionTest.kt | 4 +--- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt b/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt index 60f378b4..f5138089 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt @@ -39,7 +39,12 @@ import io.spine.grpc.MemoizingObserver import io.spine.protobuf.AnyPacker import io.spine.server.stand.Stand -public class ProjectionReader ( +/** + * Reader for projections in the bounded context. + * + * @param S type of the `Projection` to read. + */ +public class ProjectionReader ( private val stand: Stand, private val stateClass: Class ) { @@ -59,17 +64,6 @@ public class ProjectionReader ( return executeAndUnpackResponse(query) } - public fun readAll(ctx: ActorContext): ImmutableList { - Preconditions.checkNotNull(ctx) - val queryFactory = ActorRequestFactory - .fromContext(ctx) - .query() - val query = queryFactory - .select(stateClass) - .build() - return executeAndUnpackResponse(query) - } - private fun executeAndUnpackResponse(query: Query): ImmutableList { val observer = MemoizingObserver() stand.execute(query, observer) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt index a2d0ca87..3716a4bb 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt @@ -30,7 +30,6 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.ProtoTruth import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly import io.spine.client.Filters.eq -import io.spine.examples.shareaware.SharePriceMovementId import io.spine.examples.shareaware.given.GivenMoney.usd import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.examples.shareaware.server.ProjectionReader @@ -58,8 +57,7 @@ class SharePriceMovementProjectionTest { private lateinit var marketData: ThirdPartyContext - private lateinit var reader: ProjectionReader + private lateinit var reader: ProjectionReader @BeforeEach fun setUp() { From 4b99f5eb591876b8f92cbe4273b57336cf67a95f Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:30:47 +0300 Subject: [PATCH 18/60] Write documentation for the `SharePriceMovementPerMinuteRepository`. --- .../SharePriceMovementPerMinuteRepository.kt | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt index c7a04fe0..7660ea07 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt @@ -41,6 +41,9 @@ import io.spine.examples.shareaware.share.Share import io.spine.server.projection.ProjectionRepository import io.spine.server.route.EventRouting +/** + * Manages instances of the [SharePriceMovementPerMinuteProjection]. + */ public class SharePriceMovementPerMinuteRepository : ProjectionRepository { - val reader = ProjectionReader( + val reader = ProjectionReader( context().stand(), SharePriceMovementPerMinute::class.java ) @@ -73,6 +79,12 @@ public class SharePriceMovementPerMinuteRepository : }.collect(toImmutableSet()) } + /** + * Finds the ID of the `SharesPriceMovementPerMinute` projection which activity time + * has not yet expired, and it is still collecting data about share price movements. + * + * Whether the ID of the active projection is not found the new ID will be returned. + */ private fun findActiveOrCreate( sharePriceMovements: List ): SharePriceMovementId { @@ -82,23 +94,25 @@ public class SharePriceMovementPerMinuteRepository : return activeProjection.id } - private fun createNewSharePriceMovementId(share: ShareId): SharePriceMovementId { - return SharePriceMovementId - .newBuilder() - .buildWith(share) - } - + /** + * Determines whether is the `SharePriceMovementPerMinute` projection still + * collecting data about share price movements or not. + */ private fun SharePriceMovementPerMinute.isActive(): Boolean { val timeFromCreation = currentTime().minus(this.id.whenCreated) return this.id.activityTime.greaterThen(timeFromCreation) } - private fun SharePriceMovementId.Builder.buildWith(share: ShareId): SharePriceMovementId { + /** + * Returns the new ID for the `SharePriceMovementPerMinute` taking the ID of the share. + */ + private fun createNewSharePriceMovementId(share: ShareId): SharePriceMovementId { val duration = Duration .newBuilder() .setSeconds(sharePriceMovementActivityTime) .build() - return this + return SharePriceMovementId + .newBuilder() .setShare(share) .setActivityTime(duration) .setWhenCreated(currentTime()) From 51b35941e8392ee947c3ec2a25571d90e2860153 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:31:25 +0300 Subject: [PATCH 19/60] Rewrite the `Duration.greaterThan` extension. --- .../SharePriceMovementPerMinuteRepository.kt | 2 +- .../shareaware/server/market/TimeExts.kt | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt index 7660ea07..070fdd5a 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt @@ -100,7 +100,7 @@ public class SharePriceMovementPerMinuteRepository : */ private fun SharePriceMovementPerMinute.isActive(): Boolean { val timeFromCreation = currentTime().minus(this.id.whenCreated) - return this.id.activityTime.greaterThen(timeFromCreation) + return this.id.activityTime.greaterThan(timeFromCreation) } /** diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt index c091c929..33260aff 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt @@ -28,7 +28,11 @@ package io.spine.examples.shareaware.server.market import com.google.protobuf.Duration import com.google.protobuf.Timestamp +import io.spine.protobuf.Durations2 +/** + * Calculate the duration of time elapsed between two `Timestamp`s. + */ public fun Timestamp.minus(timestamp: Timestamp): Duration { val duration = Duration.newBuilder() @@ -45,12 +49,9 @@ public fun Timestamp.minus(timestamp: Timestamp): Duration { return duration.build() } -public fun Duration.greaterThen(duration: Duration): Boolean { - if (this.seconds > duration.seconds) { - return true - } - if (this.seconds <= duration.seconds) { - return false - } - return this.nanos > duration.nanos +/** + * Determines whether this `Duration` object is greater than the taken one. + */ +public fun Duration.greaterThan(duration: Duration): Boolean { + return Durations2.isGreaterThan(this, duration) } From f6547d8d4119e9acb4864a8e27f1567c14670412 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 16:33:43 +0300 Subject: [PATCH 20/60] Add documentation to the `SharePriceMovementPerMinuteProjection`. --- .../market/SharePriceMovementPerMinuteProjection.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt index eadd501b..ca3dd280 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt @@ -32,17 +32,19 @@ import io.spine.examples.shareaware.SharePriceMovementId import io.spine.examples.shareaware.market.MovementPoint import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.examples.shareaware.market.event.MarketSharesUpdated -import io.spine.examples.shareaware.share.Share import io.spine.server.projection.Projection +/** + * The view of the share price movements per minute. + */ public class SharePriceMovementPerMinuteProjection : Projection() { + SharePriceMovementPerMinute, + SharePriceMovementPerMinute.Builder>() { @Subscribe public fun on(@External e: MarketSharesUpdated) { - val share = e.shareList.find { share: Share -> share.id == builder().id.share } + val share = e.shareList.find { share -> share.id == builder().id.share } val point: MovementPoint = MovementPoint .newBuilder() .setPrice(share!!.price) From aa15d55516c0591a3a6eee365ce58bf36fc91e4b Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 17:09:40 +0300 Subject: [PATCH 21/60] Rename the `MovementPoint` to the `PriceAtTime`. --- .../shareaware/market/share_price_movement.proto | 4 ++-- .../SharePriceMovementPerMinuteProjection.kt | 6 +++--- .../market/SharePriceMovementProjectionTest.kt | 6 +++--- .../market/given/SharesPriceMovementTestEnv.kt | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index f88559b2..47ea63ae 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -50,11 +50,11 @@ message SharePriceMovementPerMinute { ShareId share = 2 [(required) = true, (column) = true]; // The list of the price-to-time points. - repeated MovementPoint movement_point = 3 ; + repeated PriceAtTime priceAtTime = 3 ; } // The share price at a particular time. -message MovementPoint { +message PriceAtTime { // The share price. spine.money.Money price = 1 [(required) = true]; diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt index ca3dd280..e8e2bd39 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt @@ -29,7 +29,7 @@ package io.spine.examples.shareaware.server.market import io.spine.core.External import io.spine.core.Subscribe import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.MovementPoint +import io.spine.examples.shareaware.market.PriceAtTime import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.examples.shareaware.market.event.MarketSharesUpdated import io.spine.server.projection.Projection @@ -45,13 +45,13 @@ public class SharePriceMovementPerMinuteProjection : @Subscribe public fun on(@External e: MarketSharesUpdated) { val share = e.shareList.find { share -> share.id == builder().id.share } - val point: MovementPoint = MovementPoint + val priceAtTime: PriceAtTime = PriceAtTime .newBuilder() .setPrice(share!!.price) .setTime(e.whenUpdated) .vBuild() builder() .setShare(builder().id.share) - .addMovementPoint(point) + .addPriceAtTime(priceAtTime) } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt index 3716a4bb..396bf2e3 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt @@ -92,12 +92,12 @@ class SharePriceMovementProjectionTest { assertThat(projectionsAfterSecondEmit.size).isEqualTo(2) assertThat(projectionsAfterSecondEmit[0]).isNotEqualTo(projectionsAfterSecondEmit[1]) - assertThat(projectionsAfterSecondEmit[0].movementPointCount).isEqualTo(1) - assertThat(projectionsAfterSecondEmit[1].movementPointCount).isEqualTo(1) + assertThat(projectionsAfterSecondEmit[0].priceAtTimeCount).isEqualTo(1) + assertThat(projectionsAfterSecondEmit[1].priceAtTimeCount).isEqualTo(1) } @Test - @DisplayName("construct the `MovementPoint`s from the `MarketSharesUpdate` event") + @DisplayName("construct the `PriceAtTime` from the `MarketSharesUpdate` event") fun state() { val shareId = tesla().id val shareWithLowerPrice = tesla(usd(10)) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt index 95decbdd..44b3b995 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt @@ -34,7 +34,7 @@ import io.spine.core.ActorContext import io.spine.core.TenantId import io.spine.examples.shareaware.ShareId import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.MovementPoint +import io.spine.examples.shareaware.market.PriceAtTime import io.spine.examples.shareaware.market.SharePriceMovementPerMinute import io.spine.money.Money import io.spine.testing.core.given.GivenUserId.newUuid @@ -50,10 +50,10 @@ fun sharePriceMovementPerMinute( secondPrice: Money, whenSecondPrice: Timestamp ): SharePriceMovementPerMinute { - val firstPoint = MovementPoint + val firstPriceAtTime = PriceAtTime .newBuilder() .buildWith(firstPrice, whenFirstPrice) - val secondPoint = MovementPoint + val secondPriceAtTime = PriceAtTime .newBuilder() .buildWith(secondPrice, whenSecondPrice) val activityTime = Duration @@ -66,8 +66,8 @@ fun sharePriceMovementPerMinute( return SharePriceMovementPerMinute .newBuilder() .setId(sharePriceMovementId) - .addMovementPoint(firstPoint) - .addMovementPoint(secondPoint) + .addPriceAtTime(firstPriceAtTime) + .addPriceAtTime(secondPriceAtTime) .setShare(share) .buildPartial() } @@ -85,10 +85,10 @@ fun actorContext(): ActorContext { .vBuild() } -private fun MovementPoint.Builder.buildWith( +private fun PriceAtTime.Builder.buildWith( price: Money, time: Timestamp -): MovementPoint { +): PriceAtTime { return this .setPrice(price) .setTime(time) From 46d3ff4f4604a703ff155b107c582998473d9f09 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 11 Jul 2023 19:27:37 +0300 Subject: [PATCH 22/60] Introduce a constant for the number of nanoseconds in one second in the `TimeExts`. --- .../io/spine/examples/shareaware/server/market/TimeExts.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt index 33260aff..09b064bd 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt @@ -30,6 +30,8 @@ import com.google.protobuf.Duration import com.google.protobuf.Timestamp import io.spine.protobuf.Durations2 +private const val NanosInSecond: Int = 1000000000 + /** * Calculate the duration of time elapsed between two `Timestamp`s. */ @@ -41,10 +43,10 @@ public fun Timestamp.minus(timestamp: Timestamp): Duration { if (duration.seconds < 0 && duration.nanos > 0) { duration.seconds += 1 - duration.nanos -= 1000000000 + duration.nanos -= NanosInSecond } else if (duration.seconds > 0 && duration.nanos < 0) { duration.seconds -= 1 - duration.nanos += 1000000000 + duration.nanos += NanosInSecond } return duration.build() } From c7b168930fa476de3930ff0867a5f0ebf972b9fc Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Thu, 13 Jul 2023 16:37:00 +0300 Subject: [PATCH 23/60] Rewrite all Kotlin files in the `server` module to Java. --- .../shareaware/server/ProjectionReader.kt | 82 ----------- ...SharePriceMovementPerMinuteProjection.java | 82 +++++++++++ .../SharePriceMovementPerMinuteProjection.kt | 57 -------- ...SharePriceMovementPerMinuteRepository.java | 94 +++++++++++++ .../SharePriceMovementPerMinuteRepository.kt | 121 ----------------- .../shareaware/server/market/TimeExts.kt | 59 -------- .../shareaware/server/ProjectionReader.java | 84 ++++++++++++ .../SharePriceMovementProjectionTest.java | 128 ++++++++++++++++++ .../SharePriceMovementProjectionTest.kt | 124 ----------------- .../given/SharesPriceMovementTestEnv.java | 127 +++++++++++++++++ .../given/SharesPriceMovementTestEnv.kt | 106 --------------- 11 files changed, 515 insertions(+), 549 deletions(-) delete mode 100644 server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java delete mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt create mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java delete mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt delete mode 100644 server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt create mode 100644 server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java create mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java delete mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt create mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java delete mode 100644 server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt diff --git a/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt b/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt deleted file mode 100644 index f5138089..00000000 --- a/server/src/main/java/io/spine/examples/shareaware/server/ProjectionReader.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server - -import com.google.common.base.Preconditions -import com.google.common.collect.ImmutableList -import io.spine.base.EntityState -import io.spine.client.ActorRequestFactory -import io.spine.client.EntityStateWithVersion -import io.spine.client.Filter -import io.spine.client.Query -import io.spine.client.QueryResponse -import io.spine.core.ActorContext -import io.spine.grpc.MemoizingObserver -import io.spine.protobuf.AnyPacker -import io.spine.server.stand.Stand - -/** - * Reader for projections in the bounded context. - * - * @param S type of the `Projection` to read. - */ -public class ProjectionReader ( - private val stand: Stand, - private val stateClass: Class -) { - - /** - * Reads projections that match the filter on behalf of the actor from the context. - */ - public fun read(ctx: ActorContext, vararg filters: Filter): ImmutableList { - Preconditions.checkNotNull(ctx) - val queryFactory = ActorRequestFactory - .fromContext(ctx) - .query() - val query = queryFactory - .select(stateClass) - .where(*filters) - .build() - return executeAndUnpackResponse(query) - } - - private fun executeAndUnpackResponse(query: Query): ImmutableList { - val observer = MemoizingObserver() - stand.execute(query, observer) - val response = observer.firstResponse() - return response - .messageList - .stream() - .map { state: EntityStateWithVersion -> - AnyPacker.unpack( - state.state, - stateClass - ) - } - .collect(ImmutableList.toImmutableList()) - } -} diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java new file mode 100644 index 00000000..b53b2b7d --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market; + +import io.spine.core.External; +import io.spine.core.Subscribe; +import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.SharePriceMovementId; +import io.spine.examples.shareaware.market.PriceAtTime; +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; +import io.spine.examples.shareaware.market.event.MarketSharesUpdated; +import io.spine.examples.shareaware.share.Share; +import io.spine.money.Money; +import io.spine.server.projection.Projection; + +import java.util.Collection; +import java.util.Optional; + +import static io.spine.util.Exceptions.newIllegalArgumentException; + +/** + * The view of the share price movements per minute. + */ +final class SharePriceMovementPerMinuteProjection extends + Projection { + + @Subscribe + void on(@External MarketSharesUpdated e) { + ShareId shareId = builder() + .getId() + .getShare(); + Money price = retrieveSharePrice(e.getShareList(), shareId); + PriceAtTime priceAtTime = PriceAtTime + .newBuilder() + .setPrice(price) + .setTime(e.getWhenUpdated()) + .vBuild(); + builder().setShare(shareId) + .addPriceAtTime(priceAtTime); + } + + /** + * Retrieves the share with provided ID from the provided {@code Collection}. + */ + private static Money retrieveSharePrice(Collection shares, ShareId id) { + Optional optionalShare = shares + .stream() + .filter(share -> share.getId().equals(id)) + .findAny(); + if (optionalShare.isEmpty()) { + throw newIllegalArgumentException("There is no share with provided ID in the list."); + } + return optionalShare.get() + .getPrice(); + } +} diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt deleted file mode 100644 index e8e2bd39..00000000 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server.market - -import io.spine.core.External -import io.spine.core.Subscribe -import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.PriceAtTime -import io.spine.examples.shareaware.market.SharePriceMovementPerMinute -import io.spine.examples.shareaware.market.event.MarketSharesUpdated -import io.spine.server.projection.Projection - -/** - * The view of the share price movements per minute. - */ -public class SharePriceMovementPerMinuteProjection : - Projection() { - - @Subscribe - public fun on(@External e: MarketSharesUpdated) { - val share = e.shareList.find { share -> share.id == builder().id.share } - val priceAtTime: PriceAtTime = PriceAtTime - .newBuilder() - .setPrice(share!!.price) - .setTime(e.whenUpdated) - .vBuild() - builder() - .setShare(builder().id.share) - .addPriceAtTime(priceAtTime) - } -} diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java new file mode 100644 index 00000000..841be2e2 --- /dev/null +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import io.spine.examples.shareaware.SharePriceMovementId; +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; +import io.spine.examples.shareaware.market.event.MarketSharesUpdated; +import io.spine.server.projection.ProjectionRepository; +import io.spine.server.route.EventRouting; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static io.spine.base.Time.currentTime; + +/** + * Manages instances of the {@link SharePriceMovementPerMinuteProjection}. + */ +public final class SharePriceMovementPerMinuteRepository extends + ProjectionRepository { + + private static final int SECONDS_IN_MINUTE = 60; + + private static final Duration PROJECTION_ACTIVE_TIME = Duration + .newBuilder() + .setSeconds(SECONDS_IN_MINUTE) + .build(); + + @OverridingMethodsMustInvokeSuper + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(MarketSharesUpdated.class, (event, context) -> toSharePriceMovements(event)); + } + + /** + * Routes the {@code MarketSharesUpdated} event to the {@code SharePriceMovementPerMinute} projections. + */ + private static ImmutableSet + toSharePriceMovements(MarketSharesUpdated event) { + return event.getShareList() + .stream() + .map(share -> { + Timestamp whenCreated = roundDownToNearestMinute(currentTime()); + return SharePriceMovementId + .newBuilder() + .setShare(share.getId()) + .setActivityTime(PROJECTION_ACTIVE_TIME) + .setWhenCreated(whenCreated) + .vBuild(); + }) + .collect(toImmutableSet()); + } + + /** + * Rounds the provided {@code Timestamp} down to the nearest minute. + */ + private static Timestamp roundDownToNearestMinute(Timestamp timestamp) { + long seconds = timestamp.getSeconds() - timestamp.getSeconds() % SECONDS_IN_MINUTE; + return Timestamp + .newBuilder() + .setSeconds(seconds) + .setNanos(0) + .build(); + } +} diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt deleted file mode 100644 index 070fdd5a..00000000 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server.market - -import com.google.common.collect.ImmutableSet -import com.google.common.collect.ImmutableSet.toImmutableSet -import com.google.protobuf.Duration -import io.spine.base.Time.currentTime -import io.spine.client.Filters.eq -import io.spine.core.ActorContext -import io.spine.examples.shareaware.ShareId -import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.SharePriceMovementPerMinute -import io.spine.examples.shareaware.market.event.MarketSharesUpdated -import io.spine.examples.shareaware.server.ProjectionReader -import io.spine.examples.shareaware.share.Share -import io.spine.server.projection.ProjectionRepository -import io.spine.server.route.EventRouting - -/** - * Manages instances of the [SharePriceMovementPerMinuteProjection]. - */ -public class SharePriceMovementPerMinuteRepository : - ProjectionRepository() { - - private val sharePriceMovementActivityTime: Long = 60 - - override fun setupEventRouting(routing: EventRouting) { - super.setupEventRouting(routing) - routing.route(MarketSharesUpdated::class.java) { event, context -> - toSharePriceMovements(event, context.actorContext()) - } - } - - /** - * Routes the `MarketSharesUpdated` event to the `SharePriceMovementPerMinute` projections. - */ - private fun toSharePriceMovements( - event: MarketSharesUpdated, context: ActorContext - ): ImmutableSet { - val reader = ProjectionReader( - context().stand(), - SharePriceMovementPerMinute::class.java - ) - val shareField = SharePriceMovementPerMinute.Field.share() - return event.shareList.stream().map { share: Share -> - val sharePriceMovements = reader.read(context, eq(shareField, share.id)) - if (sharePriceMovements.isNotEmpty()) { - findActiveOrCreate(sharePriceMovements) - } else { - createNewSharePriceMovementId(share.id) - } - }.collect(toImmutableSet()) - } - - /** - * Finds the ID of the `SharesPriceMovementPerMinute` projection which activity time - * has not yet expired, and it is still collecting data about share price movements. - * - * Whether the ID of the active projection is not found the new ID will be returned. - */ - private fun findActiveOrCreate( - sharePriceMovements: List - ): SharePriceMovementId { - val activeProjection = sharePriceMovements - .find { priceMovement -> priceMovement.isActive() } - ?: return createNewSharePriceMovementId(sharePriceMovements[0].share) - return activeProjection.id - } - - /** - * Determines whether is the `SharePriceMovementPerMinute` projection still - * collecting data about share price movements or not. - */ - private fun SharePriceMovementPerMinute.isActive(): Boolean { - val timeFromCreation = currentTime().minus(this.id.whenCreated) - return this.id.activityTime.greaterThan(timeFromCreation) - } - - /** - * Returns the new ID for the `SharePriceMovementPerMinute` taking the ID of the share. - */ - private fun createNewSharePriceMovementId(share: ShareId): SharePriceMovementId { - val duration = Duration - .newBuilder() - .setSeconds(sharePriceMovementActivityTime) - .build() - return SharePriceMovementId - .newBuilder() - .setShare(share) - .setActivityTime(duration) - .setWhenCreated(currentTime()) - .vBuild() - } -} diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt b/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt deleted file mode 100644 index 09b064bd..00000000 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/TimeExts.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server.market - -import com.google.protobuf.Duration -import com.google.protobuf.Timestamp -import io.spine.protobuf.Durations2 - -private const val NanosInSecond: Int = 1000000000 - -/** - * Calculate the duration of time elapsed between two `Timestamp`s. - */ -public fun Timestamp.minus(timestamp: Timestamp): Duration { - val duration = Duration.newBuilder() - - duration.seconds = this.seconds - timestamp.seconds - duration.nanos = this.nanos - timestamp.nanos - - if (duration.seconds < 0 && duration.nanos > 0) { - duration.seconds += 1 - duration.nanos -= NanosInSecond - } else if (duration.seconds > 0 && duration.nanos < 0) { - duration.seconds -= 1 - duration.nanos += NanosInSecond - } - return duration.build() -} - -/** - * Determines whether this `Duration` object is greater than the taken one. - */ -public fun Duration.greaterThan(duration: Duration): Boolean { - return Durations2.isGreaterThan(this, duration) -} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java b/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java new file mode 100644 index 00000000..b8c7175c --- /dev/null +++ b/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server; + +import com.google.common.collect.ImmutableList; +import io.spine.base.EntityState; +import io.spine.client.ActorRequestFactory; +import io.spine.client.Filter; +import io.spine.client.Query; +import io.spine.client.QueryResponse; +import io.spine.core.ActorContext; +import io.spine.grpc.MemoizingObserver; +import io.spine.server.stand.Stand; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.spine.protobuf.AnyPacker.unpack; + +/** + * Reader for projections in bounded context. + * + * @param type of the {@code Projection} to read + */ +public class ProjectionReader { + + private final Stand stand; + private final Class stateClass; + + public ProjectionReader(Stand stand, Class stateClass) { + this.stand = checkNotNull(stand); + this.stateClass = checkNotNull(stateClass); + } + + /** + * Reads projections that match the filter on behalf of the actor from the context. + */ + public ImmutableList read(ActorContext ctx, Filter... filters) { + checkNotNull(ctx); + var queryFactory = ActorRequestFactory + .fromContext(ctx) + .query(); + var query = queryFactory + .select(stateClass) + .where(filters) + .build(); + return executeAndUnpackResponse(query); + } + + private ImmutableList executeAndUnpackResponse(Query query) { + var observer = new MemoizingObserver(); + stand.execute(query, observer); + var response = observer.firstResponse(); + var result = response + .getMessageList() + .stream() + .map(state -> unpack(state.getState(), stateClass)) + .collect(toImmutableList()); + return result; + } +} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java new file mode 100644 index 00000000..833394ed --- /dev/null +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market; + +import com.google.common.truth.extensions.proto.ProtoTruth; +import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; +import io.spine.examples.shareaware.market.event.MarketSharesUpdated; +import io.spine.examples.shareaware.server.ProjectionReader; +import io.spine.examples.shareaware.share.Share; +import io.spine.server.BoundedContext; +import io.spine.server.BoundedContextBuilder; +import io.spine.server.integration.ThirdPartyContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static io.spine.client.Filters.eq; +import static io.spine.examples.shareaware.given.GivenMoney.usd; +import static io.spine.examples.shareaware.server.given.GivenShare.tesla; +import static io.spine.examples.shareaware.server.market.given.MarketTestEnv.marketSharesUpdated; +import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.ProjectionActivityTime; +import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.ShareFieldInProjection; +import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.actorContext; +import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.sharePriceMovementPerMinute; +import static io.spine.testing.core.given.GivenUserId.newUuid; +import static java.time.Duration.ofSeconds; + +@DisplayName("`SharePriceMovement` projection should") +final class SharePriceMovementProjectionTest { + + private ThirdPartyContext marketData; + + private ProjectionReader reader; + + @BeforeEach + void setUp() { + SharePriceMovementPerMinuteRepository repository = new SharePriceMovementPerMinuteRepository(); + BoundedContext context = BoundedContextBuilder + .assumingTests() + .add(repository) + .build(); + marketData = ThirdPartyContext.singleTenant("MarketData"); + reader = new ProjectionReader<>(context.stand(), SharePriceMovementPerMinute.class); + } + + @Test + @DisplayName("accept the events only for the activity time") + void createProjections() { + ShareId shareId = tesla().getId(); + + marketData.emittedEvent(marketSharesUpdated(), newUuid()); + sleepUninterruptibly(ofSeconds(60)); + List projectionsAfterFirstEmit = reader.read( + actorContext(), + eq(ShareFieldInProjection, shareId) + ); + assertThat(projectionsAfterFirstEmit.size()).isEqualTo(1); + + marketData.emittedEvent(marketSharesUpdated(), newUuid()); + sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); + List projectionsAfterSecondEmit = reader.read( + actorContext(), + eq(ShareFieldInProjection, shareId) + ); + assertThat(projectionsAfterSecondEmit.size()).isEqualTo(2); + + assertThat(projectionsAfterSecondEmit.get(0)) + .isNotEqualTo(projectionsAfterSecondEmit.get(1)); + assertThat(projectionsAfterSecondEmit.get(0) + .getPriceAtTimeCount()) + .isEqualTo(1); + assertThat(projectionsAfterSecondEmit.get(1) + .getPriceAtTimeCount()) + .isEqualTo(1); + } + + @Test + @DisplayName("construct the `PriceAtTime` from the `MarketSharesUpdate` event") + void state() { + ShareId shareId = tesla().getId(); + Share shareWithLowerPrice = tesla(usd(10)); + Share shareWithHigherPrice = tesla(usd(20)); + MarketSharesUpdated eventWithLowerPrice = marketSharesUpdated(shareWithLowerPrice); + MarketSharesUpdated eventWithHigherPrice = marketSharesUpdated(shareWithHigherPrice); + + marketData.emittedEvent(eventWithLowerPrice, newUuid()); + marketData.emittedEvent(eventWithHigherPrice, newUuid()); + sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); + SharePriceMovementPerMinute projection = reader + .read(actorContext(), eq(ShareFieldInProjection, shareId)) + .get(0); + SharePriceMovementPerMinute expectedProjection = + sharePriceMovementPerMinute(shareId, eventWithLowerPrice, eventWithHigherPrice); + + ProtoTruth.assertThat(projection) + .comparingExpectedFieldsOnly() + .isEqualTo(expectedProjection); + } +} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt deleted file mode 100644 index 396bf2e3..00000000 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server.market - -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.extensions.proto.ProtoTruth -import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly -import io.spine.client.Filters.eq -import io.spine.examples.shareaware.given.GivenMoney.usd -import io.spine.examples.shareaware.market.SharePriceMovementPerMinute -import io.spine.examples.shareaware.server.ProjectionReader -import io.spine.examples.shareaware.server.given.GivenShare.tesla -import io.spine.examples.shareaware.server.market.given.MarketTestEnv.marketSharesUpdated -import io.spine.examples.shareaware.server.market.given.ProjectionActivityTime -import io.spine.examples.shareaware.server.market.given.ShareFieldInProjection -import io.spine.examples.shareaware.server.market.given.actorContext -import io.spine.examples.shareaware.server.market.given.sharePriceMovementPerMinute -import io.spine.server.BoundedContext -import io.spine.server.BoundedContextBuilder -import io.spine.server.integration.ThirdPartyContext -import io.spine.testing.core.given.GivenUserId.newUuid -import java.time.Duration.ofSeconds -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test - -@DisplayName("`SharePriceMovement` projection should") -class SharePriceMovementProjectionTest { - - private lateinit var context: BoundedContext - - private lateinit var repository: SharePriceMovementPerMinuteRepository - - private lateinit var marketData: ThirdPartyContext - - private lateinit var reader: ProjectionReader - - @BeforeEach - fun setUp() { - repository = SharePriceMovementPerMinuteRepository() - context = BoundedContextBuilder - .assumingTests() - .add(repository) - .build() - marketData = ThirdPartyContext.singleTenant("MarketData") - reader = ProjectionReader(context.stand(), SharePriceMovementPerMinute::class.java) - } - - @Test - @DisplayName("accept the events only for the activity time") - fun createProjections() { - val shareId = tesla().id - - marketData.emittedEvent(marketSharesUpdated(), newUuid()) - sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) - val projectionsAfterFirstEmit = reader.read( - actorContext(), - eq(ShareFieldInProjection, shareId) - ) - assertThat(projectionsAfterFirstEmit.size).isEqualTo(1) - - marketData.emittedEvent(marketSharesUpdated(), newUuid()) - sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) - val projectionsAfterSecondEmit = reader.read( - actorContext(), - eq(ShareFieldInProjection, shareId) - ) - assertThat(projectionsAfterSecondEmit.size).isEqualTo(2) - - assertThat(projectionsAfterSecondEmit[0]).isNotEqualTo(projectionsAfterSecondEmit[1]) - assertThat(projectionsAfterSecondEmit[0].priceAtTimeCount).isEqualTo(1) - assertThat(projectionsAfterSecondEmit[1].priceAtTimeCount).isEqualTo(1) - } - - @Test - @DisplayName("construct the `PriceAtTime` from the `MarketSharesUpdate` event") - fun state() { - val shareId = tesla().id - val shareWithLowerPrice = tesla(usd(10)) - val shareWithHigherPrice = tesla(usd(20)) - val eventWithLowerPrice = marketSharesUpdated(shareWithLowerPrice) - val eventWithHigherPrice = marketSharesUpdated(shareWithHigherPrice) - - marketData.emittedEvent(eventWithLowerPrice, newUuid()) - marketData.emittedEvent(eventWithHigherPrice, newUuid()) - sleepUninterruptibly(ofSeconds(ProjectionActivityTime)) - val projection = reader.read(actorContext(), eq(ShareFieldInProjection, shareId))[0] - val expectedProjection = sharePriceMovementPerMinute( - shareId, - shareWithLowerPrice.price, - eventWithLowerPrice.whenUpdated, - shareWithHigherPrice.price, - eventWithHigherPrice.whenUpdated - ) - - ProtoTruth.assertThat(projection) - .comparingExpectedFieldsOnly() - .isEqualTo(expectedProjection) - } -} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java new file mode 100644 index 00000000..8d32c5ea --- /dev/null +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.server.market.given; + +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import io.spine.base.EntityColumn; +import io.spine.core.ActorContext; +import io.spine.core.TenantId; +import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.SharePriceMovementId; +import io.spine.examples.shareaware.market.PriceAtTime; +import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; +import io.spine.examples.shareaware.market.event.MarketSharesUpdated; +import io.spine.examples.shareaware.share.Share; +import io.spine.money.Money; + +import java.util.List; +import java.util.Optional; + +import static io.spine.base.Time.currentTime; +import static io.spine.testing.core.given.GivenUserId.newUuid; +import static io.spine.util.Exceptions.newIllegalArgumentException; + +public final class SharesPriceMovementTestEnv { + + public static final long ProjectionActivityTime = 60; + + public static final EntityColumn ShareFieldInProjection = + SharePriceMovementPerMinute.Column.share(); + + /** + * Prevents instantiation of this class. + */ + private SharesPriceMovementTestEnv() { + } + + public static SharePriceMovementPerMinute sharePriceMovementPerMinute( + ShareId shareId, + MarketSharesUpdated firstEvent, + MarketSharesUpdated secondEvent + ) { + Money firstPrice = retrieveShare(firstEvent.getShareList(), shareId).getPrice(); + Money secondPrice = retrieveShare(secondEvent.getShareList(), shareId).getPrice(); + PriceAtTime firstPriceAtTime = priceAtTime(firstPrice, firstEvent.getWhenUpdated()); + PriceAtTime secondPriceAtTime = priceAtTime(secondPrice, secondEvent.getWhenUpdated()); + Duration activityTime = Duration + .newBuilder() + .setSeconds(ProjectionActivityTime) + .build(); + SharePriceMovementId sharePriceMovementId = sharePriceMovementId(shareId, activityTime); + return SharePriceMovementPerMinute + .newBuilder() + .setId(sharePriceMovementId) + .addPriceAtTime(firstPriceAtTime) + .addPriceAtTime(secondPriceAtTime) + .setShare(shareId) + .buildPartial(); + } + + public static ActorContext actorContext() { + TenantId tenantId = TenantId + .newBuilder() + .setValue("SharePriceMovementTest") + .build(); + return ActorContext + .newBuilder() + .setActor(newUuid()) + .setTenantId(tenantId) + .setTimestamp(currentTime()) + .vBuild(); + } + + private static PriceAtTime priceAtTime(Money price, Timestamp time) { + return PriceAtTime + .newBuilder() + .setPrice(price) + .setTime(time) + .vBuild(); + } + + private static SharePriceMovementId sharePriceMovementId( + ShareId share, + Duration activityTime + ) { + return SharePriceMovementId + .newBuilder() + .setShare(share) + .setActivityTime(activityTime) + .buildPartial(); + } + + private static Share retrieveShare(List shares, ShareId id) { + Optional optionalShare = shares.stream() + .filter(share -> share.getId() + .equals(id)) + .findAny(); + if (optionalShare.isEmpty()) { + throw newIllegalArgumentException("There is no share with provided ID in the list."); + } + return optionalShare.get(); + } +} diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt deleted file mode 100644 index 44b3b995..00000000 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed 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 - * - * 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.examples.shareaware.server.market.given - -import com.google.protobuf.Duration -import com.google.protobuf.Timestamp -import io.spine.base.EntityColumn -import io.spine.base.Time.currentTime -import io.spine.core.ActorContext -import io.spine.core.TenantId -import io.spine.examples.shareaware.ShareId -import io.spine.examples.shareaware.SharePriceMovementId -import io.spine.examples.shareaware.market.PriceAtTime -import io.spine.examples.shareaware.market.SharePriceMovementPerMinute -import io.spine.money.Money -import io.spine.testing.core.given.GivenUserId.newUuid - -const val ProjectionActivityTime: Long = 60 - -val ShareFieldInProjection: EntityColumn = SharePriceMovementPerMinute.Column.share() - -fun sharePriceMovementPerMinute( - share: ShareId, - firstPrice: Money, - whenFirstPrice: Timestamp, - secondPrice: Money, - whenSecondPrice: Timestamp -): SharePriceMovementPerMinute { - val firstPriceAtTime = PriceAtTime - .newBuilder() - .buildWith(firstPrice, whenFirstPrice) - val secondPriceAtTime = PriceAtTime - .newBuilder() - .buildWith(secondPrice, whenSecondPrice) - val activityTime = Duration - .newBuilder() - .setSeconds(ProjectionActivityTime) - .build() - val sharePriceMovementId = SharePriceMovementId - .newBuilder() - .buildWith(share, activityTime) - return SharePriceMovementPerMinute - .newBuilder() - .setId(sharePriceMovementId) - .addPriceAtTime(firstPriceAtTime) - .addPriceAtTime(secondPriceAtTime) - .setShare(share) - .buildPartial() -} - -fun actorContext(): ActorContext { - val tenantId = TenantId - .newBuilder() - .setValue("SharePriceMovementTest") - .build() - return ActorContext - .newBuilder() - .setActor(newUuid()) - .setTenantId(tenantId) - .setTimestamp(currentTime()) - .vBuild() -} - -private fun PriceAtTime.Builder.buildWith( - price: Money, - time: Timestamp -): PriceAtTime { - return this - .setPrice(price) - .setTime(time) - .vBuild() -} - -private fun SharePriceMovementId.Builder.buildWith( - share: ShareId, - activityTime: Duration -): SharePriceMovementId { - return this - .setShare(share) - .setActivityTime(activityTime) - .buildPartial() -} From 7f91e96a65b3f2cd67184c911084c1ee66bad3a6 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Thu, 13 Jul 2023 16:46:12 +0300 Subject: [PATCH 24/60] Rename the `price_at_time` field in the `SharePriceMovementPerMinute` projection to the `point`. --- .../shareaware/market/share_price_movement.proto | 2 +- .../server/market/SharePriceMovementPerMinuteProjection.java | 2 +- .../server/market/SharePriceMovementProjectionTest.java | 4 ++-- .../server/market/given/SharesPriceMovementTestEnv.java | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 47ea63ae..a59b379f 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -50,7 +50,7 @@ message SharePriceMovementPerMinute { ShareId share = 2 [(required) = true, (column) = true]; // The list of the price-to-time points. - repeated PriceAtTime priceAtTime = 3 ; + repeated PriceAtTime point = 3; } // The share price at a particular time. diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index b53b2b7d..85f20c9a 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -62,7 +62,7 @@ void on(@External MarketSharesUpdated e) { .setTime(e.getWhenUpdated()) .vBuild(); builder().setShare(shareId) - .addPriceAtTime(priceAtTime); + .addPoint(priceAtTime); } /** diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java index 833394ed..e384e511 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java @@ -96,10 +96,10 @@ void createProjections() { assertThat(projectionsAfterSecondEmit.get(0)) .isNotEqualTo(projectionsAfterSecondEmit.get(1)); assertThat(projectionsAfterSecondEmit.get(0) - .getPriceAtTimeCount()) + .getPointCount()) .isEqualTo(1); assertThat(projectionsAfterSecondEmit.get(1) - .getPriceAtTimeCount()) + .getPointCount()) .isEqualTo(1); } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java index 8d32c5ea..b6d58994 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java @@ -76,8 +76,8 @@ public static SharePriceMovementPerMinute sharePriceMovementPerMinute( return SharePriceMovementPerMinute .newBuilder() .setId(sharePriceMovementId) - .addPriceAtTime(firstPriceAtTime) - .addPriceAtTime(secondPriceAtTime) + .addPoint(firstPriceAtTime) + .addPoint(secondPriceAtTime) .setShare(shareId) .buildPartial(); } From fc7a14b1389a5668162bd21c7ed89943bab2556e Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Thu, 13 Jul 2023 19:04:15 +0300 Subject: [PATCH 25/60] Rename the `time` field in the `PriceAtTime` entity to the `when`. --- .../shareaware/market/share_price_movement.proto | 4 ++-- .../server/market/SharePriceMovementPerMinuteProjection.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index a59b379f..1d5fda7c 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -59,6 +59,6 @@ message PriceAtTime { // The share price. spine.money.Money price = 1 [(required) = true]; - // The time when the share price was set. - google.protobuf.Timestamp time = 2 [(required) = true]; + // Point in time when the share price was set. + google.protobuf.Timestamp when = 2 [(required) = true]; } diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index 85f20c9a..542a5999 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -59,7 +59,7 @@ void on(@External MarketSharesUpdated e) { PriceAtTime priceAtTime = PriceAtTime .newBuilder() .setPrice(price) - .setTime(e.getWhenUpdated()) + .setWhen(e.getWhenUpdated()) .vBuild(); builder().setShare(shareId) .addPoint(priceAtTime); From 53ef674aec5fda26293210678bf86a2f3c1e7360 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Thu, 13 Jul 2023 19:30:28 +0300 Subject: [PATCH 26/60] Implement the `SharePriceMovement` mixin. --- .../shareaware/market/SharePriceMovement.java | 75 +++++++++++++++++++ .../market/share_price_movement.proto | 1 + 2 files changed, 76 insertions(+) create mode 100644 model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java diff --git a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java new file mode 100644 index 00000000..6074cc22 --- /dev/null +++ b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.market; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import io.spine.annotation.GeneratedMixin; +import io.spine.base.EntityState; +import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.SharePriceMovementId; + +/** + * Common interface for projections that displays the movements of the share price. + */ +@Immutable +@GeneratedMixin +public interface SharePriceMovement extends EntityState { + + /** + * Returns the ID of the `SharePriceMovement` projection. + */ + SharePriceMovementId getId(); + + /** + * Returns the time when the projection was created. + */ + default Timestamp whenCreated() { + return getId().getWhenCreated(); + } + + /** + * Returns the activity period of the projection. + * + *

The period when it is collecting data about the share price movements. + */ + default Duration activityTime() { + return getId().getActivityTime(); + } + + /** + * Returns the ID of the share which price movements the projection displays. + * + * @implNote do not use this field as a `column` to query the projection. + * This field is laying inside the compound ID, + * so it is not possible to query projections using this field. Use the full ID instead. + */ + default ShareId shareFromId() { + return getId().getShare(); + } +} diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 1d5fda7c..4ecf7b41 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -42,6 +42,7 @@ import "spine/money/money.proto"; // Displays the share price movement per minute on a timeline. message SharePriceMovementPerMinute { option (entity) = {kind: PROJECTION}; + option (is).java_type = "io.spine.examples.shareaware.market.SharePriceMovement"; // The ID of the share price movement. SharePriceMovementId id = 1; From 56732c5f2695d49f55463f395765073d9d05db08 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 14 Jul 2023 11:00:03 +0300 Subject: [PATCH 27/60] Rename the `setTime` to `setWhen` in the builder of the `PriceAtTime` entity. --- .../server/market/given/SharesPriceMovementTestEnv.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java index b6d58994..90c3d1a0 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java @@ -99,7 +99,7 @@ private static PriceAtTime priceAtTime(Money price, Timestamp time) { return PriceAtTime .newBuilder() .setPrice(price) - .setTime(time) + .setWhen(time) .vBuild(); } From 394c55db984f95b5b6205d5213b064ce21857947 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Fri, 14 Jul 2023 11:41:59 +0300 Subject: [PATCH 28/60] Omit the local variables types. --- ...SharePriceMovementPerMinuteProjection.java | 8 +++---- ...SharePriceMovementPerMinuteRepository.java | 4 ++-- .../SharePriceMovementProjectionTest.java | 24 +++++++++---------- .../server/market/given/MarketTestEnv.java | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index 542a5999..bb16a7f3 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -52,11 +52,11 @@ final class SharePriceMovementPerMinuteProjection extends @Subscribe void on(@External MarketSharesUpdated e) { - ShareId shareId = builder() + var shareId = builder() .getId() .getShare(); - Money price = retrieveSharePrice(e.getShareList(), shareId); - PriceAtTime priceAtTime = PriceAtTime + var price = retrieveSharePrice(e.getShareList(), shareId); + var priceAtTime = PriceAtTime .newBuilder() .setPrice(price) .setWhen(e.getWhenUpdated()) @@ -69,7 +69,7 @@ void on(@External MarketSharesUpdated e) { * Retrieves the share with provided ID from the provided {@code Collection}. */ private static Money retrieveSharePrice(Collection shares, ShareId id) { - Optional optionalShare = shares + var optionalShare = shares .stream() .filter(share -> share.getId().equals(id)) .findAny(); diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java index 841be2e2..81c0fe1c 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java @@ -69,7 +69,7 @@ protected void setupEventRouting(EventRouting routing) { return event.getShareList() .stream() .map(share -> { - Timestamp whenCreated = roundDownToNearestMinute(currentTime()); + var whenCreated = roundDownToNearestMinute(currentTime()); return SharePriceMovementId .newBuilder() .setShare(share.getId()) @@ -84,7 +84,7 @@ protected void setupEventRouting(EventRouting routing) { * Rounds the provided {@code Timestamp} down to the nearest minute. */ private static Timestamp roundDownToNearestMinute(Timestamp timestamp) { - long seconds = timestamp.getSeconds() - timestamp.getSeconds() % SECONDS_IN_MINUTE; + var seconds = timestamp.getSeconds() - timestamp.getSeconds() % SECONDS_IN_MINUTE; return Timestamp .newBuilder() .setSeconds(seconds) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java index e384e511..77bbdce0 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java @@ -63,8 +63,8 @@ final class SharePriceMovementProjectionTest { @BeforeEach void setUp() { - SharePriceMovementPerMinuteRepository repository = new SharePriceMovementPerMinuteRepository(); - BoundedContext context = BoundedContextBuilder + var repository = new SharePriceMovementPerMinuteRepository(); + var context = BoundedContextBuilder .assumingTests() .add(repository) .build(); @@ -75,11 +75,11 @@ void setUp() { @Test @DisplayName("accept the events only for the activity time") void createProjections() { - ShareId shareId = tesla().getId(); + var shareId = tesla().getId(); marketData.emittedEvent(marketSharesUpdated(), newUuid()); sleepUninterruptibly(ofSeconds(60)); - List projectionsAfterFirstEmit = reader.read( + var projectionsAfterFirstEmit = reader.read( actorContext(), eq(ShareFieldInProjection, shareId) ); @@ -87,7 +87,7 @@ void createProjections() { marketData.emittedEvent(marketSharesUpdated(), newUuid()); sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); - List projectionsAfterSecondEmit = reader.read( + var projectionsAfterSecondEmit = reader.read( actorContext(), eq(ShareFieldInProjection, shareId) ); @@ -106,19 +106,19 @@ void createProjections() { @Test @DisplayName("construct the `PriceAtTime` from the `MarketSharesUpdate` event") void state() { - ShareId shareId = tesla().getId(); - Share shareWithLowerPrice = tesla(usd(10)); - Share shareWithHigherPrice = tesla(usd(20)); - MarketSharesUpdated eventWithLowerPrice = marketSharesUpdated(shareWithLowerPrice); - MarketSharesUpdated eventWithHigherPrice = marketSharesUpdated(shareWithHigherPrice); + var shareId = tesla().getId(); + var shareWithLowerPrice = tesla(usd(10)); + var shareWithHigherPrice = tesla(usd(20)); + var eventWithLowerPrice = marketSharesUpdated(shareWithLowerPrice); + var eventWithHigherPrice = marketSharesUpdated(shareWithHigherPrice); marketData.emittedEvent(eventWithLowerPrice, newUuid()); marketData.emittedEvent(eventWithHigherPrice, newUuid()); sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); - SharePriceMovementPerMinute projection = reader + var projection = reader .read(actorContext(), eq(ShareFieldInProjection, shareId)) .get(0); - SharePriceMovementPerMinute expectedProjection = + var expectedProjection = sharePriceMovementPerMinute(shareId, eventWithLowerPrice, eventWithHigherPrice); ProtoTruth.assertThat(projection) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java index 58f2a481..441dab17 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/MarketTestEnv.java @@ -145,7 +145,7 @@ public static MarketSharesUpdated marketSharesUpdated() { } public static MarketSharesUpdated marketSharesUpdated(Share... shares) { - MarketSharesUpdated.Builder builder = MarketSharesUpdated + var builder = MarketSharesUpdated .newBuilder() .setMarket(MarketProcess.ID) .setWhenUpdated(Time.currentTime()); From b7e20cb657fa063bebe60777f371a7f41160c9e7 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 17 Jul 2023 16:55:06 +0300 Subject: [PATCH 29/60] Implement the `waitForNewState` method in the `EntitySubscription`. --- .../shareaware/server/e2e/given/E2EUser.java | 21 ++++++---- .../server/e2e/given/EntitySubscription.java | 41 ++++++++++++++++++- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java index caa2e2f5..739dcf30 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java @@ -114,7 +114,7 @@ public WalletId walletId() { */ public List looksAtAvailableShares() { var shares = availableMarketShares - .state() + .waitForNewState() .getShareList(); return shares; } @@ -123,7 +123,7 @@ public List looksAtAvailableShares() { * Describes the user's action to look at the investment. */ public InvestmentView looksAtInvestment() { - return investment.state(); + return investment.waitForNewState(); } /** @@ -132,8 +132,9 @@ public InvestmentView looksAtInvestment() { public WalletBalance replenishesWalletFor(Money amount) { var replenishWallet = replenishWallet(walletId, amount); + wallet.clearState(); post(replenishWallet); - var balanceAfterReplenishment = wallet.state(); + var balanceAfterReplenishment = wallet.waitForNewState(); var expectedBalance = walletBalanceWith(usd(500), walletId); assertThat(balanceAfterReplenishment).isEqualTo(expectedBalance); return balanceAfterReplenishment; @@ -157,8 +158,9 @@ public EitherOf2 purchase(Share share, int how var insufficientFunds = retrieveValueFrom(subscriptionOutcome); return EitherOf2.withB(insufficientFunds); } + wallet.clearState(); post(purchaseShares); - return EitherOf2.withA(wallet.state()); + return EitherOf2.withA(wallet.waitForNewState()); } /** @@ -176,8 +178,9 @@ public WalletBalance withdrawsAllMoney(WalletBalance balance) { */ private WalletBalance withdrawsMoney(Money amount) { var withdrawMoney = withdrawMoneyFrom(walletId, amount); + wallet.clearState(); post(withdrawMoney); - return wallet.state(); + return wallet.waitForNewState(); } /** @@ -198,9 +201,10 @@ private SubscriptionOutcome subscribeToEvent(Class S retrieveValueFrom(SubscriptionOutcome changedState) { try { + S value = changedState.future() + .get(10, SECONDS); cancel(changedState.subscription()); - return changedState.future() - .get(10, SECONDS); + return value; } catch (InterruptedException | ExecutionException | TimeoutException e) { throw illegalStateWithCauseOf(e); } @@ -216,8 +220,9 @@ private void cancel(Subscription subscription) { private void createWalletForUser() { var createWallet = createWallet(walletId); + wallet.clearState(); post(createWallet); - var initialBalance = wallet.state(); + var initialBalance = wallet.waitForNewState(); assertThat(initialBalance).isEqualTo(zeroWalletBalance(walletId)); } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index 57fa1d1d..d53217cf 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -32,8 +32,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import static io.spine.util.Exceptions.illegalStateWithCauseOf; +import static java.util.concurrent.TimeUnit.SECONDS; /** * Subscription for the {@code EntityState} changes. @@ -53,10 +55,26 @@ public class EntitySubscription { * Provides the current state of the subscribed entity. */ public S state() { - return entity.state(); + S state = entity.state(); + return state; + } + + /** + * Waits for a new state of the entity to arrive and return it. + */ + S waitForNewState() { + return entity.waitForNewState(); + } + + /** + * Clears the state of the entity. + */ + void clearState() { + entity.clearState(); } private static final class ObservedEntity { + private CompletableFuture future = new CompletableFuture<>(); private void setState(S value) { @@ -68,10 +86,29 @@ private void setState(S value) { private S state() { try { - return future.get(); + S value = future.get(10, SECONDS); + return value; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw illegalStateWithCauseOf(e); + } + } + + private S waitForNewState() { + try { + return future.whenComplete((value, error) -> { + if (error != null) { + throw illegalStateWithCauseOf(error); + } + }) + .orTimeout(10, SECONDS) + .get(); } catch (InterruptedException | ExecutionException e) { throw illegalStateWithCauseOf(e); } } + + private void clearState() { + future = new CompletableFuture<>(); + } } } From 3fc835541db00f8d7e7826bc26b93b8fa36d7f5c Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 10:22:40 +0300 Subject: [PATCH 30/60] Remove the redundant implementation note from the `SharePriceMovement` API documentation. --- .../examples/shareaware/market/SharePriceMovement.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java index 6074cc22..8b194c2c 100644 --- a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java +++ b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java @@ -42,7 +42,7 @@ public interface SharePriceMovement extends EntityState { /** - * Returns the ID of the `SharePriceMovement` projection. + * Returns the ID of the {@code SharePriceMovement} projection. */ SharePriceMovementId getId(); @@ -64,10 +64,6 @@ default Duration activityTime() { /** * Returns the ID of the share which price movements the projection displays. - * - * @implNote do not use this field as a `column` to query the projection. - * This field is laying inside the compound ID, - * so it is not possible to query projections using this field. Use the full ID instead. */ default ShareId shareFromId() { return getId().getShare(); From a9ea041639654bbc50ec9802e465889164ff990e Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 10:30:37 +0300 Subject: [PATCH 31/60] Rewrite documentation for the `SharePriceMovementId`. --- .../main/proto/spine_examples/shareaware/identifiers.proto | 4 ++-- .../shareaware/market/share_price_movement.proto | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/model/src/main/proto/spine_examples/shareaware/identifiers.proto b/model/src/main/proto/spine_examples/shareaware/identifiers.proto index 1e5f0c60..5bac51f7 100644 --- a/model/src/main/proto/spine_examples/shareaware/identifiers.proto +++ b/model/src/main/proto/spine_examples/shareaware/identifiers.proto @@ -125,10 +125,10 @@ message ReplenishmentOperationId { // Identifies the share price movement view. message SharePriceMovementId { - // The ID of the share which price movement to identify. + // The ID of share, for which the movements are collected. ShareId share = 1; - // The time duration while the view will collect information about share price movements. + // The duration during which the share price movements are collected. google.protobuf.Duration activity_time = 2 [(required) = true]; // The time when the share price movement view was created. diff --git a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto index 4ecf7b41..d6116b72 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/share_price_movement.proto @@ -47,7 +47,7 @@ message SharePriceMovementPerMinute { // The ID of the share price movement. SharePriceMovementId id = 1; - // The share ID price movement of which to display. + // The ID of the share, which price movement to display. ShareId share = 2 [(required) = true, (column) = true]; // The list of the price-to-time points. From 7dd23483017c166ec7161ad3cc399043279f7fc6 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 11:31:57 +0300 Subject: [PATCH 32/60] Include the ID and list of shares to the error message which throws the `SharesPriceMovementPerMinuteProjection`. --- .../SharePriceMovementPerMinuteProjection.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index bb16a7f3..7e422ead 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -38,17 +38,17 @@ import io.spine.server.projection.Projection; import java.util.Collection; -import java.util.Optional; import static io.spine.util.Exceptions.newIllegalArgumentException; +import static java.lang.String.format; /** * The view of the share price movements per minute. */ -final class SharePriceMovementPerMinuteProjection extends - Projection { +final class SharePriceMovementPerMinuteProjection + extends Projection { @Subscribe void on(@External MarketSharesUpdated e) { @@ -74,7 +74,9 @@ private static Money retrieveSharePrice(Collection shares, ShareId id) { .filter(share -> share.getId().equals(id)) .findAny(); if (optionalShare.isEmpty()) { - throw newIllegalArgumentException("There is no share with provided ID in the list."); + String errorMessage = + format("There is no share with provided ID - %s in the list - %s.", id, shares); + throw newIllegalArgumentException(errorMessage); } return optionalShare.get() .getPrice(); From 3af24fe248d1b02113baa3409e67e00f961b31d0 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 11:35:07 +0300 Subject: [PATCH 33/60] Adjust code style in the `SharePriceMovementPerMinuteRepository`. --- .../market/SharePriceMovementPerMinuteRepository.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java index 81c0fe1c..5d46f39d 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteRepository.java @@ -42,10 +42,10 @@ /** * Manages instances of the {@link SharePriceMovementPerMinuteProjection}. */ -public final class SharePriceMovementPerMinuteRepository extends - ProjectionRepository { +public final class SharePriceMovementPerMinuteRepository + extends ProjectionRepository { private static final int SECONDS_IN_MINUTE = 60; @@ -54,8 +54,8 @@ public final class SharePriceMovementPerMinuteRepository extends .setSeconds(SECONDS_IN_MINUTE) .build(); - @OverridingMethodsMustInvokeSuper @Override + @OverridingMethodsMustInvokeSuper protected void setupEventRouting(EventRouting routing) { super.setupEventRouting(routing); routing.route(MarketSharesUpdated.class, (event, context) -> toSharePriceMovements(event)); From 5ba229f79a08c0a1468488a8f20b5b3ba355e33e Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 11:44:21 +0300 Subject: [PATCH 34/60] Add `final` modifier to the `ProjectionReader` class. --- .../io/spine/examples/shareaware/server/ProjectionReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java b/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java index b8c7175c..70a8c3b1 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/ProjectionReader.java @@ -45,7 +45,7 @@ * * @param type of the {@code Projection} to read */ -public class ProjectionReader { +public final class ProjectionReader { private final Stand stand; private final Class stateClass; From a1232360eda7684590132a3ed83f56d689619e2c Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 11:53:06 +0300 Subject: [PATCH 35/60] Rename the `waitForNewState` method to the `onceUpdated` in the `EntitySubscription` class. --- .../shareaware/server/e2e/given/E2EUser.java | 12 ++++++------ .../server/e2e/given/EntitySubscription.java | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java index 739dcf30..fdc5be4e 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java @@ -114,7 +114,7 @@ public WalletId walletId() { */ public List looksAtAvailableShares() { var shares = availableMarketShares - .waitForNewState() + .onceUpdated() .getShareList(); return shares; } @@ -123,7 +123,7 @@ public List looksAtAvailableShares() { * Describes the user's action to look at the investment. */ public InvestmentView looksAtInvestment() { - return investment.waitForNewState(); + return investment.onceUpdated(); } /** @@ -134,7 +134,7 @@ public WalletBalance replenishesWalletFor(Money amount) { wallet.clearState(); post(replenishWallet); - var balanceAfterReplenishment = wallet.waitForNewState(); + var balanceAfterReplenishment = wallet.onceUpdated(); var expectedBalance = walletBalanceWith(usd(500), walletId); assertThat(balanceAfterReplenishment).isEqualTo(expectedBalance); return balanceAfterReplenishment; @@ -160,7 +160,7 @@ public EitherOf2 purchase(Share share, int how } wallet.clearState(); post(purchaseShares); - return EitherOf2.withA(wallet.waitForNewState()); + return EitherOf2.withA(wallet.onceUpdated()); } /** @@ -180,7 +180,7 @@ private WalletBalance withdrawsMoney(Money amount) { var withdrawMoney = withdrawMoneyFrom(walletId, amount); wallet.clearState(); post(withdrawMoney); - return wallet.waitForNewState(); + return wallet.onceUpdated(); } /** @@ -222,7 +222,7 @@ private void createWalletForUser() { var createWallet = createWallet(walletId); wallet.clearState(); post(createWallet); - var initialBalance = wallet.waitForNewState(); + var initialBalance = wallet.onceUpdated(); assertThat(initialBalance).isEqualTo(zeroWalletBalance(walletId)); } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index d53217cf..5a4be50c 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -60,10 +60,10 @@ public S state() { } /** - * Waits for a new state of the entity to arrive and return it. + * Waits for an update of the entity state to arrive and return this state. */ - S waitForNewState() { - return entity.waitForNewState(); + S onceUpdated() { + return entity.onceUpdated(); } /** @@ -93,7 +93,7 @@ private S state() { } } - private S waitForNewState() { + private S onceUpdated() { try { return future.whenComplete((value, error) -> { if (error != null) { From d06facc60d0a94751a98e4215c7cdd07196bd19d Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 12:02:32 +0300 Subject: [PATCH 36/60] Implement the `share` method in the `GivenShare` class. --- .../shareaware/server/given/GivenShare.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java b/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java index 9a35c123..01a7d0f4 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/given/GivenShare.java @@ -51,13 +51,7 @@ public static Share tesla() { } public static Share tesla(Money price) { - return Share - .newBuilder() - .setId(teslaId) - .setPrice(price) - .setCompanyName("Tesla") - .setCompanyLogo("testURL") - .vBuild(); + return share(teslaId, price, "Tesla"); } public static Share apple() { @@ -65,11 +59,15 @@ public static Share apple() { } public static Share apple(Money price) { + return share(appleId, price, "Apple"); + } + + private static Share share(ShareId id, Money price, String companyName) { return Share .newBuilder() - .setId(appleId) + .setId(id) .setPrice(price) - .setCompanyName("Apple") + .setCompanyName(companyName) .setCompanyLogo("testURL") .vBuild(); } From 5938ffaf83a23e9d7fc1cf1027b35a30af204f02 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 12:04:34 +0300 Subject: [PATCH 37/60] Import the `ProtoTruth` statically in the `SharePriceMovementProjectionTest`. --- .../market/SharePriceMovementProjectionTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java index 77bbdce0..0c425778 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java @@ -26,22 +26,16 @@ package io.spine.examples.shareaware.server.market; -import com.google.common.truth.extensions.proto.ProtoTruth; -import io.spine.examples.shareaware.ShareId; import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; -import io.spine.examples.shareaware.market.event.MarketSharesUpdated; import io.spine.examples.shareaware.server.ProjectionReader; -import io.spine.examples.shareaware.share.Share; -import io.spine.server.BoundedContext; import io.spine.server.BoundedContextBuilder; import io.spine.server.integration.ThirdPartyContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.List; - import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static io.spine.client.Filters.eq; import static io.spine.examples.shareaware.given.GivenMoney.usd; @@ -121,7 +115,7 @@ void state() { var expectedProjection = sharePriceMovementPerMinute(shareId, eventWithLowerPrice, eventWithHigherPrice); - ProtoTruth.assertThat(projection) + assertThat(projection) .comparingExpectedFieldsOnly() .isEqualTo(expectedProjection); } From 11e252c3e68b1f2d088bf8704adadfe5c2368c63 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 14:17:20 +0300 Subject: [PATCH 38/60] Implement the `WithShares` interface. --- .../shareaware/market/WithShares.java | 69 +++++++++++++++++++ .../shareaware/market/events.proto | 1 + ...SharePriceMovementPerMinuteProjection.java | 28 +------- .../SharePriceMovementProjectionTest.java | 6 +- .../given/SharesPriceMovementTestEnv.java | 26 ++----- 5 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 model/src/main/java/io/spine/examples/shareaware/market/WithShares.java diff --git a/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java b/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java new file mode 100644 index 00000000..8416408b --- /dev/null +++ b/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.market; + +import com.google.errorprone.annotations.Immutable; +import io.spine.annotation.GeneratedMixin; +import io.spine.base.EventMessage; +import io.spine.examples.shareaware.ShareId; +import io.spine.examples.shareaware.share.Share; + +import java.util.List; + +import static io.spine.util.Exceptions.newIllegalArgumentException; + +/** + * Common interface for signals which operate with a list of shares. + */ +@Immutable +@GeneratedMixin +public interface WithShares extends EventMessage { + + /** + * Returns the list of shares. + */ + List getShareList(); + + /** + * Retrieves the share with provided ID from the shares list. + * + * @throws IllegalArgumentException when the share with provided ID is not found in the list + */ + default Share retrieveShare(ShareId id) { + List shares = getShareList(); + var optionalShare = shares + .stream() + .filter(share -> share.getId().equals(id)) + .findAny(); + if (optionalShare.isEmpty()) { + throw newIllegalArgumentException( + "There is no share with provided ID - %s in the list - %s.", + id, shares); + } + return optionalShare.get(); + } +} diff --git a/model/src/main/proto/spine_examples/shareaware/market/events.proto b/model/src/main/proto/spine_examples/shareaware/market/events.proto index e3e0e525..f1c7a564 100644 --- a/model/src/main/proto/spine_examples/shareaware/market/events.proto +++ b/model/src/main/proto/spine_examples/shareaware/market/events.proto @@ -91,6 +91,7 @@ message MarketClosed { // Shares on market have been updated. message MarketSharesUpdated { + option (is).java_type = "io.spine.examples.shareaware.market.WithShares"; // The ID of the shares market. MarketId market = 1; diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index 7e422ead..b807ec4e 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -28,20 +28,12 @@ import io.spine.core.External; import io.spine.core.Subscribe; -import io.spine.examples.shareaware.ShareId; import io.spine.examples.shareaware.SharePriceMovementId; import io.spine.examples.shareaware.market.PriceAtTime; import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; import io.spine.examples.shareaware.market.event.MarketSharesUpdated; -import io.spine.examples.shareaware.share.Share; -import io.spine.money.Money; import io.spine.server.projection.Projection; -import java.util.Collection; - -import static io.spine.util.Exceptions.newIllegalArgumentException; -import static java.lang.String.format; - /** * The view of the share price movements per minute. */ @@ -55,7 +47,8 @@ void on(@External MarketSharesUpdated e) { var shareId = builder() .getId() .getShare(); - var price = retrieveSharePrice(e.getShareList(), shareId); + var price = e.retrieveShare(shareId) + .getPrice(); var priceAtTime = PriceAtTime .newBuilder() .setPrice(price) @@ -64,21 +57,4 @@ void on(@External MarketSharesUpdated e) { builder().setShare(shareId) .addPoint(priceAtTime); } - - /** - * Retrieves the share with provided ID from the provided {@code Collection}. - */ - private static Money retrieveSharePrice(Collection shares, ShareId id) { - var optionalShare = shares - .stream() - .filter(share -> share.getId().equals(id)) - .findAny(); - if (optionalShare.isEmpty()) { - String errorMessage = - format("There is no share with provided ID - %s in the list - %s.", id, shares); - throw newIllegalArgumentException(errorMessage); - } - return optionalShare.get() - .getPrice(); - } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java index 0c425778..e9091382 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/SharePriceMovementProjectionTest.java @@ -41,7 +41,7 @@ import static io.spine.examples.shareaware.given.GivenMoney.usd; import static io.spine.examples.shareaware.server.given.GivenShare.tesla; import static io.spine.examples.shareaware.server.market.given.MarketTestEnv.marketSharesUpdated; -import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.ProjectionActivityTime; +import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.projectionActivityTime; import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.ShareFieldInProjection; import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.actorContext; import static io.spine.examples.shareaware.server.market.given.SharesPriceMovementTestEnv.sharePriceMovementPerMinute; @@ -80,7 +80,7 @@ void createProjections() { assertThat(projectionsAfterFirstEmit.size()).isEqualTo(1); marketData.emittedEvent(marketSharesUpdated(), newUuid()); - sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); + sleepUninterruptibly(ofSeconds(projectionActivityTime)); var projectionsAfterSecondEmit = reader.read( actorContext(), eq(ShareFieldInProjection, shareId) @@ -108,7 +108,7 @@ void state() { marketData.emittedEvent(eventWithLowerPrice, newUuid()); marketData.emittedEvent(eventWithHigherPrice, newUuid()); - sleepUninterruptibly(ofSeconds(ProjectionActivityTime)); + sleepUninterruptibly(ofSeconds(projectionActivityTime)); var projection = reader .read(actorContext(), eq(ShareFieldInProjection, shareId)) .get(0); diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java index 90c3d1a0..2af2f86e 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java @@ -36,19 +36,14 @@ import io.spine.examples.shareaware.market.PriceAtTime; import io.spine.examples.shareaware.market.SharePriceMovementPerMinute; import io.spine.examples.shareaware.market.event.MarketSharesUpdated; -import io.spine.examples.shareaware.share.Share; import io.spine.money.Money; -import java.util.List; -import java.util.Optional; - import static io.spine.base.Time.currentTime; import static io.spine.testing.core.given.GivenUserId.newUuid; -import static io.spine.util.Exceptions.newIllegalArgumentException; public final class SharesPriceMovementTestEnv { - public static final long ProjectionActivityTime = 60; + public static final long projectionActivityTime = 60; public static final EntityColumn ShareFieldInProjection = SharePriceMovementPerMinute.Column.share(); @@ -64,13 +59,15 @@ public static SharePriceMovementPerMinute sharePriceMovementPerMinute( MarketSharesUpdated firstEvent, MarketSharesUpdated secondEvent ) { - Money firstPrice = retrieveShare(firstEvent.getShareList(), shareId).getPrice(); - Money secondPrice = retrieveShare(secondEvent.getShareList(), shareId).getPrice(); + Money firstPrice = firstEvent.retrieveShare(shareId) + .getPrice(); + Money secondPrice = secondEvent.retrieveShare(shareId) + .getPrice(); PriceAtTime firstPriceAtTime = priceAtTime(firstPrice, firstEvent.getWhenUpdated()); PriceAtTime secondPriceAtTime = priceAtTime(secondPrice, secondEvent.getWhenUpdated()); Duration activityTime = Duration .newBuilder() - .setSeconds(ProjectionActivityTime) + .setSeconds(projectionActivityTime) .build(); SharePriceMovementId sharePriceMovementId = sharePriceMovementId(shareId, activityTime); return SharePriceMovementPerMinute @@ -113,15 +110,4 @@ private static SharePriceMovementId sharePriceMovementId( .setActivityTime(activityTime) .buildPartial(); } - - private static Share retrieveShare(List shares, ShareId id) { - Optional optionalShare = shares.stream() - .filter(share -> share.getId() - .equals(id)) - .findAny(); - if (optionalShare.isEmpty()) { - throw newIllegalArgumentException("There is no share with provided ID in the list."); - } - return optionalShare.get(); - } } From f3047d96b794461ddc494e2a9def5aeb4c34b7e4 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 14:54:51 +0300 Subject: [PATCH 39/60] Rename the `retrieveShare` method to `find` in the `WithShares` interface. --- .../examples/shareaware/market/SharePriceMovement.java | 2 +- .../io/spine/examples/shareaware/market/WithShares.java | 6 +++--- .../market/SharePriceMovementPerMinuteProjection.java | 2 +- .../server/market/given/SharesPriceMovementTestEnv.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java index 8b194c2c..8cc0f201 100644 --- a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java +++ b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java @@ -35,7 +35,7 @@ import io.spine.examples.shareaware.SharePriceMovementId; /** - * Common interface for projections that displays the movements of the share price. + * Common interface for projections that display the movements of the share price. */ @Immutable @GeneratedMixin diff --git a/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java b/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java index 8416408b..df1c0b90 100644 --- a/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java +++ b/model/src/main/java/io/spine/examples/shareaware/market/WithShares.java @@ -49,11 +49,11 @@ public interface WithShares extends EventMessage { List getShareList(); /** - * Retrieves the share with provided ID from the shares list. + * Finds the share with provided ID from the shares list. * * @throws IllegalArgumentException when the share with provided ID is not found in the list */ - default Share retrieveShare(ShareId id) { + default Share find(ShareId id) { List shares = getShareList(); var optionalShare = shares .stream() @@ -61,7 +61,7 @@ default Share retrieveShare(ShareId id) { .findAny(); if (optionalShare.isEmpty()) { throw newIllegalArgumentException( - "There is no share with provided ID - %s in the list - %s.", + "Cannot find the share with the provided ID `%s` in the list of shares `%s`.", id, shares); } return optionalShare.get(); diff --git a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java index b807ec4e..4a596869 100644 --- a/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java +++ b/server/src/main/java/io/spine/examples/shareaware/server/market/SharePriceMovementPerMinuteProjection.java @@ -47,7 +47,7 @@ void on(@External MarketSharesUpdated e) { var shareId = builder() .getId() .getShare(); - var price = e.retrieveShare(shareId) + var price = e.find(shareId) .getPrice(); var priceAtTime = PriceAtTime .newBuilder() diff --git a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java index 2af2f86e..280605c7 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/market/given/SharesPriceMovementTestEnv.java @@ -59,9 +59,9 @@ public static SharePriceMovementPerMinute sharePriceMovementPerMinute( MarketSharesUpdated firstEvent, MarketSharesUpdated secondEvent ) { - Money firstPrice = firstEvent.retrieveShare(shareId) + Money firstPrice = firstEvent.find(shareId) .getPrice(); - Money secondPrice = secondEvent.retrieveShare(shareId) + Money secondPrice = secondEvent.find(shareId) .getPrice(); PriceAtTime firstPriceAtTime = priceAtTime(firstPrice, firstEvent.getWhenUpdated()); PriceAtTime secondPriceAtTime = priceAtTime(secondPrice, secondEvent.getWhenUpdated()); From 23035b5a2bc1150dc2325d0e8c9204a078f613bf Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 15:47:22 +0300 Subject: [PATCH 40/60] Rewrite the documentation for the `onceUpdated` method in the `EntitySubscription` class. --- .../shareaware/server/e2e/given/EntitySubscription.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index 5a4be50c..7dfbe558 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -61,6 +61,13 @@ public S state() { /** * Waits for an update of the entity state to arrive and return this state. + * + *

This method will wait for a maximum of 10 seconds for an update of the entity state to arrive. + * If the update does not arrive within the specified time, a {@code TimeoutException} will be thrown. + * + *

If the update of the entity arrives before this method is called, + * an exception will be thrown as described above. In such cases, you should use the {@link #state()} + * method to retrieve the entity state instead. */ S onceUpdated() { return entity.onceUpdated(); From 0d6f54f867da8f2f1143f321883edcc9b76ddb32 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 17:09:44 +0300 Subject: [PATCH 41/60] Implement the `onceUpdatedAfter` method in the `EntitySubscription` class. --- .../shareaware/server/e2e/given/E2EUser.java | 22 +++++------- .../server/e2e/given/EntitySubscription.java | 34 +++++++++++-------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java index fdc5be4e..9532bbea 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java @@ -114,7 +114,7 @@ public WalletId walletId() { */ public List looksAtAvailableShares() { var shares = availableMarketShares - .onceUpdated() + .state() .getShareList(); return shares; } @@ -123,7 +123,7 @@ public List looksAtAvailableShares() { * Describes the user's action to look at the investment. */ public InvestmentView looksAtInvestment() { - return investment.onceUpdated(); + return investment.state(); } /** @@ -132,9 +132,7 @@ public InvestmentView looksAtInvestment() { public WalletBalance replenishesWalletFor(Money amount) { var replenishWallet = replenishWallet(walletId, amount); - wallet.clearState(); - post(replenishWallet); - var balanceAfterReplenishment = wallet.onceUpdated(); + var balanceAfterReplenishment = wallet.onceUpdatedAfter(replenishWallet); var expectedBalance = walletBalanceWith(usd(500), walletId); assertThat(balanceAfterReplenishment).isEqualTo(expectedBalance); return balanceAfterReplenishment; @@ -158,9 +156,8 @@ public EitherOf2 purchase(Share share, int how var insufficientFunds = retrieveValueFrom(subscriptionOutcome); return EitherOf2.withB(insufficientFunds); } - wallet.clearState(); - post(purchaseShares); - return EitherOf2.withA(wallet.onceUpdated()); + WalletBalance walletAfterPurchase = wallet.onceUpdatedAfter(purchaseShares); + return EitherOf2.withA(walletAfterPurchase); } /** @@ -178,9 +175,8 @@ public WalletBalance withdrawsAllMoney(WalletBalance balance) { */ private WalletBalance withdrawsMoney(Money amount) { var withdrawMoney = withdrawMoneyFrom(walletId, amount); - wallet.clearState(); - post(withdrawMoney); - return wallet.onceUpdated(); + WalletBalance walletAfterWithdraw = wallet.onceUpdatedAfter(withdrawMoney); + return walletAfterWithdraw; } /** @@ -220,9 +216,7 @@ private void cancel(Subscription subscription) { private void createWalletForUser() { var createWallet = createWallet(walletId); - wallet.clearState(); - post(createWallet); - var initialBalance = wallet.onceUpdated(); + var initialBalance = wallet.onceUpdatedAfter(createWallet); assertThat(initialBalance).isEqualTo(zeroWalletBalance(walletId)); } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index 7dfbe558..fd52594c 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -26,15 +26,19 @@ package io.spine.examples.shareaware.server.e2e.given; +import io.spine.base.CommandMessage; import io.spine.base.EntityState; import io.spine.client.Client; +import io.spine.core.Command; import io.spine.core.UserId; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import static io.spine.util.Exceptions.illegalArgumentWithCauseOf; import static io.spine.util.Exceptions.illegalStateWithCauseOf; +import static io.spine.util.Exceptions.newIllegalArgumentException; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -44,7 +48,13 @@ public class EntitySubscription { private final ObservedEntity entity = new ObservedEntity<>(); + private final Client client; + + private final UserId user; + EntitySubscription(Class entityType, Client client, UserId user) { + this.client = client; + this.user = user; client.onBehalfOf(user) .subscribeTo(entityType) .observe(entity::setState) @@ -60,24 +70,18 @@ public S state() { } /** - * Waits for an update of the entity state to arrive and return this state. - * - *

This method will wait for a maximum of 10 seconds for an update of the entity state to arrive. - * If the update does not arrive within the specified time, a {@code TimeoutException} will be thrown. + * Posts the command on behalf of the user and waits for an update of the entity state that + * should happen as a consequence of the posted command. * - *

If the update of the entity arrives before this method is called, - * an exception will be thrown as described above. In such cases, you should use the {@link #state()} - * method to retrieve the entity state instead. - */ - S onceUpdated() { - return entity.onceUpdated(); - } - - /** - * Clears the state of the entity. + *

If an update of the entity state is not received within 10 seconds, + * a {@code TimeoutException} is thrown. */ - void clearState() { + S onceUpdatedAfter(CommandMessage command) { entity.clearState(); + client.onBehalfOf(user) + .command(command) + .postAndForget(); + return entity.onceUpdated(); } private static final class ObservedEntity { From 44648d6a0a7bc5ce820aa153cc6119292cc5a06c Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 18 Jul 2023 17:10:48 +0300 Subject: [PATCH 42/60] Rename the `shareFromId` method to `share` in the `SharePriceMovement` interface. --- .../io/spine/examples/shareaware/market/SharePriceMovement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java index 8cc0f201..396d1c19 100644 --- a/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java +++ b/model/src/main/java/io/spine/examples/shareaware/market/SharePriceMovement.java @@ -65,7 +65,7 @@ default Duration activityTime() { /** * Returns the ID of the share which price movements the projection displays. */ - default ShareId shareFromId() { + default ShareId share() { return getId().getShare(); } } From 1203b24658f7f38f219f99c68545814c1a684419 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 19 Jul 2023 12:02:33 +0300 Subject: [PATCH 43/60] Remove waiting from the `state` method in the `ObservedEntity` class. --- .../shareaware/server/e2e/given/E2EUser.java | 12 ++++++---- .../server/e2e/given/EntitySubscription.java | 23 +++++++++++-------- .../server/e2e/given/WithServer.java | 6 ++++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java index 9532bbea..4f3945a2 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java @@ -26,6 +26,7 @@ package io.spine.examples.shareaware.server.e2e.given; +import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import io.spine.base.CommandMessage; import io.spine.base.EventMessage; @@ -113,17 +114,18 @@ public WalletId walletId() { * Describes the user's action to look at available shares on the market. */ public List looksAtAvailableShares() { - var shares = availableMarketShares - .state() - .getShareList(); - return shares; + var marketShares = availableMarketShares.state(); + Preconditions.checkNotNull(marketShares); + return marketShares.getShareList(); } /** * Describes the user's action to look at the investment. */ public InvestmentView looksAtInvestment() { - return investment.state(); + var investmentView = investment.state(); + Preconditions.checkNotNull(investmentView); + return investmentView; } /** diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index fd52594c..4eadbee5 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -29,16 +29,13 @@ import io.spine.base.CommandMessage; import io.spine.base.EntityState; import io.spine.client.Client; -import io.spine.core.Command; import io.spine.core.UserId; +import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; -import static io.spine.util.Exceptions.illegalArgumentWithCauseOf; import static io.spine.util.Exceptions.illegalStateWithCauseOf; -import static io.spine.util.Exceptions.newIllegalArgumentException; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -62,8 +59,10 @@ public class EntitySubscription { } /** - * Provides the current state of the subscribed entity. + * Provides the current state of the subscribed entity + * if it arrived by the time when this method is called, null otherwise. */ + @Nullable public S state() { S state = entity.state(); return state; @@ -89,17 +88,23 @@ private static final class ObservedEntity { private CompletableFuture future = new CompletableFuture<>(); private void setState(S value) { - if(future.isDone()) { + if (future.isDone()) { future = new CompletableFuture<>(); } future.complete(value); } + /** + * Returns the current state of the entity if it exists, null otherwise. + */ + @Nullable private S state() { try { - S value = future.get(10, SECONDS); - return value; - } catch (InterruptedException | ExecutionException | TimeoutException e) { + if (future.isDone()) { + return future.get(); + } + return null; + } catch (InterruptedException | ExecutionException e) { throw illegalStateWithCauseOf(e); } } diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java index 1bd4db1b..fb2521b8 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java @@ -38,6 +38,7 @@ import java.util.ArrayList; import java.util.Collection; +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static io.grpc.ManagedChannelBuilder.forAddress; import static io.spine.server.Server.atPort; import static io.spine.util.Exceptions.illegalStateWithCauseOf; @@ -53,6 +54,7 @@ public abstract class WithServer { private Server server; private final Collection channels = new ArrayList<>(); private static final MarketDataProvider provider = MarketDataProvider.instance(); + private final Duration marketPeriod = Duration.ofSeconds(1); /** * Starts the server and runs the {@link MarketDataProvider}. @@ -63,7 +65,9 @@ void startAndConnect() throws IOException { .add(TradingContext.newBuilder()) .build(); server.start(); - provider.runWith(Duration.ofSeconds(1)); + provider.runWith(marketPeriod); + // Wait for the `MarketDataProvider` to start work. + sleepUninterruptibly(marketPeriod); } /** From 4964675148f64f4f1f775b864f66a41357927b98 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 19 Jul 2023 12:03:53 +0300 Subject: [PATCH 44/60] Rename the `onceUpdated` method to `waitForUpdate` in the `ObservedEntity` class. --- .../server/e2e/given/EntitySubscription.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index 4eadbee5..ffb45133 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -80,7 +80,7 @@ S onceUpdatedAfter(CommandMessage command) { client.onBehalfOf(user) .command(command) .postAndForget(); - return entity.onceUpdated(); + return entity.waitForUpdate(); } private static final class ObservedEntity { @@ -109,7 +109,13 @@ private S state() { } } - private S onceUpdated() { + /** + * Waits for an update of the entity state to arrive and return this state. + * + *

An update of the entity state should be received within 10 seconds, + * otherwise a {@code TimeoutException} will be thrown. + */ + private S waitForUpdate() { try { return future.whenComplete((value, error) -> { if (error != null) { @@ -123,6 +129,9 @@ private S onceUpdated() { } } + /** + * Clears the state of the entity. + */ private void clearState() { future = new CompletableFuture<>(); } From f59522576f68ccb7f5e1bacc8b1ad51af6fb6011 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 19 Jul 2023 12:39:49 +0300 Subject: [PATCH 45/60] Move waiting for the market to release shares to the `SharePurchaseTest`. --- .../examples/shareaware/server/e2e/SharePurchaseTest.java | 3 +++ .../examples/shareaware/server/e2e/given/WithServer.java | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/SharePurchaseTest.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/SharePurchaseTest.java index 1b62db65..50a9ea35 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/SharePurchaseTest.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/SharePurchaseTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static io.spine.examples.shareaware.given.GivenMoney.usd; import static io.spine.examples.shareaware.server.e2e.given.SharePurchaseTestEnv.balanceAfterPurchase; import static io.spine.examples.shareaware.server.e2e.given.SharePurchaseTestEnv.investmentAfterPurchase; @@ -56,6 +57,8 @@ void test() { var channel = openChannel(); var user = new E2EUser(channel); + // Wait for the market to release shares. + sleepUninterruptibly(marketPeriod()); var shares = user.looksAtAvailableShares(); var tesla = pickTesla(shares); diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java index fb2521b8..0fc9e10e 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java @@ -38,7 +38,6 @@ import java.util.ArrayList; import java.util.Collection; -import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static io.grpc.ManagedChannelBuilder.forAddress; import static io.spine.server.Server.atPort; import static io.spine.util.Exceptions.illegalStateWithCauseOf; @@ -66,8 +65,6 @@ void startAndConnect() throws IOException { .build(); server.start(); provider.runWith(marketPeriod); - // Wait for the `MarketDataProvider` to start work. - sleepUninterruptibly(marketPeriod); } /** @@ -91,6 +88,10 @@ protected ManagedChannel openChannel() { return channel; } + protected Duration marketPeriod() { + return marketPeriod; + } + private static void closeChannel(ManagedChannel channel) { channel.shutdown(); try { From 3d09eba688537f0b9ef1b974bdafa40e541d2481 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 19 Jul 2023 12:49:35 +0300 Subject: [PATCH 46/60] Add documentation to the `marketPeriod` method in the `WithServer` class. --- .../spine/examples/shareaware/server/e2e/given/WithServer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java index 0fc9e10e..8afb7384 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/WithServer.java @@ -88,6 +88,9 @@ protected ManagedChannel openChannel() { return channel; } + /** + * Returns the period with which the market is updating shares. + */ protected Duration marketPeriod() { return marketPeriod; } From 45a0b53f10288861695204296f947ac4eebe37a6 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:11:08 +0300 Subject: [PATCH 47/60] Create the `testutil-server` module. --- server/build.gradle.kts | 1 + settings.gradle.kts | 1 + testutil-server/build.gradle.kts | 9 +++++++++ 3 files changed, 11 insertions(+) create mode 100644 testutil-server/build.gradle.kts diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 1dbf7ea1..13b601ad 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -46,6 +46,7 @@ spine { dependencies { implementation(project(":model")) testImplementation(project(":model", "test")) + testImplementation(project(":testutil-server")) } application { diff --git a/settings.gradle.kts b/settings.gradle.kts index 8bfed053..e8e68fb1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,3 +28,4 @@ rootProject.name = "ShareAware" include("model") include("server") include("client") +include("testutil-server") diff --git a/testutil-server/build.gradle.kts b/testutil-server/build.gradle.kts new file mode 100644 index 00000000..aa01dde6 --- /dev/null +++ b/testutil-server/build.gradle.kts @@ -0,0 +1,9 @@ +import io.spine.examples.shareaware.dependency.Truth + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(Truth.lib) +} From 69ebab3fd0a931d66f6dbce33015cde2dfcb4dab Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:15:05 +0300 Subject: [PATCH 48/60] Implement the `AsyncObserver` class. --- .../testing/server/e2e/AsyncObserver.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java new file mode 100644 index 00000000..1e4b7561 --- /dev/null +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.testing.server.e2e; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import static io.spine.util.Exceptions.illegalStateWithCauseOf; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Observer for entity state changes. + * + *

It allows to observe the asynchronous mutations of the entity state. + * + * @param + * the state to observe. + * @param + * the type of the command, the execution of which should lead to changes in the state. + */ +public class AsyncObserver { + + private final Consumer howToCommand; + + private CompletableFuture future = new CompletableFuture<>(); + + private S state = null; + + private ObservationState observationState = null; + + /** + * Creates the new instance of the {@code AsyncObserver}. + * + * @param howToObserve + * a callback that defines how to observe the state {@link S}. + * @param howToCommand + * a callback that defines how to send a command {@link C}. + */ + public AsyncObserver( + Consumer> howToObserve, + Consumer howToCommand) { + this.howToCommand = howToCommand; + howToObserve.accept(value -> { + state = value; + if (future.isDone()) { + future = new CompletableFuture<>(); + } + future.complete(value); + observationState = ObservationState.OBSERVED; + }); + } + + /** + * Posts a command and waits for an update of the entity state that + * should occur as a consequence of the posted command. + * + *

If an update of the entity state is not received within 10 seconds, + * a {@code TimeoutException} is thrown. + */ + public S onceUpdatedAfter(C command) { + howToCommand.accept(command); + return waitForUpdate(); + } + + /** + * Returns the current state of the entity if it exists, null otherwise. + */ + public @Nullable S state() { + return state; + } + + /** + * Waits for an update of the entity state to arrive and return this state. + * + *

An update of the entity state should be received within 10 seconds, + * otherwise a {@code TimeoutException} will be thrown. + */ + private S waitForUpdate() { + if (observationState != null && + observationState != ObservationState.OBSERVED) { + future = new CompletableFuture<>(); + } + try { + S updatedEntity = future.whenComplete((value, error) -> { + if (error != null) { + throw illegalStateWithCauseOf(error); + } + }) + .orTimeout(10, SECONDS) + .get(); + observationState = ObservationState.UPDATED; + return updatedEntity; + } catch (InterruptedException | ExecutionException e) { + throw illegalStateWithCauseOf(e); + } + } + + /** + * Represents the states of the entity observation. + * + *

The `AsyncObserver` can observe asynchronous changes of the entity's state, + * and these states were introduced to restrict the order of the observing operations. + */ + private enum ObservationState { + OBSERVED, // The state after observation. + UPDATED // The state after an update has been received. + } +} From fa90950e92b53f082ad788b7cf28e76c50faee3f Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:23:36 +0300 Subject: [PATCH 49/60] Add tests for the `AsyncObserver` class. --- .../testing/server/e2e/AsyncObserverTest.java | 94 +++++++++++++ .../server/e2e/given/AsyncStateMutator.java | 124 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java create mode 100644 testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java new file mode 100644 index 00000000..0ac01743 --- /dev/null +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.testing.server.e2e; + +import io.spine.examples.shareaware.testing.server.e2e.given.AsyncStateMutator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeoutException; + +import static com.google.common.truth.Truth.assertThat; +import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("'AsyncObserver' should") +class AsyncObserverTest { + + @Test + @DisplayName("observe mutation of the state with the slow response from the mutator") + void observeMutationWithDelay() { + var stateMutator = new AsyncStateMutator("state", ofSeconds(5)); + var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + stateMutator::mutateState); + + var actualStateAfterFirstMutation = observer.onceUpdatedAfter(true); + var stateAfterFirstMutation = stateMutator.state(); + assertThat(actualStateAfterFirstMutation).isEqualTo(stateAfterFirstMutation); + assertThat(observer.state()).isEqualTo(stateAfterFirstMutation); + + var actualStateAfterSecondMutation = observer.onceUpdatedAfter(true); + var stateAfterSecondMutation = stateMutator.state(); + assertThat(actualStateAfterSecondMutation).isEqualTo(stateAfterSecondMutation); + assertThat(observer.state()).isEqualTo(stateAfterSecondMutation); + + stateMutator.stopMutatorThread(); + } + + @Test + @DisplayName("observe mutation of the state with the fast response from the mutator") + void observeMutationWithoutDelay() { + var stateMutator = new AsyncStateMutator("state", ofSeconds(0)); + var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + stateMutator::mutateState); + + var actualStateAfterFirstMutation = observer.onceUpdatedAfter(true); + var stateAfterFirstMutation = stateMutator.state(); + assertThat(actualStateAfterFirstMutation).isEqualTo(stateAfterFirstMutation); + assertThat(observer.state()).isEqualTo(stateAfterFirstMutation); + + stateMutator.stopMutatorThread(); + } + + @Test + @DisplayName("throw the `IllegalArgumentException` with cause of `TimeoutException`" + + "when the state mutation were not received in 10 seconds") + void throwExceptionWithoutMutation() { + var stateMutator = new AsyncStateMutator("", ofSeconds(0)); + var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + stateMutator::mutateState); + + IllegalStateException exception = + assertThrows(IllegalStateException.class, + () -> observer.onceUpdatedAfter(false)); + assertThat(exception.getCause() + .getClass()).isEqualTo(TimeoutException.class); + + stateMutator.stopMutatorThread(); + } +} diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java new file mode 100644 index 00000000..00bd00de --- /dev/null +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.testing.server.e2e.given; + +import io.spine.util.Exceptions; + +import java.security.SecureRandom; +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static java.util.concurrent.Executors.newSingleThreadExecutor; + +/** + * State mutator that working in a separate thread. + */ +public class AsyncStateMutator { + + private final Consumer> mutationNotifier; + + private final AtomicBoolean isMutated = new AtomicBoolean(false); + + private final AtomicReference state = new AtomicReference<>(); + + private final ExecutorService thread = newSingleThreadExecutor(); + + private final Random random = new SecureRandom(); + + private final Object lock = new Object(); + + private volatile boolean keepRunning = true; + + public AsyncStateMutator(String state, Duration delay) { + this.state.set(state); + mutationNotifier = consumer -> + thread.execute(() -> { + while (keepRunning) { + synchronized (lock) { + while (!isMutated.get() && keepRunning) { + try { + lock.wait(10000); + } catch (InterruptedException e) { + throw Exceptions.illegalStateWithCauseOf(e); + } + } + } + if (isMutated.get()) { + sleepUninterruptibly(delay); + consumer.accept(this.state.get()); + isMutated.set(false); + } + } + }); + } + + /** + * Mutate the state by adding the random number to it. + */ + public synchronized void mutateState(Boolean isMutating) { + if (isMutating) { + int additionalState = random.nextInt(100); + state.set(state.get() + additionalState); + } + isMutated.set(isMutating); + if (isMutating) { + synchronized (lock) { + lock.notifyAll(); + } + } + } + + /** + * Stops the thread where the {@code AsyncStateMutator} is executing. + */ + public void stopMutatorThread() { + keepRunning = false; + synchronized (lock) { + lock.notifyAll(); + } + thread.shutdown(); + } + + /** + * Returns the current state. + */ + public String state() { + return state.get(); + } + + /** + * Returns the {@code Consumer} that notifies the caller when the state has been mutated. + */ + public Consumer> mutationNotifier() { + return mutationNotifier; + } +} From 5c78e757c431c75a6827814e7281a567174fa638 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:24:10 +0300 Subject: [PATCH 50/60] Rewrite the `EntitySubscription` class. --- .../server/e2e/given/EntitySubscription.java | 111 ++---------------- 1 file changed, 11 insertions(+), 100 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index ffb45133..a6ed2974 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -30,110 +30,21 @@ import io.spine.base.EntityState; import io.spine.client.Client; import io.spine.core.UserId; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import static io.spine.util.Exceptions.illegalStateWithCauseOf; -import static java.util.concurrent.TimeUnit.SECONDS; +import io.spine.examples.shareaware.testing.server.e2e.AsyncObserver; /** - * Subscription for the {@code EntityState} changes. + * Configures the {@code AsyncObserver} with how to send a command + * and how to observe the entity state changes using {@code Spine} Client API. */ -public class EntitySubscription { - - private final ObservedEntity entity = new ObservedEntity<>(); - - private final Client client; - - private final UserId user; +class EntitySubscription extends AsyncObserver { EntitySubscription(Class entityType, Client client, UserId user) { - this.client = client; - this.user = user; - client.onBehalfOf(user) - .subscribeTo(entityType) - .observe(entity::setState) - .post(); - } - - /** - * Provides the current state of the subscribed entity - * if it arrived by the time when this method is called, null otherwise. - */ - @Nullable - public S state() { - S state = entity.state(); - return state; - } - - /** - * Posts the command on behalf of the user and waits for an update of the entity state that - * should happen as a consequence of the posted command. - * - *

If an update of the entity state is not received within 10 seconds, - * a {@code TimeoutException} is thrown. - */ - S onceUpdatedAfter(CommandMessage command) { - entity.clearState(); - client.onBehalfOf(user) - .command(command) - .postAndForget(); - return entity.waitForUpdate(); - } - - private static final class ObservedEntity { - - private CompletableFuture future = new CompletableFuture<>(); - - private void setState(S value) { - if (future.isDone()) { - future = new CompletableFuture<>(); - } - future.complete(value); - } - - /** - * Returns the current state of the entity if it exists, null otherwise. - */ - @Nullable - private S state() { - try { - if (future.isDone()) { - return future.get(); - } - return null; - } catch (InterruptedException | ExecutionException e) { - throw illegalStateWithCauseOf(e); - } - } - - /** - * Waits for an update of the entity state to arrive and return this state. - * - *

An update of the entity state should be received within 10 seconds, - * otherwise a {@code TimeoutException} will be thrown. - */ - private S waitForUpdate() { - try { - return future.whenComplete((value, error) -> { - if (error != null) { - throw illegalStateWithCauseOf(error); - } - }) - .orTimeout(10, SECONDS) - .get(); - } catch (InterruptedException | ExecutionException e) { - throw illegalStateWithCauseOf(e); - } - } - - /** - * Clears the state of the entity. - */ - private void clearState() { - future = new CompletableFuture<>(); - } + super(consumer -> client.onBehalfOf(user) + .subscribeTo(entityType) + .observe(consumer) + .post(), + commandMessage -> client.onBehalfOf(user) + .command(commandMessage) + .postAndForget()); } } From 0962ee9c0cadc0ca6c1f15afffce0f4764812630 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:36:26 +0300 Subject: [PATCH 51/60] Add `package-info` for the `testing.server.e2e` package. --- .../testing/server/e2e/package-info.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/package-info.java diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/package-info.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/package-info.java new file mode 100644 index 00000000..42852b3e --- /dev/null +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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. + */ + +/** + * This package contains the utilities for the end-to-end testing. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.shareaware.testing.server.e2e; + +import com.google.errorprone.annotations.CheckReturnValue; +import javax.annotation.ParametersAreNonnullByDefault; From ce6739956e8ba64b5fac8bd673cc586c2145d2e7 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:36:45 +0300 Subject: [PATCH 52/60] Add `package-info` for the `testing.server.e2e.given` package. --- .../server/e2e/given/package-info.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/package-info.java diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/package-info.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/package-info.java new file mode 100644 index 00000000..35c01773 --- /dev/null +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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. + */ + +/** + * Provides test environment for the end-to-end testing. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.shareaware.testing.server.e2e.given; + +import com.google.errorprone.annotations.CheckReturnValue; +import javax.annotation.ParametersAreNonnullByDefault; From 8c594a4253ae3a729f658f45642e840c9695a090 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 18:44:53 +0300 Subject: [PATCH 53/60] Remove unnecessary comments from the `AsyncObserver`. --- .../examples/shareaware/testing/server/e2e/AsyncObserver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index 1e4b7561..8160280a 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -129,7 +129,7 @@ private S waitForUpdate() { * and these states were introduced to restrict the order of the observing operations. */ private enum ObservationState { - OBSERVED, // The state after observation. - UPDATED // The state after an update has been received. + OBSERVED, + UPDATED } } From 64404e0d32068fe73022adaa752b507f8549539e Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 20:18:02 +0300 Subject: [PATCH 54/60] Use `Objects.requireNonNull` instead of `Preconditions.checkNotNull` in the `E2EUser`. --- .../examples/shareaware/server/e2e/given/E2EUser.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java index 4f3945a2..dacbae06 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/E2EUser.java @@ -26,7 +26,6 @@ package io.spine.examples.shareaware.server.e2e.given; -import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import io.spine.base.CommandMessage; import io.spine.base.EventMessage; @@ -61,6 +60,7 @@ import static io.spine.examples.shareaware.server.e2e.given.SharePurchaseTestEnv.zeroWalletBalance; import static io.spine.examples.shareaware.server.given.GivenWallet.createWallet; import static io.spine.util.Exceptions.illegalStateWithCauseOf; +import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -114,8 +114,7 @@ public WalletId walletId() { * Describes the user's action to look at available shares on the market. */ public List looksAtAvailableShares() { - var marketShares = availableMarketShares.state(); - Preconditions.checkNotNull(marketShares); + var marketShares = requireNonNull(availableMarketShares.state()); return marketShares.getShareList(); } @@ -123,8 +122,7 @@ public List looksAtAvailableShares() { * Describes the user's action to look at the investment. */ public InvestmentView looksAtInvestment() { - var investmentView = investment.state(); - Preconditions.checkNotNull(investmentView); + var investmentView = requireNonNull(investment.state()); return investmentView; } From 23adbbcedf220c040046760ddba04b59ed1282e1 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Mon, 24 Jul 2023 20:27:31 +0300 Subject: [PATCH 55/60] Mark nullable fields with `Nullable` annotation in the `AsyncObserver` class. --- .../shareaware/testing/server/e2e/AsyncObserver.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index 8160280a..6ff038ee 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -38,12 +38,12 @@ /** * Observer for entity state changes. * - *

It allows to observe the asynchronous mutations of the entity state. + *

Allows to observe the asynchronous mutations of the entity state. * * @param - * the state to observe. + * the state to observe * @param - * the type of the command, the execution of which should lead to changes in the state. + * the type of the command, the execution of which should lead to changes in the state */ public class AsyncObserver { @@ -51,9 +51,9 @@ public class AsyncObserver { private CompletableFuture future = new CompletableFuture<>(); - private S state = null; + private @Nullable S state = null; - private ObservationState observationState = null; + private @Nullable ObservationState observationState = null; /** * Creates the new instance of the {@code AsyncObserver}. From f7e036de09e6e3fbb635d07f812fb31938f4f425 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 25 Jul 2023 11:11:02 +0300 Subject: [PATCH 56/60] Extract the `waitForFutureToComplete` method in the `AsyncObserver` class. --- .../testing/server/e2e/AsyncObserver.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index 6ff038ee..e4ca7ca7 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -107,16 +107,26 @@ private S waitForUpdate() { observationState != ObservationState.OBSERVED) { future = new CompletableFuture<>(); } + S updatedEntity = waitForFutureToComplete(future); + observationState = ObservationState.UPDATED; + return updatedEntity; + } + + /** + * Waits for the provided {@code CompletableFuture} to complete. + * + *

If the completion of the provided {@code CompletableFuture} + * does not happen within 10 seconds, a {@code TimeoutException} will be thrown. + */ + private S waitForFutureToComplete(CompletableFuture future) { try { - S updatedEntity = future.whenComplete((value, error) -> { - if (error != null) { - throw illegalStateWithCauseOf(error); - } - }) - .orTimeout(10, SECONDS) - .get(); - observationState = ObservationState.UPDATED; - return updatedEntity; + return future.whenComplete((value, error) -> { + if (error != null) { + throw illegalStateWithCauseOf(error); + } + }) + .orTimeout(10, SECONDS) + .get(); } catch (InterruptedException | ExecutionException e) { throw illegalStateWithCauseOf(e); } @@ -125,7 +135,7 @@ private S waitForUpdate() { /** * Represents the states of the entity observation. * - *

The `AsyncObserver` can observe asynchronous changes of the entity's state, + *

The {@code AsyncObserver} can observe asynchronous changes of the entity's state, * and these states were introduced to restrict the order of the observing operations. */ private enum ObservationState { From a582b6dcc3f8b6f6c5bfd110cf50c07cdf4967b4 Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 25 Jul 2023 11:14:50 +0300 Subject: [PATCH 57/60] Extract `waitForMutate` and `notifyCaller` methods in the `AsyncStateMutator` class. --- .../testing/server/e2e/AsyncObserverTest.java | 3 +- .../server/e2e/given/AsyncStateMutator.java | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java index 0ac01743..610db3d6 100644 --- a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java @@ -27,7 +27,6 @@ package io.spine.examples.shareaware.testing.server.e2e; import io.spine.examples.shareaware.testing.server.e2e.given.AsyncStateMutator; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -37,7 +36,7 @@ import static java.time.Duration.ofSeconds; import static org.junit.jupiter.api.Assertions.assertThrows; -@DisplayName("'AsyncObserver' should") +@DisplayName("`AsyncObserver` should") class AsyncObserverTest { @Test diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java index 00bd00de..c153797f 100644 --- a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java @@ -63,24 +63,46 @@ public AsyncStateMutator(String state, Duration delay) { mutationNotifier = consumer -> thread.execute(() -> { while (keepRunning) { - synchronized (lock) { - while (!isMutated.get() && keepRunning) { - try { - lock.wait(10000); - } catch (InterruptedException e) { - throw Exceptions.illegalStateWithCauseOf(e); - } - } - } - if (isMutated.get()) { - sleepUninterruptibly(delay); - consumer.accept(this.state.get()); - isMutated.set(false); - } + waitForMutate(); + notifyCaller(delay, consumer); } }); } + /** + * Waits for the state mutation to occur. + * + *

If the state mutation does not occur within 10 seconds, a + * {@code TimeoutException} is thrown. + */ + private void waitForMutate() { + synchronized (lock) { + while (!isMutated.get() && keepRunning) { + try { + lock.wait(10000); + } catch (InterruptedException e) { + throw Exceptions.illegalStateWithCauseOf(e); + } + } + } + } + + /** + * Notifies the caller with the modified state. + * + * @param delay + * the duration to delay before notifying the caller. + * @param consumer + * the consumer to notify with the modified state. + */ + private void notifyCaller(Duration delay, Consumer consumer) { + if (isMutated.get()) { + sleepUninterruptibly(delay); + consumer.accept(this.state.get()); + isMutated.set(false); + } + } + /** * Mutate the state by adding the random number to it. */ From e601beb8a1173ac9661632ddb24281f749803d2a Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Tue, 25 Jul 2023 13:04:46 +0300 Subject: [PATCH 58/60] Rename the values of `ObservationState` enum to `UPDATE_PACKED` and `UPDATE_UNPACKED`. --- .../testing/server/e2e/AsyncObserver.java | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index e4ca7ca7..fe89b90e 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -30,6 +30,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static io.spine.util.Exceptions.illegalStateWithCauseOf; @@ -49,11 +50,12 @@ public class AsyncObserver { private final Consumer howToCommand; - private CompletableFuture future = new CompletableFuture<>(); + private CompletableFuture statePack = new CompletableFuture<>(); private @Nullable S state = null; - private @Nullable ObservationState observationState = null; + private final AtomicReference observationState = + new AtomicReference<>(null); /** * Creates the new instance of the {@code AsyncObserver}. @@ -69,11 +71,11 @@ public AsyncObserver( this.howToCommand = howToCommand; howToObserve.accept(value -> { state = value; - if (future.isDone()) { - future = new CompletableFuture<>(); + if (statePack.isDone()) { + statePack = new CompletableFuture<>(); } - future.complete(value); - observationState = ObservationState.OBSERVED; + statePack.complete(value); + observationState.set(ObservationState.UPDATE_PACKED); }); } @@ -103,30 +105,41 @@ public S onceUpdatedAfter(C command) { * otherwise a {@code TimeoutException} will be thrown. */ private S waitForUpdate() { - if (observationState != null && - observationState != ObservationState.OBSERVED) { - future = new CompletableFuture<>(); - } - S updatedEntity = waitForFutureToComplete(future); - observationState = ObservationState.UPDATED; + checkForUpdate(); + S updatedEntity = unpackState(); + observationState.set(ObservationState.UPDATE_UNPACKED); return updatedEntity; } /** - * Waits for the provided {@code CompletableFuture} to complete. + * Checks for the update of the entity state to arrive + * at the moment when this method is called. * - *

If the completion of the provided {@code CompletableFuture} - * does not happen within 10 seconds, a {@code TimeoutException} will be thrown. + *

If the update of the entity state has not arrived + * this method forces the waiting for update. */ - private S waitForFutureToComplete(CompletableFuture future) { + private void checkForUpdate() { + if (observationState.get() != null && + observationState.get() != ObservationState.UPDATE_PACKED) { + statePack = new CompletableFuture<>(); + } + } + + /** + * Unpack the entity state from the {@link #statePack}. + * + *

If the entity state cannot be unpacked within 10 seconds, + * a {@code TimeoutException} will be thrown. + */ + private S unpackState() { try { - return future.whenComplete((value, error) -> { - if (error != null) { - throw illegalStateWithCauseOf(error); - } - }) - .orTimeout(10, SECONDS) - .get(); + return statePack.whenComplete((value, error) -> { + if (error != null) { + throw illegalStateWithCauseOf(error); + } + }) + .orTimeout(10, SECONDS) + .get(); } catch (InterruptedException | ExecutionException e) { throw illegalStateWithCauseOf(e); } @@ -139,7 +152,12 @@ private S waitForFutureToComplete(CompletableFuture future) { * and these states were introduced to restrict the order of the observing operations. */ private enum ObservationState { - OBSERVED, - UPDATED + + // The update entity state has arrived asynchronously + // and packed for synchronization with the main thread. + UPDATE_PACKED, + + // The update of the entity state has been unpacked and ready for usage in the main thread. + UPDATE_UNPACKED } } From d29548e14df66686a9791948d6e95f9ebf6256fc Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 26 Jul 2023 11:56:46 +0300 Subject: [PATCH 59/60] Implement `StateRecipient` and `StateRouter` interfaces. --- .../server/e2e/given/EntitySubscription.java | 32 +++++++--- .../testing/server/e2e/AsyncObserver.java | 4 +- .../testing/server/e2e/StateRecipient.java | 63 +++++++++++++++++++ .../testing/server/e2e/StateRouter.java | 59 +++++++++++++++++ .../server/e2e/given/AsyncStateMutator.java | 11 ++-- 5 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRecipient.java create mode 100644 testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRouter.java diff --git a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java index a6ed2974..8d7e0577 100644 --- a/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java +++ b/server/src/test/java/io/spine/examples/shareaware/server/e2e/given/EntitySubscription.java @@ -31,6 +31,9 @@ import io.spine.client.Client; import io.spine.core.UserId; import io.spine.examples.shareaware.testing.server.e2e.AsyncObserver; +import io.spine.examples.shareaware.testing.server.e2e.StateRouter; + +import java.util.function.Consumer; /** * Configures the {@code AsyncObserver} with how to send a command @@ -39,12 +42,27 @@ class EntitySubscription extends AsyncObserver { EntitySubscription(Class entityType, Client client, UserId user) { - super(consumer -> client.onBehalfOf(user) - .subscribeTo(entityType) - .observe(consumer) - .post(), - commandMessage -> client.onBehalfOf(user) - .command(commandMessage) - .postAndForget()); + super(subscribeAndObserve(entityType, client, user), command(client, user)); + } + + /** + * Returns callback that defines how to observe an entity state using {@code Spine} Client API. + */ + @SuppressWarnings("ResultOfMethodCallIgnored") // It's fine as this callback calls once in the constructor. + private static + StateRouter subscribeAndObserve(Class entityType, Client client, UserId user) { + return recipient -> client.onBehalfOf(user) + .subscribeTo(entityType) + .observe(recipient) + .post(); + } + + /** + * Returns callback which defines how to send a command using {@code Spine} Client API. + */ + private static Consumer command(Client client, UserId user) { + return commandMessage -> client.onBehalfOf(user) + .command(commandMessage) + .postAndForget(); } } diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index fe89b90e..1c2bd24a 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -66,10 +66,10 @@ public class AsyncObserver { * a callback that defines how to send a command {@link C}. */ public AsyncObserver( - Consumer> howToObserve, + StateRouter howToObserve, Consumer howToCommand) { this.howToCommand = howToCommand; - howToObserve.accept(value -> { + howToObserve.route(value -> { state = value; if (statePack.isDone()) { statePack = new CompletableFuture<>(); diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRecipient.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRecipient.java new file mode 100644 index 00000000..a9d80314 --- /dev/null +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRecipient.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.testing.server.e2e; + +import java.util.function.Consumer; + +/** + * Represents a recipient of state updates. + * + *

This interface is designed to provide a standardized way of accepting + * and handling state updates from various routers or observers. + * + * @param the type of the state to be received + */ +public interface StateRecipient extends Consumer { + + /** + * Receives the updated state. + * + *

This method is used to receive state updates + * and define how the recipient should handle it. + * + * @param state the updated state to be received and processed + */ + void receive(S state); + + /** + * {@inheritDoc} + * + * @implNote Inheritors can choose to override this method or directly implement + * the {@link #receive} method to handle the received state. + * + * @param state the updated state to be received and processed + */ + @Override + default void accept(S state) { + receive(state); + }; +} diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRouter.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRouter.java new file mode 100644 index 00000000..88bbb9cd --- /dev/null +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/StateRouter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed 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 + * + * 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.examples.shareaware.testing.server.e2e; + +import java.util.function.Consumer; + +/** + * Represents a state router to deliver the state to the {@code StateRecipient}. + * + *

A state router acts as a bridge between the subject that produces the state + * and the recipients that are interested in receiving and processing this state. + * + * @param the type of the state to be routed to the {@code StateRecipient} + */ +public interface StateRouter extends Consumer> { + + /** + * Routes the state to the specified recipient. + * + * @param recipient the recipient to which the state should be routed. + */ + void route(StateRecipient recipient); + + /** + * Accepts a {@code StateRecipient} and routes the state to it. + * + * @param recipient the recipient to which the state should be routed. + * @implNote Inheritors can choose to override this method or directly implement + * the {@link #route} method to route the state. + */ + @Override + default void accept(StateRecipient recipient) { + route(recipient); + } +} diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java index c153797f..e28ba146 100644 --- a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java @@ -26,6 +26,8 @@ package io.spine.examples.shareaware.testing.server.e2e.given; +import io.spine.examples.shareaware.testing.server.e2e.StateRecipient; +import io.spine.examples.shareaware.testing.server.e2e.StateRouter; import io.spine.util.Exceptions; import java.security.SecureRandom; @@ -34,7 +36,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static java.util.concurrent.Executors.newSingleThreadExecutor; @@ -44,7 +45,7 @@ */ public class AsyncStateMutator { - private final Consumer> mutationNotifier; + private final StateRouter mutationNotifier; private final AtomicBoolean isMutated = new AtomicBoolean(false); @@ -95,7 +96,7 @@ private void waitForMutate() { * @param consumer * the consumer to notify with the modified state. */ - private void notifyCaller(Duration delay, Consumer consumer) { + private void notifyCaller(Duration delay, StateRecipient consumer) { if (isMutated.get()) { sleepUninterruptibly(delay); consumer.accept(this.state.get()); @@ -138,9 +139,9 @@ public String state() { } /** - * Returns the {@code Consumer} that notifies the caller when the state has been mutated. + * Returns the callback that notifies the caller when the state has been mutated. */ - public Consumer> mutationNotifier() { + public StateRouter mutationNotifier() { return mutationNotifier; } } From 7a5cddf56198c7ad188a3d1746318a5267f0bd3c Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Wed, 26 Jul 2023 12:05:43 +0300 Subject: [PATCH 60/60] Update the `AsyncStateMutator` to use the `StateRouter` interface. --- .../testing/server/e2e/AsyncObserver.java | 2 +- .../testing/server/e2e/AsyncObserverTest.java | 6 ++--- .../server/e2e/given/AsyncStateMutator.java | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java index 1c2bd24a..d3192ce9 100644 --- a/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java +++ b/testutil-server/src/main/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserver.java @@ -61,7 +61,7 @@ public class AsyncObserver { * Creates the new instance of the {@code AsyncObserver}. * * @param howToObserve - * a callback that defines how to observe the state {@link S}. + * a callback that defines how to observe and route the state {@link S}. * @param howToCommand * a callback that defines how to send a command {@link C}. */ diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java index 610db3d6..c0e1025f 100644 --- a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/AsyncObserverTest.java @@ -43,7 +43,7 @@ class AsyncObserverTest { @DisplayName("observe mutation of the state with the slow response from the mutator") void observeMutationWithDelay() { var stateMutator = new AsyncStateMutator("state", ofSeconds(5)); - var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + var observer = new AsyncObserver<>(stateMutator.mutationRouter(), stateMutator::mutateState); var actualStateAfterFirstMutation = observer.onceUpdatedAfter(true); @@ -63,7 +63,7 @@ void observeMutationWithDelay() { @DisplayName("observe mutation of the state with the fast response from the mutator") void observeMutationWithoutDelay() { var stateMutator = new AsyncStateMutator("state", ofSeconds(0)); - var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + var observer = new AsyncObserver<>(stateMutator.mutationRouter(), stateMutator::mutateState); var actualStateAfterFirstMutation = observer.onceUpdatedAfter(true); @@ -79,7 +79,7 @@ void observeMutationWithoutDelay() { "when the state mutation were not received in 10 seconds") void throwExceptionWithoutMutation() { var stateMutator = new AsyncStateMutator("", ofSeconds(0)); - var observer = new AsyncObserver<>(stateMutator.mutationNotifier(), + var observer = new AsyncObserver<>(stateMutator.mutationRouter(), stateMutator::mutateState); IllegalStateException exception = diff --git a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java index e28ba146..06bc1516 100644 --- a/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java +++ b/testutil-server/src/test/java/io/spine/examples/shareaware/testing/server/e2e/given/AsyncStateMutator.java @@ -45,7 +45,7 @@ */ public class AsyncStateMutator { - private final StateRouter mutationNotifier; + private final StateRouter mutationRouter; private final AtomicBoolean isMutated = new AtomicBoolean(false); @@ -61,11 +61,11 @@ public class AsyncStateMutator { public AsyncStateMutator(String state, Duration delay) { this.state.set(state); - mutationNotifier = consumer -> + mutationRouter = recipient -> thread.execute(() -> { while (keepRunning) { waitForMutate(); - notifyCaller(delay, consumer); + routeState(delay, recipient); } }); } @@ -89,17 +89,17 @@ private void waitForMutate() { } /** - * Notifies the caller with the modified state. + * Routes the modified state to receiver. * * @param delay - * the duration to delay before notifying the caller. - * @param consumer - * the consumer to notify with the modified state. + * the duration to delay before routing the state + * @param recipient + * the recipient to route the modified state to */ - private void notifyCaller(Duration delay, StateRecipient consumer) { + private void routeState(Duration delay, StateRecipient recipient) { if (isMutated.get()) { sleepUninterruptibly(delay); - consumer.accept(this.state.get()); + recipient.receive(this.state.get()); isMutated.set(false); } } @@ -139,9 +139,9 @@ public String state() { } /** - * Returns the callback that notifies the caller when the state has been mutated. + * Returns the callback that routes the mutated state to the recipient. */ - public StateRouter mutationNotifier() { - return mutationNotifier; + public StateRouter mutationRouter() { + return mutationRouter; } }