This repository has been archived by the owner on May 9, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 109
/
PItemEntity.java
222 lines (179 loc) · 9.29 KB
/
PItemEntity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package com.example.auction.item.impl;
import akka.Done;
import com.lightbend.lagom.javadsl.api.transport.Forbidden;
import com.lightbend.lagom.javadsl.api.transport.NotFound;
import com.lightbend.lagom.javadsl.persistence.PersistentEntity;
import com.example.auction.item.impl.PItemCommand.*;
import com.example.auction.item.impl.PItemEvent.*;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class PItemEntity extends PersistentEntity<PItemCommand, PItemEvent, PItemState> {
@Override
public Behavior initialBehavior(Optional<PItemState> snapshotState) {
PItemStatus status = snapshotState.map(PItemState::getStatus).orElse(PItemStatus.NOT_CREATED);
switch (status) {
case NOT_CREATED:
return empty();
case CREATED:
return created(snapshotState.get());
case AUCTION:
return auction(snapshotState.get());
case COMPLETED:
return completed(snapshotState.get());
case CANCELLED:
return cancelled(snapshotState.get());
default:
throw new IllegalStateException("Unknown status: " + status);
}
}
private Behavior empty() {
BehaviorBuilder builder = newBehaviorBuilder(PItemState.empty());
builder.setReadOnlyCommandHandler(GetItem.class, this::getItem);
// maybe do some validation? Eg, check that UUID of item matches entity UUID...
builder.setCommandHandler(CreateItem.class, (create, ctx) ->
ctx.thenPersist(new ItemCreated(create.getItem()), evt -> ctx.reply(Done.getInstance()))
);
builder.setEventHandlerChangingBehavior(ItemCreated.class, evt -> created(PItemState.create(evt.getItem())));
builder.setReadOnlyCommandHandler(UpdateItem.class, (updateItem, ctx) ->
// TODO: avoid using a transport Exception on PersistentEntity
ctx.commandFailed(new NotFound(entityId()))
);
return builder.build();
}
private Behavior created(PItemState state) {
BehaviorBuilder builder = newBehaviorBuilder(state);
builder.setReadOnlyCommandHandler(GetItem.class, this::getItem);
// must only emit an ItemUpdated when there's changes to be notified and the commander
// is allowed to commit those changes.
builder.setCommandHandler(UpdateItem.class, (cmd, ctx) -> {
PItem pItem = state().getItem().get();
return updateItem(cmd, ctx, pItem, () -> emitUpdatedEvent(cmd, ctx, pItem));
});
builder.setEventHandler(ItemUpdated.class, updateItemData());
builder.setCommandHandler(StartAuction.class, (startCmd, ctx) -> {
if (startCmd.getUserId().equals(state().getItem().get().getCreator())) {
return ctx.thenPersist(new AuctionStarted(entityUuid(), Instant.now()), alreadyDone(ctx));
} else {
// TODO: use a `Forbidden` instance instead of `UpdateFailureException`. See https://github.com/lagom/lagom/issues/325
// User startCmd.getUserId() is not allowed to start this auction
ctx.commandFailed(UpdateFailureException.CANT_EDIT_ITEM_OF_ANOTHER_USER);
return ctx.done();
}
});
builder.setEventHandlerChangingBehavior(AuctionStarted.class,
evt -> auction(state().start(evt.getStartTime())));
return builder.build();
}
private Behavior auction(PItemState state) {
BehaviorBuilder builder = newBehaviorBuilder(state);
builder.setReadOnlyCommandHandler(GetItem.class, this::getItem);
// must only emit an ItemUpdated if the only difference is in the description and the commander
// is allowed to commit those changes.
builder.setCommandHandler(UpdateItem.class,
(cmd, ctx) -> {
PItem pItem = state().getItem().get();
return updateItem(cmd, ctx, pItem,
() -> {
if (pItem.getItemData().differOnDescriptionOnly(cmd.getItemData())) {
return emitUpdatedEvent(cmd, ctx, pItem);
} else {
ctx.commandFailed(new UpdateFailureException("During an Auction only the 'Description' may be edited."));
return ctx.done();
}
}
);
}
);
builder.setEventHandler(ItemUpdated.class, updateItemData());
builder.setCommandHandler(UpdatePrice.class, (cmd, ctx) ->
ctx.thenPersist(new PriceUpdated(entityUuid(), cmd.getPrice()), alreadyDone(ctx)));
builder.setEventHandler(PriceUpdated.class, evt -> state().updatePrice(evt.getPrice()));
builder.setCommandHandler(FinishAuction.class, (cmd, ctx) ->
ctx.thenPersist(new AuctionFinished(entityUuid(), cmd.getWinner(), cmd.getPrice()), alreadyDone(ctx)));
builder.setEventHandlerChangingBehavior(AuctionFinished.class,
evt -> completed(state().end(evt.getWinner(), evt.getPrice())));
// Ignored commands
builder.setReadOnlyCommandHandler(StartAuction.class, this::alreadyDone);
return builder.build();
}
private Behavior completed(PItemState state) {
BehaviorBuilder builder = newBehaviorBuilder(state);
builder.setReadOnlyCommandHandler(GetItem.class, this::getItem);
// a completed auction's item can't be edited.
builder.setReadOnlyCommandHandler(UpdateItem.class, (updateItem, ctx) ->
ctx.commandFailed(new UpdateFailureException("Can't update an item of a completed Auction."))
);
// a completed auction can't be restarted.
builder.setReadOnlyCommandHandler(StartAuction.class, (updateItem, ctx) ->
ctx.invalidCommand("Can't reopen an auction.")
);
// Ignore these commands, they may come due to at least once messaging
builder.setReadOnlyCommandHandler(UpdatePrice.class, this::alreadyDone);
builder.setReadOnlyCommandHandler(FinishAuction.class, this::alreadyDone);
return builder.build();
}
private Behavior cancelled(PItemState state) {
BehaviorBuilder builder = newBehaviorBuilder(state);
builder.setReadOnlyCommandHandler(GetItem.class, this::getItem);
// Ignore these commands, they may come due to at least once messaging
builder.setReadOnlyCommandHandler(UpdatePrice.class, this::alreadyDone);
builder.setReadOnlyCommandHandler(FinishAuction.class, this::alreadyDone);
return builder.build();
}
/**
* Invokes <code>onSuccess</code> if the commander in the command equals the creator of this PItem and the command payload
* differs from the current item data. Tipically <code>onSuccess</code> will
* be {{{@link PItemEntity#emitUpdatedEvent(UpdateItem, CommandContext, PItem)}}} but extra logic may be
* required in some cases.
*/
private Persist updateItem(UpdateItem cmd, CommandContext<PItem> ctx, PItem pItem, Supplier<Persist> onSuccess) {
if (!pItem.getCreator().equals(cmd.getCommander())) {
ctx.commandFailed(UpdateFailureException.CANT_EDIT_ITEM_OF_ANOTHER_USER);
return ctx.done();
} else if (!pItem.getItemData().equals(cmd.getItemData())) {
return onSuccess.get();
} else {
// when update and current are equal there's no need to emit an event.
return ctx.done();
}
}
private Persist emitUpdatedEvent(UpdateItem cmd, CommandContext<PItem> ctx, PItem pItem) {
return ctx.thenPersist(
new ItemUpdated(pItem.getId(), pItem.getCreator(), cmd.getItemData(), pItem.getStatus()),
// when the command is accepted for processing we return a copy of the
// state with the updates applied.
evt -> ctx.reply(pItem.withDetails(cmd.getItemData())));
}
/**
* convenience method to update the PItem in the PItemState with altering Instants, Status, etc...
*
* @return
*/
private Function<ItemUpdated, PItemState> updateItemData() {
return (evt) -> state().updateDetails(evt.getItemDetails());
}
/**
* Convenience method to handle commands which have already been processed (idempotent processing).
* TODO: review naming. See AuctionEvent#alreadyDone in bidding-impl project.
*/
private void alreadyDone(Object command, ReadOnlyCommandContext<Done> ctx) {
ctx.reply(Done.getInstance());
}
/**
* Convenience method to handle commands which have already been processed (idempotent processing).
* TODO: review naming. See AuctionEvent#alreadyDone in bidding-impl project.
*/
private <T> Consumer<T> alreadyDone(ReadOnlyCommandContext<Done> ctx) {
return (evt) -> ctx.reply(Done.getInstance());
}
private void getItem(GetItem get, ReadOnlyCommandContext<Optional<PItem>> ctx) {
ctx.reply(state().getItem());
}
private UUID entityUuid() {
return UUID.fromString(entityId());
}
}