diff --git a/account/account-web/src/main/java/demo/account/action/ActivateAccount.java b/account/account-web/src/main/java/demo/account/action/ActivateAccount.java index c14c022..d1fc15d 100644 --- a/account/account-web/src/main/java/demo/account/action/ActivateAccount.java +++ b/account/account-web/src/main/java/demo/account/action/ActivateAccount.java @@ -29,7 +29,7 @@ public class ActivateAccount extends Action { Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED) .contains(account.getStatus()), "The account cannot be activated"); - AccountService accountService = account.getProvider(AccountModule.class) + AccountService accountService = account.getModule(AccountModule.class) .getDefaultService(); // Activate the account diff --git a/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java b/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java index 0b62407..b6b88f9 100644 --- a/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java +++ b/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java @@ -26,7 +26,7 @@ public class ArchiveAccount extends Action { return (account) -> { Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); - AccountService accountService = account.getProvider(AccountModule.class) + AccountService accountService = account.getModule(AccountModule.class) .getDefaultService(); // Archive the account diff --git a/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java b/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java index 22e68b1..96af28f 100644 --- a/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java +++ b/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java @@ -26,7 +26,7 @@ public class ConfirmAccount extends Action { return (account) -> { Assert.isTrue(account.getStatus() == ACCOUNT_PENDING, "The account has already been confirmed"); - AccountService accountService = account.getProvider(AccountModule.class) + AccountService accountService = account.getModule(AccountModule.class) .getDefaultService(); // Confirm the account diff --git a/account/account-web/src/main/java/demo/account/action/SuspendAccount.java b/account/account-web/src/main/java/demo/account/action/SuspendAccount.java index 995bcbe..bec22b4 100644 --- a/account/account-web/src/main/java/demo/account/action/SuspendAccount.java +++ b/account/account-web/src/main/java/demo/account/action/SuspendAccount.java @@ -26,7 +26,7 @@ public class SuspendAccount extends Action { return (account) -> { Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); - AccountService accountService = account.getProvider(AccountModule.class) + AccountService accountService = account.getModule(AccountModule.class) .getDefaultService(); // Suspend the account diff --git a/account/account-web/src/main/java/demo/account/domain/Account.java b/account/account-web/src/main/java/demo/account/domain/Account.java index d8e7e40..1680b57 100644 --- a/account/account-web/src/main/java/demo/account/domain/Account.java +++ b/account/account-web/src/main/java/demo/account/domain/Account.java @@ -140,9 +140,9 @@ public class Account extends AbstractEntity { */ @Override @SuppressWarnings("unchecked") - public , A extends Aggregate> T getProvider() throws + public , A extends Aggregate> T getModule() throws IllegalArgumentException { - AccountModule accountProvider = getProvider(AccountModule.class); + AccountModule accountProvider = getModule(AccountModule.class); return (T) accountProvider; } diff --git a/account/account-web/src/main/java/demo/config/WebMvcConfig.java b/account/account-web/src/main/java/demo/config/WebMvcConfig.java index 003cb08..56979b4 100644 --- a/account/account-web/src/main/java/demo/config/WebMvcConfig.java +++ b/account/account-web/src/main/java/demo/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package demo.config; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; @@ -24,6 +25,7 @@ public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List> converters) { final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); converter.setObjectMapper(objectMapper); converters.add(converter); } diff --git a/account/account-web/src/main/java/demo/order/domain/Order.java b/account/account-web/src/main/java/demo/order/domain/Order.java index c487f04..b6fe1a2 100644 --- a/account/account-web/src/main/java/demo/order/domain/Order.java +++ b/account/account-web/src/main/java/demo/order/domain/Order.java @@ -76,7 +76,7 @@ public class Order extends Aggregate { } public Order post() { - OrderModule orderProvider = getProvider(); + OrderModule orderProvider = getModule(); return orderProvider.getDefaultService() .create(this); } @@ -115,9 +115,9 @@ public class Order extends Aggregate { */ @Override @SuppressWarnings("unchecked") - public , A extends Aggregate> T getProvider() throws + public , A extends Aggregate> T getModule() throws IllegalArgumentException { - OrderModule orderProvider = getProvider(OrderModule.class); + OrderModule orderProvider = getModule(OrderModule.class); return (T) orderProvider; } diff --git a/account/account-web/src/main/resources/application.yml b/account/account-web/src/main/resources/application.yml index dbbf0c2..7850a4d 100644 --- a/account/account-web/src/main/resources/application.yml +++ b/account/account-web/src/main/resources/application.yml @@ -4,6 +4,8 @@ spring: --- spring: profiles: development + jackson: + default-property-inclusion: non_null cloud: stream: bindings: diff --git a/account/account-worker/src/main/resources/application.yml b/account/account-worker/src/main/resources/application.yml index 8c05513..90ebffe 100644 --- a/account/account-worker/src/main/resources/application.yml +++ b/account/account-worker/src/main/resources/application.yml @@ -13,8 +13,10 @@ spring: contentType: 'application/json' consumer: durableSubscription: true + jackson: + default-property-inclusion: non_null server: - port: 8081 + port: 0 amazon: aws: access-key-id: replace diff --git a/order/order-web/src/main/java/demo/config/WebMvcConfig.java b/order/order-web/src/main/java/demo/config/WebMvcConfig.java index 003cb08..56979b4 100644 --- a/order/order-web/src/main/java/demo/config/WebMvcConfig.java +++ b/order/order-web/src/main/java/demo/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package demo.config; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; @@ -24,6 +25,7 @@ public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List> converters) { final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); converter.setObjectMapper(objectMapper); converters.add(converter); } diff --git a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java index 9052ce9..1a712fa 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java @@ -21,7 +21,7 @@ public class ConnectAccount extends Action { public BiConsumer getConsumer() { return (order, accountId) -> { - OrderService orderService = order.getProvider(OrderModule.class) + OrderService orderService = order.getModule(OrderModule.class) .getDefaultService(); // Connect the account diff --git a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java index e364298..6c26805 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java @@ -22,7 +22,7 @@ public class ConnectPayment extends Action { public BiConsumer getConsumer() { return (order, paymentId) -> { - OrderService orderService = order.getProvider(OrderModule.class) + OrderService orderService = order.getModule(OrderModule.class) .getDefaultService(); // Connect the account diff --git a/order/order-web/src/main/java/demo/order/action/CreatePayment.java b/order/order-web/src/main/java/demo/order/action/CreatePayment.java index c6c7b5c..8bdd629 100644 --- a/order/order-web/src/main/java/demo/order/action/CreatePayment.java +++ b/order/order-web/src/main/java/demo/order/action/CreatePayment.java @@ -42,7 +42,7 @@ public class CreatePayment extends Action { Assert.isTrue(order.getPaymentId() == null, "Payment has already been created"); Assert.isTrue(order.getStatus() == OrderStatus.ACCOUNT_CONNECTED, "Account must be connected first"); - OrderService orderService = order.getProvider(OrderModule.class) + OrderService orderService = order.getModule(OrderModule.class) .getDefaultService(); Payment payment = new Payment(); diff --git a/order/order-web/src/main/java/demo/order/action/DeleteOrder.java b/order/order-web/src/main/java/demo/order/action/DeleteOrder.java new file mode 100644 index 0000000..254ca44 --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/DeleteOrder.java @@ -0,0 +1,40 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.order.domain.Order; +import demo.order.domain.OrderModule; +import demo.payment.domain.Payment; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.function.Consumer; + +/** + * Processes a {@link Payment} for an {@link Order}. + * + * @author Kenny Bastani + */ +@Service +public class DeleteOrder extends Action { + + private RestTemplate restTemplate; + + public DeleteOrder(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public Consumer getConsumer() { + return (order) -> { + // Delete payment + if (order.getPaymentId() != null) { + String href = "http://payment-web/v1/payments/" + order.getPaymentId(); + restTemplate.delete(href); + } + + // Delete order + order.getModule(OrderModule.class) + .getDefaultService() + .delete(order.getIdentity()); + }; + } +} diff --git a/order/order-web/src/main/java/demo/order/controller/OrderController.java b/order/order-web/src/main/java/demo/order/controller/OrderController.java index 0886db6..6b4c41d 100644 --- a/order/order-web/src/main/java/demo/order/controller/OrderController.java +++ b/order/order-web/src/main/java/demo/order/controller/OrderController.java @@ -52,14 +52,14 @@ public class OrderController { @RequestMapping(path = "/orders/{id}") public ResponseEntity getOrder(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource(id)) + return Optional.ofNullable(getOrderResource(orderService.get(id))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @DeleteMapping(path = "/orders/{id}") public ResponseEntity deleteOrder(@PathVariable Long id) { - return Optional.ofNullable(orderService.delete(id)) + return Optional.ofNullable(orderService.get(id).delete()) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) .orElseThrow(() -> new RuntimeException("Order deletion failed")); } @@ -132,19 +132,6 @@ public class OrderController { .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - /** - * Retrieves a hypermedia resource for {@link Order} with the specified identifier. - * - * @param id is the unique identifier for looking up the {@link Order} entity - * @return a hypermedia resource for the fetched {@link Order} - */ - private Resource getOrderResource(Long id) { - // Get the order for the provided id - Order order = orderService.get(id); - - return getOrderResource(order); - } - /** * Creates a new {@link Order} entity and persists the result to the repository. * @@ -225,7 +212,7 @@ public class OrderController { * @return is a hypermedia enriched resource for the supplied {@link Order} entity */ private Resource getOrderResource(Order order) { - Assert.notNull(order, "Order must not be null"); + if(order == null) return null; // Add command link order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands")); diff --git a/order/order-web/src/main/java/demo/order/domain/Order.java b/order/order-web/src/main/java/demo/order/domain/Order.java index 30f64f3..03f0b92 100644 --- a/order/order-web/src/main/java/demo/order/domain/Order.java +++ b/order/order-web/src/main/java/demo/order/domain/Order.java @@ -135,6 +135,14 @@ public class Order extends AbstractEntity { return this; } + public boolean delete() { + getAction(DeleteOrder.class) + .getConsumer() + .accept(this); + + return true; + } + @JsonIgnore public Double calculateTotal() { return getLineItems() diff --git a/order/order-web/src/main/resources/application.yml b/order/order-web/src/main/resources/application.yml index 2d0e424..2b9f626 100644 --- a/order/order-web/src/main/resources/application.yml +++ b/order/order-web/src/main/resources/application.yml @@ -10,6 +10,8 @@ spring: output: destination: order contentType: 'application/json' + jackson: + default-property-inclusion: non_null server: port: 0 events: diff --git a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java index f6fa652..f02d3bd 100644 --- a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java +++ b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java @@ -1,12 +1,13 @@ package demo.config; -import demo.event.OrderEvent; -import demo.event.OrderEventType; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import demo.function.*; -import demo.order.Order; -import demo.order.OrderStatus; -import demo.payment.Payment; -import demo.stream.OrderStream; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvents; +import demo.payment.domain.Payment; +import demo.order.event.OrderEventProcessor; import org.apache.log4j.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,9 +31,9 @@ import java.util.Map; * expressions. Actions are executed during transitions between a source state and a target state. *

