diff --git a/order/order-web/pom.xml b/order/order-web/pom.xml index f169d55..b2d6d44 100644 --- a/order/order-web/pom.xml +++ b/order/order-web/pom.xml @@ -51,6 +51,11 @@ org.springframework.boot spring-boot-starter-integration + + org.kbastani + spring-boot-starter-data-events + 1.0-SNAPSHOT + com.h2database diff --git a/order/order-web/src/main/java/demo/OrderServiceApplication.java b/order/order-web/src/main/java/demo/OrderServiceApplication.java index bae1da9..e1754ab 100644 --- a/order/order-web/src/main/java/demo/OrderServiceApplication.java +++ b/order/order-web/src/main/java/demo/OrderServiceApplication.java @@ -2,11 +2,13 @@ package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.hateoas.config.EnableHypermediaSupport; @SpringBootApplication +@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) public class OrderServiceApplication { - public static void main(String[] args) { - SpringApplication.run(OrderServiceApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(OrderServiceApplication.class, args); + } } diff --git a/order/order-web/src/main/java/demo/domain/BaseEntity.java b/order/order-web/src/main/java/demo/domain/AbstractEntity.java similarity index 57% rename from order/order-web/src/main/java/demo/domain/BaseEntity.java rename to order/order-web/src/main/java/demo/domain/AbstractEntity.java index fb472ef..ce9652c 100644 --- a/order/order-web/src/main/java/demo/domain/BaseEntity.java +++ b/order/order-web/src/main/java/demo/domain/AbstractEntity.java @@ -1,17 +1,20 @@ package demo.domain; +import demo.event.Event; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.springframework.hateoas.ResourceSupport; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import javax.persistence.*; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class BaseEntity extends ResourceSupport implements Serializable { +public abstract class AbstractEntity extends Aggregate implements Serializable { + + private T identity; @CreatedDate private Long createdAt; @@ -19,7 +22,10 @@ public class BaseEntity extends ResourceSupport implements Serializable { @LastModifiedDate private Long lastModified; - public BaseEntity() { + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List events = new ArrayList<>(); + + public AbstractEntity() { } public Long getCreatedAt() { @@ -38,6 +44,24 @@ public class BaseEntity extends ResourceSupport implements Serializable { this.lastModified = lastModified; } + @Override + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + @Override + public T getIdentity() { + return identity; + } + + public void setIdentity(T id) { + this.identity = id; + } + @Override public String toString() { return "BaseEntity{" + diff --git a/order/order-web/src/main/java/demo/event/ConsistencyModel.java b/order/order-web/src/main/java/demo/event/ConsistencyModel.java deleted file mode 100644 index 8bef081..0000000 --- a/order/order-web/src/main/java/demo/event/ConsistencyModel.java +++ /dev/null @@ -1,6 +0,0 @@ -package demo.event; - -public enum ConsistencyModel { - BASE, - ACID -} diff --git a/order/order-web/src/main/java/demo/event/EventController.java b/order/order-web/src/main/java/demo/event/EventController.java deleted file mode 100644 index ffcd8da..0000000 --- a/order/order-web/src/main/java/demo/event/EventController.java +++ /dev/null @@ -1,39 +0,0 @@ -package demo.event; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Optional; - -@RestController -@RequestMapping("/v1") -public class EventController { - - private final EventService eventService; - - public EventController(EventService eventService) { - this.eventService = eventService; - } - - @PostMapping(path = "/events/{id}") - public ResponseEntity createEvent(@RequestBody OrderEvent event, @PathVariable Long id) { - return Optional.ofNullable(eventService.createEvent(id, event, ConsistencyModel.ACID)) - .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) - .orElseThrow(() -> new IllegalArgumentException("Event creation failed")); - } - - @PutMapping(path = "/events/{id}") - public ResponseEntity updateEvent(@RequestBody OrderEvent event, @PathVariable Long id) { - return Optional.ofNullable(eventService.updateEvent(id, event)) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("Event update failed")); - } - - @GetMapping(path = "/events/{id}") - public ResponseEntity getEvent(@PathVariable Long id) { - return Optional.ofNullable(eventService.getEvent(id)) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } -} diff --git a/order/order-web/src/main/java/demo/event/EventRepository.java b/order/order-web/src/main/java/demo/event/EventRepository.java deleted file mode 100644 index a17083d..0000000 --- a/order/order-web/src/main/java/demo/event/EventRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package demo.event; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.query.Param; - -public interface EventRepository extends JpaRepository { - Page findOrderEventsByOrderId(@Param("orderId") Long orderId, Pageable pageable); -} diff --git a/order/order-web/src/main/java/demo/event/EventService.java b/order/order-web/src/main/java/demo/event/EventService.java deleted file mode 100644 index 09f0e92..0000000 --- a/order/order-web/src/main/java/demo/event/EventService.java +++ /dev/null @@ -1,219 +0,0 @@ -package demo.event; - -import demo.order.Order; -import demo.order.OrderController; -import org.apache.log4j.Logger; -import org.springframework.cache.annotation.CacheConfig; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cloud.stream.messaging.Source; -import org.springframework.data.domain.PageRequest; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.Resource; -import org.springframework.http.RequestEntity; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -/** - * The {@link EventService} provides transactional service methods for {@link OrderEvent} - * entities of the Order Service. Order domain events are generated with a {@link OrderEventType}, - * and action logs are appended to the {@link OrderEvent}. - * - * @author kbastani - */ -@Service -@CacheConfig(cacheNames = {"order-events"}) -public class EventService { - - private final Logger log = Logger.getLogger(EventService.class); - - private final EventRepository eventRepository; - private final Source orderStreamSource; - private final RestTemplate restTemplate; - - public EventService(EventRepository eventRepository, Source orderStreamSource, RestTemplate restTemplate) { - this.eventRepository = eventRepository; - this.orderStreamSource = orderStreamSource; - this.restTemplate = restTemplate; - } - - /** - * Create a new {@link OrderEvent} and append it to the event log of the referenced {@link Order}. - * After the {@link OrderEvent} has been persisted, send the event to the order stream. Events can - * be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}. - * - * @param orderId is the unique identifier for the {@link Order} - * @param event is the {@link OrderEvent} to create - * @param consistencyModel is the desired consistency model for the response - * @return an {@link OrderEvent} that has been appended to the {@link Order}'s event log - */ - public OrderEvent createEvent(Long orderId, OrderEvent event, ConsistencyModel consistencyModel) { - event = createEvent(orderId, event); - return raiseEvent(event, consistencyModel); - } - - /** - * Raise an {@link OrderEvent} that attempts to transition the state of an {@link Order}. - * - * @param event is an {@link OrderEvent} that will be raised - * @param consistencyModel is the consistency model for this request - * @return an {@link OrderEvent} that has been appended to the {@link Order}'s event log - */ - public OrderEvent raiseEvent(OrderEvent event, ConsistencyModel consistencyModel, Link... links) { - // Add embedded links - event.add(links); - - switch (consistencyModel) { - case BASE: - asyncRaiseEvent(event); - break; - case ACID: - event = raiseEvent(event); - break; - } - - return event; - } - - /** - * Raise an asynchronous {@link OrderEvent} by sending an AMQP message to the order stream. Any - * state changes will be applied to the {@link Order} outside of the current HTTP request context. - *

- * Use this operation when a workflow can be processed asynchronously outside of the current HTTP - * request context. - * - * @param event is an {@link OrderEvent} that will be raised - */ - private void asyncRaiseEvent(OrderEvent event) { - // Append the order event to the stream - orderStreamSource.output() - .send(MessageBuilder - .withPayload(getOrderEventResource(event)) - .build()); - } - - /** - * Raise a synchronous {@link OrderEvent} by sending a HTTP request to the order stream. The response - * is a blocking operation, which ensures that the result of a multi-step workflow will not return until - * the transaction reaches a consistent state. - *

- * Use this operation when the result of a workflow must be returned within the current HTTP request context. - * - * @param event is an {@link OrderEvent} that will be raised - * @return an {@link OrderEvent} which contains the consistent state of an {@link Order} - */ - private OrderEvent raiseEvent(OrderEvent event) { - try { - // Create a new request entity - RequestEntity> requestEntity = RequestEntity.post( - URI.create("http://localhost:8081/v1/events")) - .contentType(MediaTypes.HAL_JSON) - .body(getOrderEventResource(event), Resource.class); - - // Update the order entity's status - Order result = restTemplate.exchange(requestEntity, Order.class) - .getBody(); - - log.info(result); - event.setOrder(result); - } catch (Exception ex) { - log.error(ex); - } - - return event; - } - - - /** - * Create a new {@link OrderEvent} and publish it to the order stream. - * - * @param event is the {@link OrderEvent} to publish to the order stream - * @return a hypermedia {@link OrderEvent} resource - */ - @CacheEvict(cacheNames = "order-events", key = "#id.toString()") - public OrderEvent createEvent(Long id, OrderEvent event) { - // Save new event - event = addEvent(event); - Assert.notNull(event, "The event could not be appended to the order"); - - return event; - } - - /** - * Get an {@link OrderEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link OrderEvent} - * @return an {@link OrderEvent} - */ - public Resource getEvent(Long id) { - return getOrderEventResource(eventRepository.findOne(id)); - } - - /** - * Update an {@link OrderEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link OrderEvent} - * @param event is the {@link OrderEvent} to update - * @return the updated {@link OrderEvent} - */ - @CacheEvict(cacheNames = "order-events", key = "#event.order().getOrderId().toString()") - public OrderEvent updateEvent(Long id, OrderEvent event) { - Assert.notNull(id); - Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId())); - - return eventRepository.save(event); - } - - /** - * Get {@link OrderEvents} for the supplied {@link Order} identifier. - * - * @param id is the unique identifier of the {@link Order} - * @return a list of {@link OrderEvent} wrapped in a hypermedia {@link OrderEvents} resource - */ - @Cacheable(cacheNames = "order-events", key = "#id.toString()") - public List getOrderEvents(Long id) { - return eventRepository.findOrderEventsByOrderId(id, - new PageRequest(0, Integer.MAX_VALUE)).getContent(); - } - - /** - * Gets a hypermedia resource for a {@link OrderEvent} entity. - * - * @param event is the {@link OrderEvent} to enrich with hypermedia - * @return a hypermedia resource for the supplied {@link OrderEvent} entity - */ - private Resource getOrderEventResource(OrderEvent event) { - event.add(Arrays.asList( - linkTo(OrderController.class) - .slash("events") - .slash(event.getEventId()) - .withSelfRel(), - linkTo(OrderController.class) - .slash("orders") - .slash(event.getOrder().getOrderId()) - .withRel("order"))); - return new Resource(event, event.getLinks()); - } - - /** - * Add a {@link OrderEvent} to an {@link Order} entity. - * - * @param event is the {@link OrderEvent} to append to an {@link Order} entity - * @return the newly appended {@link OrderEvent} entity - */ - @CacheEvict(cacheNames = "order-events", key = "#event.order().getOrderId().toString()") - private OrderEvent addEvent(OrderEvent event) { - event = eventRepository.saveAndFlush(event); - return event; - } -} diff --git a/order/order-web/src/main/java/demo/event/OrderEvent.java b/order/order-web/src/main/java/demo/event/OrderEvent.java index c494449..d2ae60e 100644 --- a/order/order-web/src/main/java/demo/event/OrderEvent.java +++ b/order/order-web/src/main/java/demo/event/OrderEvent.java @@ -1,34 +1,41 @@ package demo.event; import com.fasterxml.jackson.annotation.JsonIgnore; -import demo.domain.BaseEntity; import demo.order.Order; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; /** - * The domain event {@link OrderEvent} tracks the type and state of events as - * applied to the {@link Order} domain object. This event resource can be used - * to event source the aggregate state of {@link Order}. + * The domain event {@link OrderEvent} tracks the type and state of events as applied to the {@link Order} domain + * object. This event resource can be used to event source the aggregate state of {@link Order}. *

- * This event resource also provides a transaction log that can be used to append - * actions to the event. + * This event resource also provides a transaction log that can be used to append actions to the event. * - * @author kbastani + * @author Kenny Bastani */ @Entity -public class OrderEvent extends BaseEntity { +@EntityListeners(AuditingEntityListener.class) +public class OrderEvent extends Event { @Id - @GeneratedValue - private Long id; + @GeneratedValue(strategy = GenerationType.AUTO) + private Long eventId; @Enumerated(EnumType.STRING) private OrderEventType type; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JsonIgnore - private Order order; + private Order entity; + + @CreatedDate + private Long createdAt; + + @LastModifiedDate + private Long lastModified; public OrderEvent() { } @@ -37,37 +44,69 @@ public class OrderEvent extends BaseEntity { this.type = type; } - @JsonIgnore + public OrderEvent(OrderEventType type, Order entity) { + this.type = type; + this.entity = entity; + } + + @Override public Long getEventId() { - return id; + return eventId; } + @Override public void setEventId(Long id) { - this.id = id; + eventId = id; } + @Override public OrderEventType getType() { return type; } + @Override public void setType(OrderEventType type) { this.type = type; } - public Order getOrder() { - return order; + @Override + public Order getEntity() { + return entity; } - public void setOrder(Order order) { - this.order = order; + @Override + public void setEntity(Order entity) { + this.entity = entity; + } + + @Override + public Long getCreatedAt() { + return createdAt; + } + + @Override + public void setCreatedAt(Long createdAt) { + this.createdAt = createdAt; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + @Override + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; } @Override public String toString() { return "OrderEvent{" + - "id=" + id + + "eventId=" + eventId + ", type=" + type + - ", order=" + order + + ", entity=" + entity + + ", createdAt=" + createdAt + + ", lastModified=" + lastModified + "} " + super.toString(); } } diff --git a/order/order-web/src/main/java/demo/event/OrderEventRepository.java b/order/order-web/src/main/java/demo/event/OrderEventRepository.java new file mode 100644 index 0000000..7b0ac30 --- /dev/null +++ b/order/order-web/src/main/java/demo/event/OrderEventRepository.java @@ -0,0 +1,4 @@ +package demo.event; + +public interface OrderEventRepository extends EventRepository { +} diff --git a/order/order-web/src/main/java/demo/event/OrderEventType.java b/order/order-web/src/main/java/demo/event/OrderEventType.java index 38e79b5..23a028c 100644 --- a/order/order-web/src/main/java/demo/event/OrderEventType.java +++ b/order/order-web/src/main/java/demo/event/OrderEventType.java @@ -4,10 +4,10 @@ import demo.order.Order; import demo.order.OrderStatus; /** - * The {@link OrderEventType} represents a collection of possible events that describe - * state transitions of {@link OrderStatus} on the {@link Order} aggregate. + * The {@link OrderEventType} represents a collection of possible events that describe state transitions of + * {@link OrderStatus} on the {@link Order} aggregate. * - * @author kbastani + * @author Kenny Bastani */ public enum OrderEventType { ORDER_CREATED, diff --git a/order/order-web/src/main/java/demo/event/OrderEvents.java b/order/order-web/src/main/java/demo/event/OrderEvents.java deleted file mode 100644 index 8eb0356..0000000 --- a/order/order-web/src/main/java/demo/event/OrderEvents.java +++ /dev/null @@ -1,74 +0,0 @@ -package demo.event; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import demo.order.Order; -import demo.order.OrderController; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.LinkBuilder; -import org.springframework.hateoas.Resources; - -import java.io.Serializable; -import java.util.List; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -/** - * The {@link OrderEvents} is a hypermedia collection of {@link OrderEvent} resources. - * - * @author kbastani - */ -public class OrderEvents extends Resources implements Serializable { - - private Long orderId; - - /** - * Create a new {@link OrderEvents} hypermedia resources collection for an {@link Order}. - * - * @param orderId is the unique identifier for the {@link Order} - * @param content is the collection of {@link OrderEvents} attached to the {@link Order} - */ - public OrderEvents(Long orderId, List content) { - this(content); - this.orderId = orderId; - - // Add hypermedia links to resources parent - add(linkTo(OrderController.class) - .slash("orders") - .slash(orderId) - .slash("events") - .withSelfRel(), - linkTo(OrderController.class) - .slash("orders") - .slash(orderId) - .withRel("order")); - - LinkBuilder linkBuilder = linkTo(EventController.class); - - // Add hypermedia links to each item of the collection - content.stream().parallel().forEach(event -> event.add( - linkBuilder.slash("events") - .slash(event.getEventId()) - .withSelfRel() - )); - } - - /** - * Creates a {@link Resources} instance with the given content and {@link Link}s (optional). - * - * @param content must not be {@literal null}. - * @param links the links to be added to the {@link Resources}. - */ - private OrderEvents(Iterable content, Link... links) { - super(content, links); - } - - /** - * Get the {@link Order} identifier that the {@link OrderEvents} apply to. - * - * @return the order identifier - */ - @JsonIgnore - public Long getOrderId() { - return orderId; - } -} diff --git a/order/order-web/src/main/java/demo/order/LineItem.java b/order/order-web/src/main/java/demo/order/LineItem.java index 2d3f2d0..58d22c9 100644 --- a/order/order-web/src/main/java/demo/order/LineItem.java +++ b/order/order-web/src/main/java/demo/order/LineItem.java @@ -1,15 +1,15 @@ package demo.order; import com.fasterxml.jackson.annotation.JsonIgnore; +import demo.domain.Value; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import java.io.Serializable; @Entity -public class LineItem implements Serializable { +public class LineItem implements Value { @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/order/order-web/src/main/java/demo/order/Order.java b/order/order-web/src/main/java/demo/order/Order.java index c5398c8..410a2d1 100644 --- a/order/order-web/src/main/java/demo/order/Order.java +++ b/order/order-web/src/main/java/demo/order/Order.java @@ -1,26 +1,26 @@ package demo.order; -import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import demo.address.Address; import demo.address.AddressType; -import demo.domain.BaseEntity; +import demo.domain.AbstractEntity; +import demo.domain.Command; import demo.event.OrderEvent; +import demo.order.action.*; +import demo.order.controller.OrderController; +import org.springframework.hateoas.Link; import javax.persistence.*; import java.util.HashSet; import java.util.Set; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + @Entity(name = "orders") -public class Order extends BaseEntity { - +public class Order extends AbstractEntity { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue private Long id; - private Long accountId; - private Long paymentId; - - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Set events = new HashSet<>(); @Enumerated(value = EnumType.STRING) private OrderStatus status; @@ -31,6 +31,8 @@ public class Order extends BaseEntity { @OneToOne(cascade = CascadeType.ALL) private Address shippingAddress; + private Long accountId, paymentId; + public Order() { this.status = OrderStatus.ORDER_CREATED; } @@ -43,33 +45,17 @@ public class Order extends BaseEntity { this.shippingAddress.setAddressType(AddressType.SHIPPING); } - @JsonIgnore - public Long getOrderId() { - return id; + @JsonProperty("orderId") + @Override + public Long getIdentity() { + return this.id; } - public void setOrderId(Long id) { + @Override + public void setIdentity(Long id) { this.id = id; } - @JsonIgnore - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - @JsonIgnore - public Long getPaymentId() { - return paymentId; - } - - public void setPaymentId(Long paymentId) { - this.paymentId = paymentId; - } - public OrderStatus getStatus() { return status; } @@ -78,15 +64,6 @@ public class Order extends BaseEntity { this.status = status; } - @JsonIgnore - public Set getEvents() { - return events; - } - - public void setEvents(Set events) { - this.events = events; - } - public Set getLineItems() { return lineItems; } @@ -103,19 +80,70 @@ public class Order extends BaseEntity { this.shippingAddress = shippingAddress; } - public void addLineItem(LineItem lineItem) { - lineItems.add(lineItem); + public Long getAccountId() { + return accountId; } + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getPaymentId() { + return paymentId; + } + + public void setPaymentId(Long paymentId) { + this.paymentId = paymentId; + } + + @Command(method = "connectAccount", controller = OrderController.class) + public Order connectAccount(Long accountId) { + getAction(ConnectAccount.class) + .getConsumer() + .accept(this, accountId); + return this; + } + + @Command(method = "connectPayment", controller = OrderController.class) + public Order connectPayment(Long paymentId) { + getAction(ConnectPayment.class) + .getConsumer() + .accept(this, paymentId); + return this; + } + + @Command(method = "createPayment", controller = OrderController.class) + public Order createPayment() { + getAction(CreatePayment.class) + .getConsumer() + .accept(this); + return this; + } + + @Command(method = "processPayment", controller = OrderController.class) + public Order processPayment() { + getAction(ProcessPayment.class) + .getConsumer() + .accept(this); + return this; + } + + @Command(method = "reserveInventory", controller = OrderController.class) + public Order reserveInventory(Long paymentId) { + getAction(ReserveInventory.class) + .getConsumer() + .accept(this); + return this; + } + + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ @Override - public String toString() { - return "Order{" + - "id=" + id + - ", accountId=" + accountId + - ", paymentId=" + paymentId + - ", status=" + status + - ", lineItems=" + lineItems + - ", shippingAddress=" + shippingAddress + - "} " + super.toString(); + public Link getId() { + return linkTo(OrderController.class) + .slash("orders") + .slash(getIdentity()) + .withSelfRel(); } } diff --git a/order/order-web/src/main/java/demo/order/OrderCommand.java b/order/order-web/src/main/java/demo/order/OrderCommand.java deleted file mode 100644 index 17ea5ce..0000000 --- a/order/order-web/src/main/java/demo/order/OrderCommand.java +++ /dev/null @@ -1,9 +0,0 @@ -package demo.order; - -public enum OrderCommand { - CONNECT_ACCOUNT, - RESERVE_INVENTORY, - CREATE_PAYMENT, - CONNECT_PAYMENT, - PROCESS_PAYMENT -} diff --git a/order/order-web/src/main/java/demo/order/OrderCommands.java b/order/order-web/src/main/java/demo/order/OrderCommands.java deleted file mode 100644 index c491ac1..0000000 --- a/order/order-web/src/main/java/demo/order/OrderCommands.java +++ /dev/null @@ -1,12 +0,0 @@ -package demo.order; - -import org.springframework.hateoas.ResourceSupport; - -/** - * A hypermedia resource that describes the collection of commands that - * can be applied to a {@link Order} aggregate. - * - * @author kbastani - */ -public class OrderCommands extends ResourceSupport { -} diff --git a/order/order-web/src/main/java/demo/order/OrderProvider.java b/order/order-web/src/main/java/demo/order/OrderProvider.java new file mode 100644 index 0000000..1c5b1aa --- /dev/null +++ b/order/order-web/src/main/java/demo/order/OrderProvider.java @@ -0,0 +1,35 @@ +package demo.order; + +import demo.domain.Provider; +import demo.event.EventService; +import demo.event.OrderEvent; + +@org.springframework.stereotype.Service +public class OrderProvider extends Provider { + + private final OrderService orderService; + private final EventService eventService; + + public OrderProvider(OrderService orderService, EventService eventService) { + this.orderService = orderService; + this.eventService = eventService; + } + + public OrderService getOrderService() { + return orderService; + } + + public EventService getEventService() { + return eventService; + } + + @Override + public OrderService getDefaultService() { + return orderService; + } + + @Override + public EventService getDefaultEventService() { + return eventService; + } +} diff --git a/order/order-web/src/main/java/demo/order/OrderService.java b/order/order-web/src/main/java/demo/order/OrderService.java index 551fc76..b931bfa 100644 --- a/order/order-web/src/main/java/demo/order/OrderService.java +++ b/order/order-web/src/main/java/demo/order/OrderService.java @@ -1,60 +1,27 @@ package demo.order; -import demo.event.ConsistencyModel; -import demo.event.EventService; +import demo.domain.Service; import demo.event.OrderEvent; import demo.event.OrderEventType; -import demo.payment.Payment; -import demo.payment.PaymentMethod; -import org.apache.log4j.Logger; -import org.springframework.cache.annotation.CacheConfig; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.Resource; -import org.springframework.http.RequestEntity; -import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; - -@Service -@CacheConfig(cacheNames = {"orders"}) -public class OrderService { - - private final Logger log = Logger.getLogger(OrderService.class); +@org.springframework.stereotype.Service +public class OrderService extends Service { private final OrderRepository orderRepository; - private final EventService eventService; - private final RestTemplate restTemplate; - public OrderService(OrderRepository orderRepository, EventService eventService, RestTemplate restTemplate) { + public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; - this.eventService = eventService; - this.restTemplate = restTemplate; } - @CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()") public Order registerOrder(Order order) { - order = createOrder(order); - - //cacheManager.getCache("orders").evict(order.getOrderId()); + order = create(order); // Trigger the order creation event - OrderEvent event = appendEvent(order.getOrderId(), - new OrderEvent(OrderEventType.ORDER_CREATED)); + order.sendAsyncEvent(new OrderEvent(OrderEventType.ORDER_CREATED, order)); - // Attach order identifier - event.getOrder().setOrderId(order.getOrderId()); - - // Return the result - return event.getOrder(); + return order; } /** @@ -63,8 +30,7 @@ public class OrderService { * @param order is the {@link Order} to create * @return the newly created {@link Order} */ - @CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()") - public Order createOrder(Order order) { + public Order create(Order order) { // Save the order to the repository order = orderRepository.saveAndFlush(order); @@ -78,34 +44,24 @@ public class OrderService { * @param id is the unique identifier of a {@link Order} entity * @return an {@link Order} entity */ - @Cacheable(cacheNames = "orders", key = "#id.toString()") - public Order getOrder(Long id) { + public Order get(Long id) { return orderRepository.findOne(id); } /** * Update an {@link Order} entity with the supplied identifier. * - * @param id is the unique identifier of the {@link Order} entity * @param order is the {@link Order} containing updated fields * @return the updated {@link Order} entity */ - @CachePut(cacheNames = "orders", key = "#id.toString()") - public Order updateOrder(Long id, Order order) { - Assert.notNull(id, "Order id must be present in the resource URL"); + public Order update(Order order) { + Assert.notNull(order.getIdentity(), "Order id must be present in the resource URL"); Assert.notNull(order, "Order request body cannot be null"); - if (order.getOrderId() != null) { - Assert.isTrue(Objects.equals(id, order.getOrderId()), - "The order id in the request body must match the resource URL"); - } else { - order.setOrderId(id); - } - - Assert.state(orderRepository.exists(id), + Assert.state(orderRepository.exists(order.getIdentity()), "The order with the supplied id does not exist"); - Order currentOrder = orderRepository.findOne(id); + Order currentOrder = get(order.getIdentity()); currentOrder.setAccountId(order.getAccountId()); currentOrder.setPaymentId(order.getPaymentId()); currentOrder.setLineItems(order.getLineItems()); @@ -120,174 +76,10 @@ public class OrderService { * * @param id is the unique identifier for the {@link Order} */ - @CacheEvict(cacheNames = "orders", key = "#id.toString()") - public Boolean deleteOrder(Long id) { + public boolean delete(Long id) { Assert.state(orderRepository.exists(id), "The order with the supplied id does not exist"); this.orderRepository.delete(id); return true; } - - /** - * Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier. - * - * @param orderId is the unique identifier for the {@link Order} - * @param event is the {@link OrderEvent} to append to the {@link Order} entity - * @param links is the optional {@link Link} to embed in the {@link org.springframework.hateoas.Resource} - * @return the newly appended {@link OrderEvent} - */ - public OrderEvent appendEvent(Long orderId, OrderEvent event, Link... links) { - return appendEvent(orderId, event, ConsistencyModel.ACID, links); - } - - /** - * Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier. - * - * @param orderId is the unique identifier for the {@link Order} - * @param event is the {@link OrderEvent} to append to the {@link Order} entity - * @return the newly appended {@link OrderEvent} - */ - public OrderEvent appendEvent(Long orderId, OrderEvent event) { - return appendEvent(orderId, event, ConsistencyModel.ACID); - } - - /** - * Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier. - * - * @param orderId is the unique identifier for the {@link Order} - * @param event is the {@link OrderEvent} to append to the {@link Order} entity - * @return the newly appended {@link OrderEvent} - */ - public OrderEvent appendEvent(Long orderId, OrderEvent event, ConsistencyModel consistencyModel, Link... links) { - Order order = getOrder(orderId); - Assert.notNull(order, "The order with the supplied id does not exist"); - event.setOrder(order); - event = eventService.createEvent(orderId, event); - order.getEvents().add(event); - order = orderRepository.saveAndFlush(order); - event.setOrder(order); - eventService.raiseEvent(event, consistencyModel, links); - return event; - } - - /** - * Apply an {@link OrderCommand} to the {@link Order} with a specified identifier. - * - * @param id is the unique identifier of the {@link Order} - * @param orderCommand is the command to apply to the {@link Order} - * @return a hypermedia resource containing the updated {@link Order} - */ - @CachePut(cacheNames = "orders", key = "#id.toString()") - public Order applyCommand(Long id, OrderCommand orderCommand) { - Order order = getOrder(id); - - Assert.notNull(order, "The order for the supplied id could not be found"); - - OrderStatus status = order.getStatus(); - - // TODO: Implement apply command - - return order; - } - - public Order connectAccount(Long id, Long accountId) { - // Get the order - Order order = getOrder(id); - - // Connect the account - order.setAccountId(accountId); - order.setStatus(OrderStatus.ACCOUNT_CONNECTED); - order = updateOrder(id, order); - - //cacheManager.getCache("orders").evict(id); - - // Trigger the account connected event - OrderEvent event = appendEvent(order.getOrderId(), - new OrderEvent(OrderEventType.ACCOUNT_CONNECTED)); - - // Set non-serializable fields - event.getOrder().setAccountId(order.getAccountId()); - event.getOrder().setPaymentId(order.getPaymentId()); - event.getOrder().setOrderId(order.getOrderId()); - - // Return the result - return event.getOrder(); - } - - public Order connectPayment(Long id, Long paymentId) { - // Get the order - Order order = getOrder(id); - - // Connect the account - order.setPaymentId(paymentId); - order.setStatus(OrderStatus.PAYMENT_CONNECTED); - order = updateOrder(id, order); - - // cacheManager.getCache("orders").evict(id); - - // Trigger the account connected event - OrderEvent event = appendEvent(order.getOrderId(), - new OrderEvent(OrderEventType.PAYMENT_CONNECTED)); - - // Set non-serializable fields - event.getOrder().setAccountId(order.getAccountId()); - event.getOrder().setPaymentId(order.getPaymentId()); - event.getOrder().setOrderId(order.getOrderId()); - - // Return the result - return event.getOrder(); - } - - public Order createPayment(Long id) { - // Get the order - Order order = getOrder(id); - - Payment payment = new Payment(); - - // Calculate payment amount - payment.setAmount(order.getLineItems() - .stream() - .mapToDouble(a -> (a.getPrice() + a.getTax()) * a.getQuantity()) - .sum()); - - // Set payment method - payment.setPaymentMethod(PaymentMethod.CREDIT_CARD); - - // Create a new request entity - RequestEntity> requestEntity = RequestEntity.post( - URI.create("http://localhost:8082/v1/payments")) - .contentType(MediaTypes.HAL_JSON) - .body(new Resource(payment), Resource.class); - - // Update the order entity's status - payment = restTemplate.exchange(requestEntity, Payment.class) - .getBody(); - - log.info(payment); - - // Update the status - order.setStatus(OrderStatus.PAYMENT_CREATED); - order = updateOrder(id, order); - - // cacheManager.getCache("orders").evict(id); - - // Trigger the account connected event - OrderEvent event = appendEvent(order.getOrderId(), - new OrderEvent(OrderEventType.PAYMENT_CREATED), - new Link(payment.getId().getHref(), "payment")); - - // Set non-serializable fields - event.getOrder() - .setAccountId(Optional.ofNullable(event.getOrder().getAccountId()) - .orElse(order.getAccountId())); - - event.getOrder() - .setPaymentId(Optional.ofNullable(event.getOrder().getPaymentId()) - .orElse(order.getPaymentId())); - - event.getOrder().setOrderId(order.getOrderId()); - - // Return the result - return event.getOrder(); - } } 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 new file mode 100644 index 0000000..a504e6e --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java @@ -0,0 +1,31 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.event.OrderEvent; +import demo.event.OrderEventType; +import demo.order.Order; +import demo.order.OrderProvider; +import demo.order.OrderService; +import demo.order.OrderStatus; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +@Service +public class ConnectAccount extends Action { + + public BiConsumer getConsumer() { + return (order, accountId) -> { + OrderService orderService = order.getProvider(OrderProvider.class) + .getDefaultService(); + + // Connect the account + order.setAccountId(accountId); + order.setStatus(OrderStatus.ACCOUNT_CONNECTED); + order = orderService.update(order); + + // Trigger the account connected event + order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED)); + }; + } +} 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 new file mode 100644 index 0000000..ef830d5 --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java @@ -0,0 +1,31 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.event.OrderEvent; +import demo.event.OrderEventType; +import demo.order.Order; +import demo.order.OrderProvider; +import demo.order.OrderService; +import demo.order.OrderStatus; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +@Service +public class ConnectPayment extends Action { + public BiConsumer getConsumer() { + return (order, paymentId) -> { + + OrderService orderService = order.getProvider(OrderProvider.class) + .getDefaultService(); + + // Connect the account + order.setPaymentId(paymentId); + order.setStatus(OrderStatus.PAYMENT_CONNECTED); + order = orderService.update(order); + + // Trigger the account connected event + order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED)); + }; + } +} 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 new file mode 100644 index 0000000..ddcd71f --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/CreatePayment.java @@ -0,0 +1,77 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.event.OrderEvent; +import demo.event.OrderEventType; +import demo.order.Order; +import demo.order.OrderProvider; +import demo.order.OrderService; +import demo.order.OrderStatus; +import demo.payment.Payment; +import demo.payment.PaymentMethod; +import org.apache.log4j.Logger; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.function.Consumer; + +@Service +public class CreatePayment extends Action { + + private final Logger log = Logger.getLogger(CreatePayment.class); + + private RestTemplate restTemplate; + + public CreatePayment(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public Consumer getConsumer() { + return order -> { + + OrderService orderService = (OrderService) order.getProvider(OrderProvider.class) + .getDefaultService(); + + Payment payment = new Payment(); + + // Calculate payment amount + payment.setAmount(order.getLineItems() + .stream() + .mapToDouble(a -> (a.getPrice() + a.getTax()) * a.getQuantity()) + .sum()); + + // Set payment method + payment.setPaymentMethod(PaymentMethod.CREDIT_CARD); + + // Create a new request entity + RequestEntity> requestEntity = RequestEntity.post( + URI.create("http://localhost:8082/v1/payments")) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaTypes.HAL_JSON) + .body(new Resource<>(payment), Resource.class); + + // Update the order entity's status + Resource paymentResource = restTemplate + .exchange(requestEntity, Resource.class) + .getBody(); + + log.info(paymentResource); + + // Update the status + order.setStatus(OrderStatus.PAYMENT_CREATED); + order = orderService.update(order); + + OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_CREATED, order); + event.add(paymentResource.getLink("self") + .withRel("payment")); + + // Trigger the payment created + order.sendAsyncEvent(event); + }; + } +} diff --git a/order/order-web/src/main/java/demo/order/action/ProcessPayment.java b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java new file mode 100644 index 0000000..faa837e --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java @@ -0,0 +1,14 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.order.Order; +import org.springframework.stereotype.Service; + +import java.util.function.Consumer; + +@Service +public class ProcessPayment extends Action { + public Consumer getConsumer() { + return (order) -> {}; + } +} diff --git a/order/order-web/src/main/java/demo/order/action/ReserveInventory.java b/order/order-web/src/main/java/demo/order/action/ReserveInventory.java new file mode 100644 index 0000000..5a7d434 --- /dev/null +++ b/order/order-web/src/main/java/demo/order/action/ReserveInventory.java @@ -0,0 +1,14 @@ +package demo.order.action; + +import demo.domain.Action; +import demo.order.Order; +import org.springframework.stereotype.Service; + +import java.util.function.Consumer; + +@Service +public class ReserveInventory extends Action { + public Consumer getConsumer() { + return (order) -> {}; + } +} diff --git a/order/order-web/src/main/java/demo/order/OrderController.java b/order/order-web/src/main/java/demo/order/controller/OrderController.java similarity index 50% rename from order/order-web/src/main/java/demo/order/OrderController.java rename to order/order-web/src/main/java/demo/order/controller/OrderController.java index b7140ea..5ce4421 100644 --- a/order/order-web/src/main/java/demo/order/OrderController.java +++ b/order/order-web/src/main/java/demo/order/controller/OrderController.java @@ -1,15 +1,21 @@ -package demo.order; +package demo.order.controller; import demo.event.EventController; import demo.event.EventService; +import demo.event.Events; import demo.event.OrderEvent; -import demo.event.OrderEvents; -import org.springframework.hateoas.*; +import demo.order.Order; +import demo.order.OrderService; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceSupport; 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.Optional; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @@ -19,9 +25,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; public class OrderController { private final OrderService orderService; - private final EventService eventService; + private final EventService eventService; - public OrderController(OrderService orderService, EventService eventService) { + public OrderController(OrderService orderService, EventService eventService) { this.orderService = orderService; this.eventService = eventService; } @@ -40,7 +46,7 @@ public class OrderController { .orElseThrow(() -> new RuntimeException("Order update failed")); } - @GetMapping(path = "/orders/{id}") + @RequestMapping(path = "/orders/{id}") public ResponseEntity getOrder(@PathVariable Long id) { return Optional.ofNullable(getOrderResource(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) @@ -49,12 +55,12 @@ public class OrderController { @DeleteMapping(path = "/orders/{id}") public ResponseEntity deleteOrder(@PathVariable Long id) { - return Optional.ofNullable(orderService.deleteOrder(id)) + return Optional.ofNullable(orderService.delete(id)) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) .orElseThrow(() -> new RuntimeException("Order deletion failed")); } - @GetMapping(path = "/orders/{id}/events") + @RequestMapping(path = "/orders/{id}/events") public ResponseEntity getOrderEvents(@PathVariable Long id) { return Optional.of(getOrderEventResources(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) @@ -68,48 +74,50 @@ public class OrderController { .orElseThrow(() -> new RuntimeException("Append order event failed")); } - @GetMapping(path = "/orders/{id}/commands") - public ResponseEntity getOrderCommands(@PathVariable Long id) { - return Optional.ofNullable(getCommandsResource(id)) + @RequestMapping(path = "/orders/{id}/commands") + public ResponseEntity getCommands(@PathVariable Long id) { + return Optional.ofNullable(getCommandsResources(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The order could not be found")); } - @GetMapping(path = "/orders/{id}/commands/connectAccount") + @RequestMapping(path = "/orders/{id}/commands/connectAccount") public ResponseEntity connectAccount(@PathVariable Long id, @RequestParam(value = "accountId") Long accountId) { - return Optional.ofNullable(getOrderResource(orderService.connectAccount(id, accountId))) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(orderService.get(id) + .connectAccount(accountId)) + .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/orders/{id}/commands/connectPayment") + @RequestMapping(path = "/orders/{id}/commands/connectPayment") public ResponseEntity connectPayment(@PathVariable Long id, @RequestParam(value = "paymentId") Long paymentId) { - return Optional.ofNullable(getOrderResource(orderService.connectPayment(id, paymentId))) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(orderService.get(id) + .connectPayment(paymentId)) + .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/orders/{id}/commands/createPayment") + @RequestMapping(path = "/orders/{id}/commands/createPayment") public ResponseEntity createPayment(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource( - orderService.createPayment(id))) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(orderService.get(id) + .createPayment()) + .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/orders/{id}/commands/processPayment") + @RequestMapping(path = "/orders/{id}/commands/processPayment") public ResponseEntity processPayment(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource( - orderService.applyCommand(id, OrderCommand.PROCESS_PAYMENT))) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(orderService.get(id) + .processPayment()) + .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/orders/{id}/commands/reserveInventory") + @RequestMapping(path = "/orders/{id}/commands/reserveInventory") public ResponseEntity reserveInventory(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource( - orderService.applyCommand(id, OrderCommand.RESERVE_INVENTORY))) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(orderService.get(id) + .reserveInventory(id)) + .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @@ -120,17 +128,10 @@ public class OrderController { * @return a hypermedia resource for the fetched {@link Order} */ private Resource getOrderResource(Long id) { - Resource orderResource = null; - // Get the order for the provided id - Order order = orderService.getOrder(id); + Order order = orderService.get(id); - // If the order exists, wrap the hypermedia response - if (order != null) - orderResource = getOrderResource(order); - - - return orderResource; + return getOrderResource(order); } /** @@ -156,12 +157,13 @@ public class OrderController { * @return a hypermedia resource for the updated {@link Order} */ private Resource updateOrderResource(Long id, Order order) { - return getOrderResource(orderService.updateOrder(id, order)); + order.setIdentity(id); + return getOrderResource(orderService.update(order)); } /** - * Appends an {@link OrderEvent} domain event to the event log of the {@link Order} - * aggregate with the specified orderId. + * Appends an {@link OrderEvent} domain event to the event log of the {@link Order} aggregate with the + * specified orderId. * * @param orderId is the unique identifier for the {@link Order} * @param event is the {@link OrderEvent} that attempts to alter the state of the {@link Order} @@ -170,7 +172,8 @@ public class OrderController { private Resource appendEventResource(Long orderId, OrderEvent event) { Resource eventResource = null; - event = orderService.appendEvent(orderId, event); + orderService.get(orderId) + .sendAsyncEvent(event); if (event != null) { eventResource = new Resource<>(event, @@ -188,75 +191,20 @@ public class OrderController { return eventResource; } - /** - * Get the {@link OrderCommand} hypermedia resource that lists the available commands that can be applied - * to an {@link Order} entity. - * - * @param id is the {@link Order} identifier to provide command links for - * @return an {@link OrderCommands} with a collection of embedded command links - */ - private OrderCommands getCommandsResource(Long id) { - // Get the order resource for the identifier - Resource orderResource = getOrderResource(id); + private Events getOrderEventResources(Long id) { + return eventService.find(id); + } - // Create a new order commands hypermedia resource - OrderCommands commandResource = new OrderCommands(); + private LinkBuilder linkBuilder(String name, Long id) { + Method method; - // Add order command hypermedia links - if (orderResource != null) { - commandResource.add( - new Link(new UriTemplate( - getCommandLinkBuilder(id) - .slash("connectAccount") - .toUri() - .toString(), - new TemplateVariables( - new TemplateVariable("accountId", - TemplateVariable.VariableType.REQUEST_PARAM))), "connectAccount"), - getCommandLinkBuilder(id) - .slash("reserveInventory") - .withRel("reserveInventory"), - getCommandLinkBuilder(id) - .slash("createPayment") - .withRel("createPayment"), - new Link(new UriTemplate( - getCommandLinkBuilder(id) - .slash("connectPayment") - .toUri() - .toString(), - new TemplateVariables( - new TemplateVariable("paymentId", - TemplateVariable.VariableType.REQUEST_PARAM))), "connectPayment"), - getCommandLinkBuilder(id) - .slash("processPayment") - .withRel("processPayment") - ); + try { + method = OrderController.class.getMethod(name, Long.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); } - return commandResource; - } - - /** - * Get {@link OrderEvents} for the supplied {@link Order} identifier. - * - * @param id is the unique identifier of the {@link Order} - * @return a list of {@link OrderEvent} wrapped in a hypermedia {@link OrderEvents} resource - */ - private OrderEvents getOrderEventResources(Long id) { - return new OrderEvents(id, eventService.getOrderEvents(id)); - } - - /** - * Generate a {@link LinkBuilder} for generating the {@link OrderCommands}. - * - * @param id is the unique identifier for a {@link Order} - * @return a {@link LinkBuilder} for the {@link OrderCommands} - */ - private LinkBuilder getCommandLinkBuilder(Long id) { - return linkTo(OrderController.class) - .slash("orders") - .slash(id) - .slash("commands"); + return linkTo(OrderController.class, method, id); } /** @@ -266,29 +214,26 @@ public class OrderController { * @return is a hypermedia enriched resource for the supplied {@link Order} entity */ private Resource getOrderResource(Order order) { - Resource orderResource; + Assert.notNull(order, "Order must not be null"); - // Prepare hypermedia response - orderResource = new Resource<>(order, - linkTo(OrderController.class) - .slash("orders") - .slash(order.getOrderId()) - .withSelfRel(), - linkTo(OrderController.class) - .slash("orders") - .slash(order.getOrderId()) - .slash("events") - .withRel("events"), - getCommandLinkBuilder(order.getOrderId()) - .withRel("commands") - ); + // Add command link + order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands")); + + // Add get events link + order.add(linkBuilder("getOrderEvents", order.getIdentity()).withRel("events")); if (order.getAccountId() != null) - orderResource.add(new Link("http://account-service/v1/accounts/" + order.getAccountId(), "account")); + order.add(new Link("http://account-service/v1/accounts/" + order.getAccountId(), "account")); if (order.getPaymentId() != null) - orderResource.add(new Link("http://localhost:8082/v1/payments/" + order.getPaymentId(), "payment")); + order.add(new Link("http://localhost:8082/v1/payments/" + order.getPaymentId(), "payment")); - return orderResource; + return new Resource<>(order); + } + + private ResourceSupport getCommandsResources(Long id) { + Order order = new Order(); + order.setIdentity(id); + return new Resource<>(order.getCommands()); } } diff --git a/order/order-web/src/main/java/demo/payment/Payment.java b/order/order-web/src/main/java/demo/payment/Payment.java index 13324e0..c600399 100644 --- a/order/order-web/src/main/java/demo/payment/Payment.java +++ b/order/order-web/src/main/java/demo/payment/Payment.java @@ -1,8 +1,6 @@ package demo.payment; -import demo.domain.BaseEntity; - -public class Payment extends BaseEntity { +public class Payment { private Double amount; private PaymentMethod paymentMethod; diff --git a/order/order-worker/src/main/java/demo/event/EventService.java b/order/order-worker/src/main/java/demo/event/EventService.java index dc7ee97..b424485 100644 --- a/order/order-worker/src/main/java/demo/event/EventService.java +++ b/order/order-worker/src/main/java/demo/event/EventService.java @@ -29,6 +29,8 @@ public class EventService { 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 diff --git a/payment/payment-web/src/main/java/demo/domain/AbstractEntity.java b/payment/payment-web/src/main/java/demo/domain/AbstractEntity.java index 431a1d0..e0d6b22 100644 --- a/payment/payment-web/src/main/java/demo/domain/AbstractEntity.java +++ b/payment/payment-web/src/main/java/demo/domain/AbstractEntity.java @@ -1,16 +1,19 @@ package demo.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; +import demo.event.Event; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import javax.persistence.*; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class AbstractEntity extends Aggregate implements Serializable { +public abstract class AbstractEntity extends Aggregate implements Serializable { private T identity; @@ -20,6 +23,9 @@ public class AbstractEntity extends Aggregate impleme @LastModifiedDate private Long lastModified; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List events = new ArrayList<>(); + public AbstractEntity() { } @@ -39,6 +45,16 @@ public class AbstractEntity extends Aggregate impleme this.lastModified = lastModified; } + @Override + @JsonIgnore + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + @Override public T getIdentity() { return identity; diff --git a/payment/payment-web/src/main/java/demo/payment/Payment.java b/payment/payment-web/src/main/java/demo/payment/Payment.java index 1bfa404..3b4f069 100644 --- a/payment/payment-web/src/main/java/demo/payment/Payment.java +++ b/payment/payment-web/src/main/java/demo/payment/Payment.java @@ -11,8 +11,6 @@ import demo.payment.controller.PaymentController; import org.springframework.hateoas.Link; import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @@ -23,15 +21,12 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * @author Kenny Bastani */ @Entity -public class Payment extends AbstractEntity { +public class Payment extends AbstractEntity { @Id @GeneratedValue private Long id; - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Set events = new HashSet<>(); - @Enumerated(value = EnumType.STRING) private PaymentStatus status; @@ -61,15 +56,6 @@ public class Payment extends AbstractEntity { this.id = id; } - @JsonIgnore - public Set getEvents() { - return events; - } - - public void setEvents(Set events) { - this.events = events; - } - public PaymentStatus getStatus() { return status; } @@ -131,6 +117,4 @@ public class Payment extends AbstractEntity { .slash(getIdentity()) .withSelfRel(); } - - } diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentProvider.java b/payment/payment-web/src/main/java/demo/payment/PaymentProvider.java index 8e83593..9f76a06 100644 --- a/payment/payment-web/src/main/java/demo/payment/PaymentProvider.java +++ b/payment/payment-web/src/main/java/demo/payment/PaymentProvider.java @@ -1,19 +1,27 @@ package demo.payment; import demo.domain.Provider; -import demo.domain.Service; +import demo.event.EventService; +import demo.event.PaymentEvent; @org.springframework.stereotype.Service public class PaymentProvider extends Provider { private final PaymentService paymentService; + private final EventService eventService; - public PaymentProvider(PaymentService paymentService) { + public PaymentProvider(PaymentService paymentService, EventService eventService) { this.paymentService = paymentService; + this.eventService = eventService; } @Override - protected Service getDefaultService() { + public PaymentService getDefaultService() { return paymentService; } + + @Override + public EventService getDefaultEventService() { + return eventService; + } } diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentService.java b/payment/payment-web/src/main/java/demo/payment/PaymentService.java index f4fe652..a18b15f 100644 --- a/payment/payment-web/src/main/java/demo/payment/PaymentService.java +++ b/payment/payment-web/src/main/java/demo/payment/PaymentService.java @@ -4,11 +4,8 @@ import demo.domain.Service; import demo.event.EventService; import demo.event.PaymentEvent; import demo.event.PaymentEventType; -import demo.util.ConsistencyModel; import org.springframework.util.Assert; -import java.util.Objects; - /** * The {@link PaymentService} provides transactional support for managing {@link Payment} entities. This service also * provides event sourcing support for {@link PaymentEvent}. Events can be appended to an {@link Payment}, which @@ -18,7 +15,7 @@ import java.util.Objects; * @author Kenny Bastani */ @org.springframework.stereotype.Service -public class PaymentService extends Service { +public class PaymentService extends Service { private final PaymentRepository paymentRepository; private final EventService eventService; @@ -29,125 +26,50 @@ public class PaymentService extends Service { } public Payment registerPayment(Payment payment) { - - payment = createPayment(payment); + payment = create(payment); // Trigger the payment creation event - PaymentEvent event = appendEvent(payment.getIdentity(), - new PaymentEvent(PaymentEventType.PAYMENT_CREATED)); + PaymentEvent event = payment.sendEvent(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment)); // Attach payment identifier - event.getEntity().setIdentity(payment.getIdentity()); + event.getEntity() + .setIdentity(payment.getIdentity()); + + event.getEntity().getLinks().clear(); // Return the result return event.getEntity(); } - /** - * Create a new {@link Payment} entity. - * - * @param payment is the {@link Payment} to create - * @return the newly created {@link Payment} - */ - public Payment createPayment(Payment payment) { - - // Save the payment to the repository - payment = paymentRepository.saveAndFlush(payment); - - return payment; - } - - /** - * Get an {@link Payment} entity for the supplied identifier. - * - * @param id is the unique identifier of a {@link Payment} entity - * @return an {@link Payment} entity - */ - public Payment getPayment(Long id) { + public Payment get(Long id) { return paymentRepository.findOne(id); } - /** - * Update an {@link Payment} entity with the supplied identifier. - * - * @param id is the unique identifier of the {@link Payment} entity - * @param payment is the {@link Payment} containing updated fields - * @return the updated {@link Payment} entity - */ - public Payment updatePayment(Long id, Payment payment) { - Assert.notNull(id, "Payment id must be present in the resource URL"); + public Payment create(Payment payment) { + // Save the payment to the repository + return paymentRepository.saveAndFlush(payment); + } + + public Payment update(Payment payment) { Assert.notNull(payment, "Payment request body cannot be null"); + Assert.notNull(payment.getIdentity(), "Payment id must be present in the resource URL"); - if (payment.getIdentity() != null) { - Assert.isTrue(Objects.equals(id, payment.getIdentity()), - "The payment id in the request body must match the resource URL"); - } else { - payment.setIdentity(id); - } - - Assert.state(paymentRepository.exists(id), + Assert.state(paymentRepository.exists(payment.getIdentity()), "The payment with the supplied id does not exist"); - Payment currentPayment = paymentRepository.findOne(id); + Payment currentPayment = get(payment.getIdentity()); currentPayment.setStatus(payment.getStatus()); + currentPayment.setPaymentMethod(payment.getPaymentMethod()); + currentPayment.setOrderId(payment.getOrderId()); + currentPayment.setAmount(payment.getAmount()); return paymentRepository.save(currentPayment); } - /** - * Delete the {@link Payment} with the supplied identifier. - * - * @param id is the unique identifier for the {@link Payment} - */ - public Boolean deletePayment(Long id) { + public boolean delete(Long id) { Assert.state(paymentRepository.exists(id), "The payment with the supplied id does not exist"); this.paymentRepository.delete(id); return true; } - - /** - * Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier. - * - * @param paymentId is the unique identifier for the {@link Payment} - * @param event is the {@link PaymentEvent} to append to the {@link Payment} entity - * @return the newly appended {@link PaymentEvent} - */ - public PaymentEvent appendEvent(Long paymentId, PaymentEvent event) { - return appendEvent(paymentId, event, ConsistencyModel.BASE); - } - - /** - * Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier. - * - * @param paymentId is the unique identifier for the {@link Payment} - * @param event is the {@link PaymentEvent} to append to the {@link Payment} entity - * @return the newly appended {@link PaymentEvent} - */ - public PaymentEvent appendEvent(Long paymentId, PaymentEvent event, ConsistencyModel consistencyModel) { - - // Get the entity - Payment payment = getPayment(paymentId); - Assert.notNull(payment, "The payment with the supplied id does not exist"); - - // Add the entity to the event - event.setEntity(payment); - event = eventService.save(paymentId, event); - - // Add the event to the entity - payment.getEvents().add(event); - paymentRepository.saveAndFlush(payment); - - // Applies the event for the chosen consistency model - switch (consistencyModel) { - case BASE: - eventService.sendAsync(event); - break; - case ACID: - event = eventService.send(event); - break; - } - - return event; - } } 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 0cf72ea..ddff6c8 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 @@ -61,7 +61,7 @@ public class PaymentController { @RequestMapping(path = "/payments/{id}", method = RequestMethod.DELETE) public ResponseEntity deletePayment(@PathVariable Long id) { - return Optional.ofNullable(paymentService.deletePayment(id)) + return Optional.ofNullable(paymentService.delete(id)) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) .orElseThrow(() -> new RuntimeException("Payment deletion failed")); } @@ -74,7 +74,7 @@ public class PaymentController { } @RequestMapping(path = "/payments/{id}/events", method = RequestMethod.POST) - public ResponseEntity createPayment(@PathVariable Long id, @RequestBody PaymentEvent event) { + public ResponseEntity createPaymentEvents(@PathVariable Long id, @RequestBody PaymentEvent event) { return Optional.ofNullable(appendEventResource(id, event)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) .orElseThrow(() -> new RuntimeException("Append payment event failed")); @@ -89,7 +89,7 @@ public class PaymentController { @RequestMapping(path = "/payments/{id}/commands/connectOrder") public ResponseEntity connectOrder(@PathVariable Long id, @RequestParam(value = "orderId") Long orderId) { - return Optional.of(paymentService.getPayment(id) + return Optional.of(paymentService.get(id) .connectOrder(orderId)) .map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); @@ -97,7 +97,7 @@ public class PaymentController { @RequestMapping(path = "/payments/{id}/commands/processPayment") public ResponseEntity processPayment(@PathVariable Long id) { - return Optional.of(paymentService.getPayment(id) + return Optional.of(paymentService.get(id) .processPayment()) .map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); @@ -111,7 +111,7 @@ public class PaymentController { */ private Resource getPaymentResource(Long id) { // Get the payment for the provided id - Payment payment = paymentService.getPayment(id); + Payment payment = paymentService.get(id); return getPaymentResource(payment); } @@ -128,7 +128,9 @@ public class PaymentController { // Create the new payment payment = paymentService.registerPayment(payment); - return getPaymentResource(payment); + payment.getLinks().clear(); + + return new Resource<>(payment); } /** @@ -139,7 +141,8 @@ public class PaymentController { * @return a hypermedia resource for the updated {@link Payment} */ private Resource updatePaymentResource(Long id, Payment payment) { - return getPaymentResource(paymentService.updatePayment(id, payment)); + payment.setIdentity(id); + return getPaymentResource(paymentService.update(payment)); } /** @@ -153,7 +156,7 @@ public class PaymentController { private Resource appendEventResource(Long paymentId, PaymentEvent event) { Resource eventResource = null; - event = paymentService.appendEvent(paymentId, event); + event = paymentService.get(paymentId).sendEvent(event); if (event != null) { eventResource = new Resource<>(event, @@ -196,11 +199,15 @@ public class PaymentController { private Resource getPaymentResource(Payment payment) { Assert.notNull(payment, "Payment must not be null"); - // Add command link - payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands")); + if(!payment.hasLink("commands")) { + // Add command link + payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands")); + } - // Add get events link - payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events")); + if(!payment.hasLink("events")) { + // Add get events link + payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events")); + } return new Resource<>(payment); } 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 e128b12..a08e2fe 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java @@ -40,7 +40,7 @@ public class PaymentControllerTest { Payment payment = new Payment(42.0, PaymentMethod.CREDIT_CARD); - given(this.paymentService.getPayment(1L)).willReturn(payment); + given(this.paymentService.get(1L)).willReturn(payment); given(this.eventService.find(1L)).willReturn(new Events<>(1L, Collections .singletonList(new PaymentEvent(PaymentEventType .PAYMENT_CREATED)))); 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 72f50ef..78ba5ef 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java @@ -33,7 +33,7 @@ public class PaymentServiceTests { given(this.paymentRepository.findOne(1L)).willReturn(expected); - Payment actual = paymentService.getPayment(1L); + Payment actual = paymentService.get(1L); assertThat(actual).isNotNull(); assertThat(actual.getPaymentMethod()).isEqualTo(PaymentMethod.CREDIT_CARD); 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 f65e125..427f555 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 @@ -2,9 +2,12 @@ package demo.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import demo.event.Event; +import demo.event.EventService; import org.springframework.context.ApplicationContext; import org.springframework.core.ResolvableType; import org.springframework.hateoas.*; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.annotation.RequestParam; @@ -24,7 +27,8 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * * @author Kenny Bastani */ -public abstract class Aggregate extends ResourceSupport implements Value { +public abstract class Aggregate extends ResourceSupport implements + Value { @JsonProperty("id") abstract ID getIdentity(); @@ -39,6 +43,7 @@ public abstract class Aggregate extends ResourceSupport * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist */ @SuppressWarnings("unchecked") + @JsonIgnore protected , A extends Aggregate> T getAction( Class actionType) throws IllegalArgumentException { Provider provider = getProvider(); @@ -53,7 +58,8 @@ public abstract class Aggregate extends ResourceSupport * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist */ @SuppressWarnings("unchecked") - protected , A extends Aggregate> T getProvider() throws IllegalArgumentException { + @JsonIgnore + public , A extends Aggregate> T getProvider() throws IllegalArgumentException { return getProvider((Class) ResolvableType .forClassWithGenerics(Provider.class, ResolvableType.forInstance(this)) .getRawClass()); @@ -65,12 +71,50 @@ public abstract class Aggregate extends ResourceSupport * @return an instance of the requested {@link Provider} * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist */ - protected , A extends Aggregate> T getProvider( - Class providerType) throws IllegalArgumentException { + @JsonIgnore + public , A extends Aggregate> T getProvider(Class providerType) throws + IllegalArgumentException { Assert.notNull(applicationContext, "The application context is unavailable"); T provider = applicationContext.getBean(providerType); Assert.notNull(provider, "The requested provider is not registered in the application context"); - return provider; + return (T) provider; + } + + @JsonIgnore + public abstract List getEvents(); + + /** + * Append a new {@link Event} to the {@link Aggregate} reference for the supplied identifier. + * + * @param event is the {@link Event} to append to the {@link Aggregate} entity + * @return the newly appended {@link Event} + */ + + public E sendEvent(E event, Link... links) { + EventService eventService = getEventService(); + event = eventService.send(appendEvent(event), links); + return event; + } + + /** + * Append a new {@link Event} to the {@link Aggregate} reference for the supplied identifier. + * + * @param event is the {@link Event} to append to the {@link Aggregate} entity + * @return the newly appended {@link Event} + */ + + public boolean sendAsyncEvent(E event, Link... links) { + return getEventService().sendAsync(appendEvent(event), links); + } + + @Transactional + @SuppressWarnings("unchecked") + public E appendEvent(E event) { + event.setEntity(this); + getEventService().save(event); + getEvents().add(event); + getEntityService().update(this); + return event; } @Override @@ -79,13 +123,13 @@ public abstract class Aggregate extends ResourceSupport .stream() .collect(Collectors.toList()); - links.add(getId()); + if(!super.hasLink("self")) + links.add(getId()); return links; } @JsonIgnore - @SuppressWarnings("unchecked") public CommandResources getCommands() { CommandResources commandResources = new CommandResources(); @@ -120,6 +164,18 @@ public abstract class Aggregate extends ResourceSupport return commandResources; } + @SuppressWarnings("unchecked") + private Service getEntityService() { + return (Service) getProvider().getDefaultService(); + } + + @SuppressWarnings("unchecked") + private EventService getEventService() { + return (EventService) getProvider().getDefaultEventService(); + } + public static class CommandResources extends ResourceSupport { } + + } diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Provider.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Provider.java index f1b70fc..82d227f 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Provider.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Provider.java @@ -1,5 +1,6 @@ package demo.domain; +import demo.event.EventService; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -24,5 +25,7 @@ public abstract class Provider implements ApplicationContex Provider.applicationContext = applicationContext; } - protected abstract Service getDefaultService(); + public abstract Service getDefaultService(); + + public abstract EventService getDefaultEventService(); } diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Service.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Service.java index c36762e..7e0eb11 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Service.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Service.java @@ -4,6 +4,8 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import java.io.Serializable; + /** * A {@link Service} is a functional unit that provides a need. Services are immutable and often stateless. Services * always consume or produce {@link Commodity} objects. Services are addressable and discoverable by other services. @@ -11,7 +13,7 @@ import org.springframework.context.ApplicationContextAware; * @author Kenny Bastani */ @org.springframework.stereotype.Service -public abstract class Service implements ApplicationContextAware { +public abstract class Service implements ApplicationContextAware { private ApplicationContext applicationContext; @Override @@ -19,6 +21,11 @@ public abstract class Service implements ApplicationContext this.applicationContext = applicationContext; } + public abstract T get(ID id); + public abstract T create(T entity); + public abstract T update(T entity); + public abstract boolean delete(ID id); + @SuppressWarnings("unchecked") public > A getAction(Class clazz) { return applicationContext.getBean(clazz); diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java index c5e2abe..8ea9716 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java @@ -1,5 +1,6 @@ package demo.event; +import demo.domain.Aggregate; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.core.EvoInflectorRelProvider; @@ -21,7 +22,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * @see org.springframework.stereotype.Repository * @see ResourceSupport */ -public abstract class Event extends ResourceSupport { +public abstract class Event extends ResourceSupport { public Event() { } diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java index 1a73a45..b18b845 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java @@ -1,7 +1,7 @@ package demo.event; +import demo.domain.Aggregate; import org.springframework.hateoas.Link; -import org.springframework.hateoas.ResourceSupport; import java.io.Serializable; @@ -19,18 +19,14 @@ public interface EventService { * Raises a synchronous domain event. An {@link Event} will be applied to an entity through a chain of HTTP * requests/responses. * - * @param event - * @param links * @return the applied {@link Event} */ - S send(S event, Link... links); + S send(S event, Link... links); /** * Raises an asynchronous domain event. An {@link Event} will be applied to an entity through a chain of AMQP * messages. * - * @param event - * @param links * @return a flag indicating if the {@link Event} message was sent successfully */ Boolean sendAsync(S event, Link... links); @@ -39,7 +35,6 @@ public interface EventService { * Saves a given event entity. Use the returned instance for further operations as the save operation might have * changed the entity instance completely. * - * @param event * @return the saved event entity */ S save(S event); @@ -48,8 +43,6 @@ public interface EventService { * Saves a given event entity. Use the returned instance for further operations as the save operation might have * changed the entity instance completely. The {@link ID} parameter is the unique {@link Event} identifier. * - * @param id - * @param event * @return the saved event entity */ S save(ID id, S event); @@ -57,7 +50,6 @@ public interface EventService { /** * Retrieves an {@link Event} entity by its id. * - * @param id * @return the {@link Event} entity with the given id or {@literal null} if none found */ T findOne(EID id); @@ -65,7 +57,6 @@ public interface EventService { /** * Retrieves an entity's {@link Event}s by its id. * - * @param entityId * @return a {@link Events} containing a collection of {@link Event}s */ E find(ID entityId); diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java index 3eeec65..933477d 100755 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java @@ -1,12 +1,13 @@ package demo.event; +import demo.domain.Aggregate; import org.apache.log4j.Logger; import org.springframework.cloud.stream.messaging.Source; import org.springframework.data.domain.PageRequest; import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.integration.support.MessageBuilder; import org.springframework.web.client.RestTemplate; @@ -38,14 +39,18 @@ class EventServiceImpl implements Even this.restTemplate = restTemplate; } - public S send(S event, Link... links) { + public S send(S event, Link... links) { // Assemble request to the event stream processor RequestEntity> requestEntity = RequestEntity.post(URI.create(EVENT_PROCESSOR_URL)) - .contentType(MediaTypes.HAL_JSON).body(new Resource(event), Resource.class); + .contentType(MediaTypes.HAL_JSON) + .body(new Resource(event), Resource.class); try { // Send the event to the event stream processor - E entity = (E) restTemplate.exchange(requestEntity, event.getEntity().getClass()).getBody(); + E entity = (E) restTemplate.exchange(requestEntity, event.getEntity() + .getClass()) + .getBody(); + // Set the applied entity reference to the event event.setEntity(entity); } catch (Exception ex) { @@ -56,7 +61,10 @@ class EventServiceImpl implements Even } public Boolean sendAsync(S event, Link... links) { - return eventStream.output().send(MessageBuilder.withPayload(event).build()); + return eventStream.output() + .send(MessageBuilder.withPayload(event) + .setHeader("contentType", MediaType.APPLICATION_JSON_UTF8_VALUE) + .build()); } public S save(S event) { @@ -75,6 +83,7 @@ class EventServiceImpl implements Even public E find(ID entityId) { return (E) new Events(entityId, eventRepository.findEventsByEntityId(entityId, - new PageRequest(0, Integer.MAX_VALUE)).getContent()); + new PageRequest(0, Integer.MAX_VALUE)) + .getContent()); } } diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java index 9513e4e..6f7c3d1 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java @@ -1,8 +1,8 @@ package demo.event; import com.fasterxml.jackson.annotation.JsonIgnore; +import demo.domain.Aggregate; import org.springframework.hateoas.Link; -import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import java.io.Serializable; @@ -13,7 +13,7 @@ import java.util.List; * * @author Kenny Bastani */ -public class Events extends Resources> { +public class Events extends Resources> { private ID entityId; diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/EmptyEvent.java b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/EmptyEvent.java new file mode 100644 index 0000000..61a962b --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/domain/EmptyEvent.java @@ -0,0 +1,57 @@ +package demo.domain; + +import demo.event.Event; + +import java.io.Serializable; + +public class EmptyEvent extends Event { + @Override + public Serializable getEventId() { + return null; + } + + @Override + public void setEventId(Serializable eventId) { + + } + + @Override + public Object getType() { + return null; + } + + @Override + public void setType(Object type) { + + } + + @Override + public Aggregate getEntity() { + return null; + } + + @Override + public void setEntity(Aggregate entity) { + + } + + @Override + public Long getCreatedAt() { + return null; + } + + @Override + public void setCreatedAt(Long createdAt) { + + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public void setLastModified(Long lastModified) { + + } +} 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 34f9aca..98e9dae 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 @@ -1,5 +1,6 @@ package demo.domain; +import demo.event.EventService; import lombok.*; import org.junit.After; import org.junit.Before; @@ -14,6 +15,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.function.Consumer; import static junit.framework.TestCase.assertEquals; @@ -97,7 +99,7 @@ public class ProviderTests { @NoArgsConstructor @Getter @Setter - public static class EmptyAggregate extends Aggregate { + public static class EmptyAggregate extends Aggregate { @NonNull private Long id; @NonNull @@ -121,6 +123,11 @@ public class ProviderTests { Long getIdentity() { return this.id; } + + @Override + public List getEvents() { + return null; + } } @Getter @@ -128,15 +135,41 @@ public class ProviderTests { public static class EmptyProvider extends Provider { private final EmptyService emptyService; - public Service getDefaultService() { + public Service getDefaultService() { return emptyService; } + + @Override + public EventService getDefaultEventService() { + return null; + } } - public static class EmptyService extends Service { + public static class EmptyService extends Service { public EmptyAggregate getEmptyAggregate(Long id) { return new EmptyAggregate(id, AggregateStatus.CREATED); } + + + @Override + public EmptyAggregate get(Long aLong) { + return null; + } + + @Override + public EmptyAggregate create(EmptyAggregate entity) { + return null; + } + + @Override + public EmptyAggregate update(EmptyAggregate entity) { + return null; + } + + @Override + public boolean delete(Long aLong) { + return false; + } } public static class EmptyAction extends Action {