* A state machine provides a robust declarative language for describing the state of an {@link Order} - * resource given a sequence of ordered {@link demo.event.OrderEvents}. When an event is received - * in {@link OrderStream}, an in-memory state machine is fully replicated given the - * {@link demo.event.OrderEvents} attached to an {@link Order} resource. + * resource given a sequence of ordered {@link OrderEvents}. When an event is received + * in {@link OrderEventProcessor}, an in-memory state machine is fully replicated given the + * {@link OrderEvents} attached to an {@link Order} resource. * * @author kbastani */ @@ -50,7 +51,6 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter states) { try { - // Describe the initial condition of the order state machine states.withStates() .initial(OrderStatus.ORDER_CREATED) .states(EnumSet.allOf(OrderStatus.class)); @@ -59,6 +59,32 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter + * The {@link OrderFunction} argument is only applied if an {@link OrderEvent} is provided as a + * message header in the {@link StateContext}. + * + * @param context is the state machine context that may include an {@link OrderEvent} + * @param orderFunction is the order function to apply after the state machine has completed replication + * @return an {@link OrderEvent} only if this event has not yet been processed, otherwise returns null + */ + private OrderEvent applyEvent(StateContext context, OrderFunction orderFunction) { + OrderEvent event = null; + log.info(String.format("Replicate event: %s", context.getMessage().getPayload())); + + if (context.getMessageHeader("event") != null) { + event = context.getMessageHeaders().get("event", OrderEvent.class); + log.info(String.format("State replication complete: %s", event.getType())); + orderFunction.apply(event); + } + + return event; + } + /** * Configures the {@link StateMachine} that describes how {@link OrderEventType} drives the state * of an {@link Order}. Events are applied as transitions from a source {@link OrderStatus} to @@ -253,20 +279,26 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter template = new HashMap(); + Map template = new HashMap<>(); + template.put("orderId", order.getIdentity()); + + // Connect payment to order + Payment payment = paymentResource.follow("self", "commands", "connectOrder") + .withTemplateParameters(template) + .toObject(Payment.class); + + template = new HashMap<>(); template.put("paymentId", payment.getPaymentId()); - return orderResource.follow("commands", "connectPayment") + + // Connect order to payment + order = orderResource.follow("commands", "connectPayment") .withTemplateParameters(template) .toObject(Order.class); + return order; })); } @@ -321,36 +353,6 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter - * The {@link OrderFunction} argument is only applied if an {@link OrderEvent} is provided as a - * message header in the {@link StateContext}. - * - * @param context is the state machine context that may include an {@link OrderEvent} - * @param orderFunction is the order function to apply after the state machine has completed replication - * @return an {@link OrderEvent} only if this event has not yet been processed, otherwise returns null - */ - private OrderEvent applyEvent(StateContext context, - OrderFunction orderFunction) { - OrderEvent orderEvent = null; - // Log out the progress of the state machine replication - log.info("Replicate event: " + context.getMessage().getPayload()); - - // The machine is finished replicating when an OrderEvent is found in the message header - if (context.getMessageHeader("event") != null) { - orderEvent = (OrderEvent) context.getMessageHeader("event"); - log.info("State machine replicated: " + orderEvent.getType()); - - // Apply the provided function to the OrderEvent - orderFunction.apply(orderEvent); - } - - return orderEvent; - } } diff --git a/order/order-worker/src/main/java/demo/domain/BaseEntity.java b/order/order-worker/src/main/java/demo/domain/AbstractEntity.java similarity index 89% rename from order/order-worker/src/main/java/demo/domain/BaseEntity.java rename to order/order-worker/src/main/java/demo/domain/AbstractEntity.java index 586dbb4..083d231 100644 --- a/order/order-worker/src/main/java/demo/domain/BaseEntity.java +++ b/order/order-worker/src/main/java/demo/domain/AbstractEntity.java @@ -2,12 +2,12 @@ package demo.domain; import org.springframework.hateoas.ResourceSupport; -public class BaseEntity extends ResourceSupport { +public class AbstractEntity extends ResourceSupport { private Long createdAt; private Long lastModified; - public BaseEntity() { + public AbstractEntity() { } public Long getCreatedAt() { diff --git a/order/order-worker/src/main/java/demo/event/EventService.java b/order/order-worker/src/main/java/demo/event/EventService.java deleted file mode 100644 index b424485..0000000 --- a/order/order-worker/src/main/java/demo/event/EventService.java +++ /dev/null @@ -1,84 +0,0 @@ -package demo.event; - -import demo.order.Order; -import demo.order.OrderStatus; -import demo.state.StateMachineService; -import org.apache.log4j.Logger; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.client.Traverson; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.statemachine.StateMachine; -import org.springframework.stereotype.Service; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; - -@Service -public class EventService { - - final private Logger log = Logger.getLogger(EventService.class); - final private StateMachineService stateMachineService; - - public EventService(StateMachineService stateMachineService) { - this.stateMachineService = stateMachineService; - } - - public Order apply(OrderEvent orderEvent) { - - Order result; - - log.info(orderEvent); - - log.info("Order event received: " + orderEvent.getLink("self").getHref()); - - // Generate a state machine for computing the state of the order resource - StateMachine stateMachine = - stateMachineService.getStateMachine(); - - // Follow the hypermedia link to fetch the attached order - Traverson traverson = new Traverson( - URI.create(orderEvent.getLink("order").getHref()), - MediaTypes.HAL_JSON - ); - - // Get the event log for the attached order resource - OrderEvents events = traverson.follow("events") - .toEntity(OrderEvents.class) - .getBody(); - - // Prepare order event message headers - Map headerMap = new HashMap<>(); - headerMap.put("event", orderEvent); - - // Replicate the current state of the order resource - events.getContent() - .stream() - .sorted((a1, a2) -> a1.getCreatedAt().compareTo(a2.getCreatedAt())) - .forEach(e -> { - MessageHeaders headers = new MessageHeaders(null); - - // Check to see if this is the current event - if (e.getLink("self").equals(orderEvent.getLink("self"))) { - headers = new MessageHeaders(headerMap); - } - - // Send the event to the state machine - stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), headers)); - }); - - - // Get result - Map context = stateMachine.getExtendedState() - .getVariables(); - - // Get the order result - result = (Order) context.getOrDefault("order", null); - - // Destroy the state machine - stateMachine.stop(); - - return result; - } -} diff --git a/order/order-worker/src/main/java/demo/function/AccountConnected.java b/order/order-worker/src/main/java/demo/function/AccountConnected.java index 3fe3dc5..47591da 100644 --- a/order/order-worker/src/main/java/demo/function/AccountConnected.java +++ b/order/order-worker/src/main/java/demo/function/AccountConnected.java @@ -1,9 +1,9 @@ package demo.function; -import demo.order.Order; -import demo.order.OrderStatus; -import demo.event.OrderEvent; -import demo.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/OrderCreated.java b/order/order-worker/src/main/java/demo/function/OrderCreated.java index 6c98ce3..c7fb4ce 100644 --- a/order/order-worker/src/main/java/demo/function/OrderCreated.java +++ b/order/order-worker/src/main/java/demo/function/OrderCreated.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/OrderFunction.java b/order/order-worker/src/main/java/demo/function/OrderFunction.java index 7214b53..1cc71db 100644 --- a/order/order-worker/src/main/java/demo/function/OrderFunction.java +++ b/order/order-worker/src/main/java/demo/function/OrderFunction.java @@ -1,9 +1,9 @@ package demo.function; -import demo.order.Order; -import demo.order.OrderStatus; -import demo.event.OrderEvent; -import demo.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; @@ -11,7 +11,7 @@ import java.util.function.Function; /** * The {@link OrderFunction} is an abstraction used to map actions that are triggered by - * state transitions on a {@link demo.order.Order} resource on to a function. Mapped functions + * state transitions on a {@link Order} resource on to a function. Mapped functions * can take multiple forms and reside either remotely or locally on the classpath of this application. * * @author kbastani diff --git a/order/order-worker/src/main/java/demo/function/PaymentConnected.java b/order/order-worker/src/main/java/demo/function/PaymentConnected.java index db1dace..033484c 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentConnected.java +++ b/order/order-worker/src/main/java/demo/function/PaymentConnected.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/PaymentCreated.java b/order/order-worker/src/main/java/demo/function/PaymentCreated.java index a8743c4..baabc31 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentCreated.java +++ b/order/order-worker/src/main/java/demo/function/PaymentCreated.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/PaymentFailed.java b/order/order-worker/src/main/java/demo/function/PaymentFailed.java index 0227ca9..b009a91 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentFailed.java +++ b/order/order-worker/src/main/java/demo/function/PaymentFailed.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/PaymentPending.java b/order/order-worker/src/main/java/demo/function/PaymentPending.java index 75e53cd..1e83d9c 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentPending.java +++ b/order/order-worker/src/main/java/demo/function/PaymentPending.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java index 7f522b9..95073b0 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java +++ b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/ReservationFailed.java b/order/order-worker/src/main/java/demo/function/ReservationFailed.java index 6933bce..834b041 100644 --- a/order/order-worker/src/main/java/demo/function/ReservationFailed.java +++ b/order/order-worker/src/main/java/demo/function/ReservationFailed.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/ReservationPending.java b/order/order-worker/src/main/java/demo/function/ReservationPending.java index 6e341b6..23cd1ad 100644 --- a/order/order-worker/src/main/java/demo/function/ReservationPending.java +++ b/order/order-worker/src/main/java/demo/function/ReservationPending.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java b/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java index 7c9ce41..f833121 100644 --- a/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java +++ b/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java @@ -1,9 +1,9 @@ package demo.function; -import demo.event.OrderEvent; -import demo.event.OrderEventType; -import demo.order.Order; -import demo.order.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; diff --git a/order/order-worker/src/main/java/demo/order/StateFactory.java b/order/order-worker/src/main/java/demo/order/StateFactory.java new file mode 100644 index 0000000..cc4b871 --- /dev/null +++ b/order/order-worker/src/main/java/demo/order/StateFactory.java @@ -0,0 +1,82 @@ +package demo.order; + +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import demo.order.event.OrderEvents; +import org.apache.log4j.Logger; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.client.Traverson; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +@Service +public class StateFactory { + + final private Logger log = Logger.getLogger(StateFactory.class); + final private StateService stateService; + + public StateFactory(StateService stateService) { + this.stateService = stateService; + } + + public Order apply(OrderEvent orderEvent) { + Assert.notNull(orderEvent, "Cannot apply a null event"); + Assert.notNull(orderEvent.getId(), "The event payload's identity link was not found"); + + StateMachine stateMachine = getStateMachine(orderEvent); + stateMachine.stop(); + + return stateMachine.getExtendedState().get("order", Order.class); + } + + private StateMachine getStateMachine(OrderEvent orderEvent) { + Link eventId = orderEvent.getId(); + log.info(String.format("Order event received: %s", eventId)); + + StateMachine stateMachine; + Map contextMap; + OrderEvents eventLog; + + eventLog = getEventLog(orderEvent); + contextMap = getEventHeaders(orderEvent); + stateMachine = stateService.newStateMachine(); + + // Replicate the aggregate state + eventLog.getContent().stream() + .sorted((a, b) -> a.getCreatedAt().compareTo(b.getCreatedAt())) + .forEach(e -> stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), e.getId() + .equals(eventId) ? new MessageHeaders(contextMap) : new MessageHeaders(null)))); + + return stateMachine; + } + + private Map getEventHeaders(OrderEvent orderEvent) { + Map headerMap = new HashMap<>(); + headerMap.put("event", orderEvent); + return headerMap; + } + + private OrderEvents getEventLog(OrderEvent event) { + // Follow the hypermedia link to fetch the attached order + Traverson traverson = new Traverson( + URI.create(event.getLink("order") + .getHref()), + MediaTypes.HAL_JSON + ); + + // Get the event log for the attached order resource + return traverson.follow("events") + .toEntity(OrderEvents.class) + .getBody(); + } +} diff --git a/order/order-worker/src/main/java/demo/state/StateMachineService.java b/order/order-worker/src/main/java/demo/order/StateService.java similarity index 57% rename from order/order-worker/src/main/java/demo/state/StateMachineService.java rename to order/order-worker/src/main/java/demo/order/StateService.java index b3327dc..aed3a9c 100644 --- a/order/order-worker/src/main/java/demo/state/StateMachineService.java +++ b/order/order-worker/src/main/java/demo/order/StateService.java @@ -1,7 +1,10 @@ -package demo.state; +package demo.order; -import demo.order.OrderStatus; -import demo.event.OrderEventType; +import demo.order.domain.Order; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEventType; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEvents; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.stereotype.Service; @@ -9,27 +12,27 @@ import org.springframework.stereotype.Service; import java.util.UUID; /** - * The {@link StateMachineService} provides factory access to get new state machines for - * replicating the state of an {@link demo.order.Order} from {@link demo.event.OrderEvents}. + * The {@link StateService} provides factory access to get new state machines for + * replicating the state of an {@link Order} from {@link OrderEvents}. * * @author kbastani */ @Service -public class StateMachineService { +public class StateService { private final StateMachineFactory factory; - public StateMachineService(StateMachineFactory factory) { + public StateService(StateMachineFactory factory) { this.factory = factory; } /** * Create a new state machine that is initially configured and ready for replicating - * the state of an {@link demo.order.Order} from a sequence of {@link demo.event.OrderEvent}. + * the state of an {@link Order} from a sequence of {@link OrderEvent}. * * @return a new instance of {@link StateMachine} */ - public StateMachine getStateMachine() { + public StateMachine newStateMachine() { // Create a new state machine in its initial state StateMachine stateMachine = factory.getStateMachine(UUID.randomUUID().toString()); diff --git a/order/order-worker/src/main/java/demo/event/EventController.java b/order/order-worker/src/main/java/demo/order/controller/EventController.java similarity index 80% rename from order/order-worker/src/main/java/demo/event/EventController.java rename to order/order-worker/src/main/java/demo/order/controller/EventController.java index 1bb5607..8d6c978 100644 --- a/order/order-worker/src/main/java/demo/event/EventController.java +++ b/order/order-worker/src/main/java/demo/order/controller/EventController.java @@ -1,5 +1,7 @@ -package demo.event; +package demo.order.controller; +import demo.order.StateFactory; +import demo.order.event.OrderEvent; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -13,9 +15,9 @@ import java.util.Optional; @RequestMapping("/v1") public class EventController { - private EventService eventService; + private StateFactory eventService; - public EventController(EventService eventService) { + public EventController(StateFactory eventService) { this.eventService = eventService; } diff --git a/order/order-worker/src/main/java/demo/address/Address.java b/order/order-worker/src/main/java/demo/order/domain/Address.java similarity index 98% rename from order/order-worker/src/main/java/demo/address/Address.java rename to order/order-worker/src/main/java/demo/order/domain/Address.java index 265bd5c..9495af7 100644 --- a/order/order-worker/src/main/java/demo/address/Address.java +++ b/order/order-worker/src/main/java/demo/order/domain/Address.java @@ -1,4 +1,4 @@ -package demo.address; +package demo.order.domain; import java.io.Serializable; diff --git a/order/order-worker/src/main/java/demo/address/AddressType.java b/order/order-worker/src/main/java/demo/order/domain/AddressType.java similarity index 67% rename from order/order-worker/src/main/java/demo/address/AddressType.java rename to order/order-worker/src/main/java/demo/order/domain/AddressType.java index f6ba778..79b5424 100644 --- a/order/order-worker/src/main/java/demo/address/AddressType.java +++ b/order/order-worker/src/main/java/demo/order/domain/AddressType.java @@ -1,4 +1,4 @@ -package demo.address; +package demo.order.domain; public enum AddressType { SHIPPING, diff --git a/order/order-worker/src/main/java/demo/order/LineItem.java b/order/order-worker/src/main/java/demo/order/domain/LineItem.java similarity index 98% rename from order/order-worker/src/main/java/demo/order/LineItem.java rename to order/order-worker/src/main/java/demo/order/domain/LineItem.java index 02a26a4..eebfbde 100644 --- a/order/order-worker/src/main/java/demo/order/LineItem.java +++ b/order/order-worker/src/main/java/demo/order/domain/LineItem.java @@ -1,4 +1,4 @@ -package demo.order; +package demo.order.domain; import java.io.Serializable; diff --git a/order/order-worker/src/main/java/demo/order/Order.java b/order/order-worker/src/main/java/demo/order/domain/Order.java similarity index 54% rename from order/order-worker/src/main/java/demo/order/Order.java rename to order/order-worker/src/main/java/demo/order/domain/Order.java index eb8c22b..8f10a8f 100644 --- a/order/order-worker/src/main/java/demo/order/Order.java +++ b/order/order-worker/src/main/java/demo/order/domain/Order.java @@ -1,16 +1,16 @@ -package demo.order; +package demo.order.domain; -import demo.address.Address; -import demo.address.AddressType; -import demo.domain.BaseEntity; -import demo.event.OrderEvent; +import com.fasterxml.jackson.annotation.JsonProperty; +import demo.domain.AbstractEntity; +import demo.order.event.OrderEvent; +import org.springframework.hateoas.Link; import java.util.HashSet; import java.util.Set; -public class Order extends BaseEntity { +public class Order extends AbstractEntity { - private Long orderId; + private Long id; private OrderStatus status; @@ -23,19 +23,13 @@ public class Order extends BaseEntity { public Order() { } - public Order(String accountNumber, Address shippingAddress) { - this(); - this.shippingAddress = shippingAddress; - if (shippingAddress.getAddressType() == null) - this.shippingAddress.setAddressType(AddressType.SHIPPING); + @JsonProperty("orderId") + public Long getIdentity() { + return this.id; } - public Long getOrderId() { - return orderId; - } - - public void setOrderId(Long id) { - this.orderId = orderId; + public void setIdentity(Long id) { + this.id = id; } public OrderStatus getStatus() { @@ -74,14 +68,13 @@ public class Order extends BaseEntity { lineItems.add(lineItem); } + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ @Override - public String toString() { - return "Order{" + - "orderId=" + orderId + - ", status=" + status + - ", events=" + events + - ", lineItems=" + lineItems + - ", shippingAddress=" + shippingAddress + - "} " + super.toString(); + public Link getId() { + return getLink("self"); } + + } diff --git a/order/order-worker/src/main/java/demo/order/OrderStatus.java b/order/order-worker/src/main/java/demo/order/domain/OrderStatus.java similarity index 90% rename from order/order-worker/src/main/java/demo/order/OrderStatus.java rename to order/order-worker/src/main/java/demo/order/domain/OrderStatus.java index b4f6230..5357b46 100644 --- a/order/order-worker/src/main/java/demo/order/OrderStatus.java +++ b/order/order-worker/src/main/java/demo/order/domain/OrderStatus.java @@ -1,4 +1,4 @@ -package demo.order; +package demo.order.domain; public enum OrderStatus { ORDER_CREATED, diff --git a/order/order-worker/src/main/java/demo/event/OrderEvent.java b/order/order-worker/src/main/java/demo/order/event/OrderEvent.java similarity index 80% rename from order/order-worker/src/main/java/demo/event/OrderEvent.java rename to order/order-worker/src/main/java/demo/order/event/OrderEvent.java index 605803e..cc4fc49 100644 --- a/order/order-worker/src/main/java/demo/event/OrderEvent.java +++ b/order/order-worker/src/main/java/demo/order/event/OrderEvent.java @@ -1,8 +1,8 @@ -package demo.event; +package demo.order.event; -import demo.domain.BaseEntity; +import demo.domain.AbstractEntity; -public class OrderEvent extends BaseEntity { +public class OrderEvent extends AbstractEntity { private OrderEventType type; diff --git a/order/order-worker/src/main/java/demo/stream/OrderStream.java b/order/order-worker/src/main/java/demo/order/event/OrderEventProcessor.java similarity index 55% rename from order/order-worker/src/main/java/demo/stream/OrderStream.java rename to order/order-worker/src/main/java/demo/order/event/OrderEventProcessor.java index 6245be0..8e8517e 100644 --- a/order/order-worker/src/main/java/demo/stream/OrderStream.java +++ b/order/order-worker/src/main/java/demo/order/event/OrderEventProcessor.java @@ -1,8 +1,7 @@ -package demo.stream; +package demo.order.event; -import demo.event.EventService; -import demo.event.OrderEvent; -import demo.order.Order; +import demo.order.StateFactory; +import demo.order.domain.Order; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; @@ -10,24 +9,24 @@ import org.springframework.cloud.stream.messaging.Sink; import org.springframework.context.annotation.Profile; /** - * The {@link OrderStream} monitors for a variety of {@link OrderEvent} domain + * The {@link OrderEventProcessor} monitors for a variety of {@link OrderEvent} domain * events for an {@link Order}. * * @author kbastani */ @EnableAutoConfiguration @EnableBinding(Sink.class) -@Profile({ "cloud", "development" }) -public class OrderStream { +@Profile({"cloud", "development"}) +public class OrderEventProcessor { - private EventService eventService; + private StateFactory stateFactory; - public OrderStream(EventService eventService) { - this.eventService = eventService; + public OrderEventProcessor(StateFactory stateFactory) { + this.stateFactory = stateFactory; } @StreamListener(Sink.INPUT) public void streamListener(OrderEvent orderEvent) { - eventService.apply(orderEvent); + stateFactory.apply(orderEvent); } } diff --git a/order/order-worker/src/main/java/demo/event/OrderEventType.java b/order/order-worker/src/main/java/demo/order/event/OrderEventType.java similarity index 91% rename from order/order-worker/src/main/java/demo/event/OrderEventType.java rename to order/order-worker/src/main/java/demo/order/event/OrderEventType.java index 1b31827..23b441e 100644 --- a/order/order-worker/src/main/java/demo/event/OrderEventType.java +++ b/order/order-worker/src/main/java/demo/order/event/OrderEventType.java @@ -1,4 +1,4 @@ -package demo.event; +package demo.order.event; public enum OrderEventType { ORDER_CREATED, diff --git a/order/order-worker/src/main/java/demo/event/OrderEvents.java b/order/order-worker/src/main/java/demo/order/event/OrderEvents.java similarity index 80% rename from order/order-worker/src/main/java/demo/event/OrderEvents.java rename to order/order-worker/src/main/java/demo/order/event/OrderEvents.java index f67c72a..c2da683 100644 --- a/order/order-worker/src/main/java/demo/event/OrderEvents.java +++ b/order/order-worker/src/main/java/demo/order/event/OrderEvents.java @@ -1,4 +1,4 @@ -package demo.event; +package demo.order.event; import org.springframework.hateoas.Resources; diff --git a/order/order-worker/src/main/java/demo/payment/Payment.java b/order/order-worker/src/main/java/demo/payment/domain/Payment.java similarity index 90% rename from order/order-worker/src/main/java/demo/payment/Payment.java rename to order/order-worker/src/main/java/demo/payment/domain/Payment.java index 3e5b96e..e9dca48 100644 --- a/order/order-worker/src/main/java/demo/payment/Payment.java +++ b/order/order-worker/src/main/java/demo/payment/domain/Payment.java @@ -1,8 +1,8 @@ -package demo.payment; +package demo.payment.domain; -import demo.domain.BaseEntity; +import demo.domain.AbstractEntity; -public class Payment extends BaseEntity { +public class Payment extends AbstractEntity { private Long paymentId; private Double amount; diff --git a/order/order-worker/src/main/java/demo/payment/PaymentMethod.java b/order/order-worker/src/main/java/demo/payment/domain/PaymentMethod.java similarity index 61% rename from order/order-worker/src/main/java/demo/payment/PaymentMethod.java rename to order/order-worker/src/main/java/demo/payment/domain/PaymentMethod.java index 1591721..b8f8fac 100644 --- a/order/order-worker/src/main/java/demo/payment/PaymentMethod.java +++ b/order/order-worker/src/main/java/demo/payment/domain/PaymentMethod.java @@ -1,4 +1,4 @@ -package demo.payment; +package demo.payment.domain; public enum PaymentMethod { CREDIT_CARD diff --git a/order/order-worker/src/main/java/demo/payment/PaymentStatus.java b/order/order-worker/src/main/java/demo/payment/domain/PaymentStatus.java similarity index 73% rename from order/order-worker/src/main/java/demo/payment/PaymentStatus.java rename to order/order-worker/src/main/java/demo/payment/domain/PaymentStatus.java index ef13e22..dead7ae 100644 --- a/order/order-worker/src/main/java/demo/payment/PaymentStatus.java +++ b/order/order-worker/src/main/java/demo/payment/domain/PaymentStatus.java @@ -1,7 +1,8 @@ -package demo.payment; +package demo.payment.domain; public enum PaymentStatus { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/order/order-worker/src/main/resources/application.yml b/order/order-worker/src/main/resources/application.yml index 7efcdf6..adf54e9 100644 --- a/order/order-worker/src/main/resources/application.yml +++ b/order/order-worker/src/main/resources/application.yml @@ -13,6 +13,8 @@ spring: contentType: 'application/json' consumer: durableSubscription: true + jackson: + default-property-inclusion: non_null server: port: 0 amazon: @@ -24,4 +26,6 @@ spring: profiles: test eureka: client: - enabled: false \ No newline at end of file + enabled: false +server: + port: 0 \ No newline at end of file diff --git a/order/order-worker/src/main/resources/bootstrap.yml b/order/order-worker/src/main/resources/bootstrap.yml index 7983fac..0ed3294 100644 --- a/order/order-worker/src/main/resources/bootstrap.yml +++ b/order/order-worker/src/main/resources/bootstrap.yml @@ -1,4 +1,4 @@ spring: application: - name: account-worker + name: order-worker --- \ No newline at end of file diff --git a/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java b/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java index 003cb08..56979b4 100644 --- a/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java +++ b/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package demo.config; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; @@ -24,6 +25,7 @@ public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List> converters) { final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); converter.setObjectMapper(objectMapper); converters.add(converter); } diff --git a/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java b/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java index d4d7f57..6660bc4 100644 --- a/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java +++ b/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java @@ -2,6 +2,11 @@ package demo.payment.action; import demo.domain.Action; import demo.payment.domain.Payment; +import demo.payment.domain.PaymentModule; +import demo.payment.domain.PaymentService; +import demo.payment.domain.PaymentStatus; +import demo.payment.event.PaymentEvent; +import demo.payment.event.PaymentEventType; import org.springframework.stereotype.Service; import java.util.function.BiConsumer; @@ -9,6 +14,17 @@ import java.util.function.BiConsumer; @Service public class ConnectOrder extends Action { public BiConsumer getConsumer() { - return Payment::setOrderId; + return (payment, orderId) -> { + PaymentService paymentService = payment.getModule(PaymentModule.class) + .getDefaultService(); + + // Connect the payment to the order + payment.setOrderId(orderId); + payment.setStatus(PaymentStatus.ORDER_CONNECTED); + payment = paymentService.update(payment); + + // Trigger the payment connected + payment.sendAsyncEvent(new PaymentEvent(PaymentEventType.ORDER_CONNECTED, payment)); + }; } } diff --git a/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java b/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java index 283bb8a..b21655e 100644 --- a/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java +++ b/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java @@ -6,17 +6,18 @@ import demo.event.Events; import demo.payment.event.PaymentEvent; import demo.payment.domain.Payment; import demo.payment.domain.PaymentService; -import org.springframework.hateoas.ExposesResourceFor; -import org.springframework.hateoas.LinkBuilder; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceSupport; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.hateoas.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; +import java.util.Random; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @@ -32,10 +33,13 @@ public class PaymentController { private final PaymentService paymentService; private final EventService eventService; + private final DiscoveryClient discoveryClient; - public PaymentController(PaymentService paymentService, EventService eventService) { + public PaymentController(PaymentService paymentService, EventService eventService, + DiscoveryClient discoveryClient) { this.paymentService = paymentService; this.eventService = eventService; + this.discoveryClient = discoveryClient; } @RequestMapping(path = "/payments", method = RequestMethod.POST) @@ -199,16 +203,23 @@ public class PaymentController { private Resource getPaymentResource(Payment payment) { Assert.notNull(payment, "Payment must not be null"); - if(!payment.hasLink("commands")) { + if (!payment.hasLink("commands")) { // Add command link payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands")); } - if(!payment.hasLink("events")) { + if (!payment.hasLink("events")) { // Add get events link payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events")); } + // Add remote payment link + if (payment.getOrderId() != null) { + Link result = getRemoteLink("order-web", "/v1/orders/{id}", payment.getOrderId(), "order "); + if (result != null) + payment.add(result); + } + return new Resource<>(payment); } @@ -217,4 +228,19 @@ public class PaymentController { payment.setIdentity(id); return new Resource<>(payment.getCommands()); } + + private Link getRemoteLink(String service, String relative, Object identifier, String rel) { + Link result = null; + List serviceInstances = discoveryClient.getInstances(service); + if (serviceInstances.size() > 0) { + ServiceInstance serviceInstance = serviceInstances.get(new Random().nextInt(serviceInstances.size())); + result = new Link(new UriTemplate(serviceInstance.getUri() + .toString() + .concat(relative)).with("id", TemplateVariable.VariableType.PATH_VARIABLE) + .expand(identifier) + .toString()) + .withRel(rel); + } + return result; + } } diff --git a/payment/payment-web/src/main/java/demo/payment/domain/Payment.java b/payment/payment-web/src/main/java/demo/payment/domain/Payment.java index dfaad36..6a449c1 100644 --- a/payment/payment-web/src/main/java/demo/payment/domain/Payment.java +++ b/payment/payment-web/src/main/java/demo/payment/domain/Payment.java @@ -1,13 +1,12 @@ package demo.payment.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import demo.domain.AbstractEntity; import demo.domain.Command; -import demo.payment.event.PaymentEvent; import demo.payment.action.ConnectOrder; import demo.payment.action.ProcessPayment; import demo.payment.controller.PaymentController; +import demo.payment.event.PaymentEvent; import org.springframework.hateoas.Link; import javax.persistence.*; @@ -41,6 +40,7 @@ public class Payment extends AbstractEntity { } public Payment(Double amount, PaymentMethod paymentMethod) { + this(); this.amount = amount; this.paymentMethod = paymentMethod; } @@ -80,7 +80,6 @@ public class Payment extends AbstractEntity { this.paymentMethod = paymentMethod; } - @JsonIgnore public Long getOrderId() { return orderId; } diff --git a/payment/payment-web/src/main/java/demo/payment/domain/PaymentService.java b/payment/payment-web/src/main/java/demo/payment/domain/PaymentService.java index e3c61b6..944afff 100644 --- a/payment/payment-web/src/main/java/demo/payment/domain/PaymentService.java +++ b/payment/payment-web/src/main/java/demo/payment/domain/PaymentService.java @@ -19,11 +19,9 @@ import org.springframework.util.Assert; public class PaymentService extends Service { private final PaymentRepository paymentRepository; - private final EventService eventService; public PaymentService(PaymentRepository paymentRepository, EventService eventService) { this.paymentRepository = paymentRepository; - this.eventService = eventService; } public Payment registerPayment(Payment payment) { diff --git a/payment/payment-web/src/main/java/demo/payment/domain/PaymentStatus.java b/payment/payment-web/src/main/java/demo/payment/domain/PaymentStatus.java index abc4aba..e1ecc67 100644 --- a/payment/payment-web/src/main/java/demo/payment/domain/PaymentStatus.java +++ b/payment/payment-web/src/main/java/demo/payment/domain/PaymentStatus.java @@ -10,6 +10,7 @@ import demo.payment.event.PaymentEvent; */ public enum PaymentStatus { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/payment/payment-web/src/main/java/demo/payment/event/PaymentEventType.java b/payment/payment-web/src/main/java/demo/payment/event/PaymentEventType.java index 0b12610..415a97d 100644 --- a/payment/payment-web/src/main/java/demo/payment/event/PaymentEventType.java +++ b/payment/payment-web/src/main/java/demo/payment/event/PaymentEventType.java @@ -11,6 +11,7 @@ import demo.payment.domain.PaymentStatus; */ public enum PaymentEventType { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/payment/payment-web/src/main/resources/application.yml b/payment/payment-web/src/main/resources/application.yml index 00f5e53..16ab89d 100644 --- a/payment/payment-web/src/main/resources/application.yml +++ b/payment/payment-web/src/main/resources/application.yml @@ -10,6 +10,8 @@ spring: output: destination: payment contentType: 'application/json' + jackson: + default-property-inclusion: non_null redis: host: localhost port: 6379 diff --git a/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java b/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java index 8e5eb76..e8aaf8e 100644 --- a/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java +++ b/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java @@ -10,6 +10,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.Assert; @@ -25,6 +27,9 @@ public class EventServiceTests { @Autowired private EventService eventService; + @MockBean + private DiscoveryClient discoveryClient; + @Test public void getPaymentReturnsPayment() throws Exception { Payment payment = new Payment(11.0, PaymentMethod.CREDIT_CARD); diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java index 4962121..80d0428 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java @@ -13,6 +13,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @@ -39,6 +40,9 @@ public class PaymentControllerTest { @MockBean private EventService eventService; + @MockBean + private DiscoveryClient discoveryClient; + @Test public void getUserPaymentResourceShouldReturnPayment() throws Exception { String content = "{\"paymentMethod\": \"CREDIT_CARD\", \"amount\": 42.0 }"; diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java index 0b3cfcc..1c71656 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java @@ -10,6 +10,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @@ -26,6 +27,9 @@ public class PaymentServiceTests { @MockBean private PaymentRepository paymentRepository; + @MockBean + private DiscoveryClient discoveryClient; + private PaymentService paymentService; @Before diff --git a/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java b/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java index ffab680..305cce5 100644 --- a/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java +++ b/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java @@ -75,6 +75,12 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter orderConnected() { + return context -> applyEvent(context, + new OrderConnected(context, event -> { + log.info(event.getType() + ": " + event.getLink("payment").getHref()); + // Get the payment resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("payment").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Payment.class) + .getBody(); + })); + } + @Bean public Action paymentPending() { return context -> applyEvent(context, diff --git a/payment/payment-worker/src/main/java/demo/event/PaymentEventType.java b/payment/payment-worker/src/main/java/demo/event/PaymentEventType.java index 483ea22..45700f8 100644 --- a/payment/payment-worker/src/main/java/demo/event/PaymentEventType.java +++ b/payment/payment-worker/src/main/java/demo/event/PaymentEventType.java @@ -11,6 +11,7 @@ import demo.payment.PaymentStatus; */ public enum PaymentEventType { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/payment/payment-worker/src/main/java/demo/function/OrderConnected.java b/payment/payment-worker/src/main/java/demo/function/OrderConnected.java new file mode 100644 index 0000000..bfbf17f --- /dev/null +++ b/payment/payment-worker/src/main/java/demo/function/OrderConnected.java @@ -0,0 +1,31 @@ +package demo.function; + +import demo.event.PaymentEvent; +import demo.event.PaymentEventType; +import demo.payment.Payment; +import demo.payment.PaymentStatus; +import org.apache.log4j.Logger; +import org.springframework.statemachine.StateContext; + +import java.util.function.Function; + +public class OrderConnected extends PaymentFunction { + + final private Logger log = Logger.getLogger(OrderConnected.class); + + public OrderConnected(StateContext context, Function lambda) { + super(context, lambda); + } + + /** + * Apply an {@link PaymentEvent} to the lambda function that was provided through the + * constructor of this {@link PaymentFunction}. + * + * @param event is the {@link PaymentEvent} to apply to the lambda function + */ + @Override + public Payment apply(PaymentEvent event) { + log.info("Executing workflow for order connected..."); + return super.apply(event); + } +} diff --git a/payment/payment-worker/src/main/java/demo/payment/Payment.java b/payment/payment-worker/src/main/java/demo/payment/Payment.java index 2ecce59..f7d5cf2 100644 --- a/payment/payment-worker/src/main/java/demo/payment/Payment.java +++ b/payment/payment-worker/src/main/java/demo/payment/Payment.java @@ -8,6 +8,7 @@ public class Payment extends BaseEntity { private Double amount; private PaymentMethod paymentMethod; private PaymentStatus status; + private Long orderId; public Payment() { } @@ -44,6 +45,14 @@ public class Payment extends BaseEntity { this.paymentMethod = paymentMethod; } + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + @Override public String toString() { return "Payment{" + diff --git a/payment/payment-worker/src/main/java/demo/payment/PaymentStatus.java b/payment/payment-worker/src/main/java/demo/payment/PaymentStatus.java index eca6bbd..a99f09b 100644 --- a/payment/payment-worker/src/main/java/demo/payment/PaymentStatus.java +++ b/payment/payment-worker/src/main/java/demo/payment/PaymentStatus.java @@ -9,6 +9,7 @@ package demo.payment; */ public enum PaymentStatus { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/payment/payment-worker/src/main/resources/application.yml b/payment/payment-worker/src/main/resources/application.yml index 5f19ca0..a06a9a4 100644 --- a/payment/payment-worker/src/main/resources/application.yml +++ b/payment/payment-worker/src/main/resources/application.yml @@ -13,6 +13,8 @@ spring: contentType: 'application/json' consumer: durableSubscription: true + jackson: + default-property-inclusion: non_null server: port: 0 amazon: diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Aggregate.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Aggregate.java index 2290619..2c9a276 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Aggregate.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Aggregate.java @@ -46,7 +46,7 @@ public abstract class Aggregate extend @JsonIgnore protected , A extends Aggregate> T getAction( Class actionType) throws IllegalArgumentException { - Module provider = getProvider(); + Module provider = getModule(); Service service = provider.getDefaultService(); return (T) service.getAction(actionType); } @@ -59,8 +59,8 @@ public abstract class Aggregate extend */ @SuppressWarnings("unchecked") @JsonIgnore - public , A extends Aggregate> T getProvider() throws IllegalArgumentException { - return getProvider((Class) ResolvableType + public , A extends Aggregate> T getModule() throws IllegalArgumentException { + return getModule((Class) ResolvableType .forClassWithGenerics(Module.class, ResolvableType.forInstance(this)) .getRawClass()); } @@ -72,7 +72,7 @@ public abstract class Aggregate extend * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist */ @JsonIgnore - public , A extends Aggregate> T getProvider(Class providerType) throws + public , A extends Aggregate> T getModule(Class providerType) throws IllegalArgumentException { Assert.notNull(applicationContext, "The application context is unavailable"); T provider = applicationContext.getBean(providerType); @@ -167,13 +167,13 @@ public abstract class Aggregate extend @SuppressWarnings("unchecked") @JsonIgnore protected Service, ID> getEntityService() { - return (Service, ID>) getProvider().getDefaultService(); + return (Service, ID>) getModule().getDefaultService(); } @SuppressWarnings("unchecked") @JsonIgnore protected EventService getEventService() { - return (EventService) getProvider().getDefaultEventService(); + return (EventService) getModule().getDefaultEventService(); } public static class CommandResources extends ResourceSupport { diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/ProviderTests.java b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/ProviderTests.java index 1188cc8..7acdf04 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/ProviderTests.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/ProviderTests.java @@ -42,18 +42,18 @@ public class ProviderTests { @Test public void testGetProviderReturnsProvider() { - assertNotNull(new EmptyAggregate().getProvider(EmptyProvider.class)); + assertNotNull(new EmptyAggregate().getModule(EmptyProvider.class)); } @Test public void testGetServiceReturnsService() { - EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class); + EmptyProvider provider = new EmptyAggregate().getModule(EmptyProvider.class); assertNotNull(provider.getEmptyService()); } @Test public void testGetActionReturnsAction() { - EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class); + EmptyProvider provider = new EmptyAggregate().getModule(EmptyProvider.class); EmptyService service = provider.getEmptyService(); assertNotNull(service.getAction(EmptyAction.class)); } @@ -61,7 +61,7 @@ public class ProviderTests { @Test public void testProcessCommandChangesStatus() { EmptyAggregate aggregate = new EmptyAggregate(0L, AggregateStatus.CREATED); - EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class); + EmptyProvider provider = new EmptyAggregate().getModule(EmptyProvider.class); EmptyService service = provider.getEmptyService(); EmptyAction emptyAction = service.getAction(EmptyAction.class); emptyAction.getConsumer().accept(aggregate); @@ -107,7 +107,7 @@ public class ProviderTests { @Command(controller = EmptyController.class, method = "emptyAction") public void emptyAction() { - EmptyProvider emptyProvider = this.getProvider(); + EmptyProvider emptyProvider = this.getModule(); emptyProvider.getEmptyService() .getAction(EmptyAction.class) .getConsumer()