Eventing through aggregate

This commit is contained in:
Kenny Bastani
2016-12-30 20:34:59 -05:00
parent 55cf52132a
commit 2cdfab4ec1
41 changed files with 738 additions and 972 deletions

View File

@@ -51,6 +51,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId> <artifactId>spring-boot-starter-integration</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.kbastani</groupId>
<artifactId>spring-boot-starter-data-events</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>

View File

@@ -2,11 +2,13 @@ package demo;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.hateoas.config.EnableHypermediaSupport;
@SpringBootApplication @SpringBootApplication
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class OrderServiceApplication { public class OrderServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args); SpringApplication.run(OrderServiceApplication.class, args);
} }
} }

View File

@@ -1,17 +1,20 @@
package demo.domain; package demo.domain;
import demo.event.Event;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.hateoas.ResourceSupport;
import javax.persistence.EntityListeners; import javax.persistence.*;
import javax.persistence.MappedSuperclass;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class BaseEntity extends ResourceSupport implements Serializable { public abstract class AbstractEntity<E extends Event, T extends Serializable> extends Aggregate<E, T> implements Serializable {
private T identity;
@CreatedDate @CreatedDate
private Long createdAt; private Long createdAt;
@@ -19,7 +22,10 @@ public class BaseEntity extends ResourceSupport implements Serializable {
@LastModifiedDate @LastModifiedDate
private Long lastModified; private Long lastModified;
public BaseEntity() { @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<E> events = new ArrayList<>();
public AbstractEntity() {
} }
public Long getCreatedAt() { public Long getCreatedAt() {
@@ -38,6 +44,24 @@ public class BaseEntity extends ResourceSupport implements Serializable {
this.lastModified = lastModified; this.lastModified = lastModified;
} }
@Override
public List<E> getEvents() {
return events;
}
public void setEvents(List<E> events) {
this.events = events;
}
@Override
public T getIdentity() {
return identity;
}
public void setIdentity(T id) {
this.identity = id;
}
@Override @Override
public String toString() { public String toString() {
return "BaseEntity{" + return "BaseEntity{" +

View File

@@ -1,6 +0,0 @@
package demo.event;
public enum ConsistencyModel {
BASE,
ACID
}

View File

@@ -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));
}
}

View File

@@ -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<OrderEvent, Long> {
Page<OrderEvent> findOrderEventsByOrderId(@Param("orderId") Long orderId, Pageable pageable);
}

View File

@@ -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.
* <p>
* 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.
* <p>
* 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<Resource<OrderEvent>> 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<OrderEvent> 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<OrderEvent> 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<OrderEvent> 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<OrderEvent>(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;
}
}

View File

@@ -1,34 +1,41 @@
package demo.event; package demo.event;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.BaseEntity;
import demo.order.Order; 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.*; import javax.persistence.*;
/** /**
* The domain event {@link OrderEvent} tracks the type and state of events as * The domain event {@link OrderEvent} tracks the type and state of events as applied to the {@link Order} domain
* applied to the {@link Order} domain object. This event resource can be used * object. This event resource can be used to event source the aggregate state of {@link Order}.
* to event source the aggregate state of {@link Order}.
* <p> * <p>
* This event resource also provides a transaction log that can be used to append * This event resource also provides a transaction log that can be used to append actions to the event.
* actions to the event.
* *
* @author kbastani * @author Kenny Bastani
*/ */
@Entity @Entity
public class OrderEvent extends BaseEntity { @EntityListeners(AuditingEntityListener.class)
public class OrderEvent extends Event<Order, OrderEventType, Long> {
@Id @Id
@GeneratedValue @GeneratedValue(strategy = GenerationType.AUTO)
private Long id; private Long eventId;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private OrderEventType type; private OrderEventType type;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore @JsonIgnore
private Order order; private Order entity;
@CreatedDate
private Long createdAt;
@LastModifiedDate
private Long lastModified;
public OrderEvent() { public OrderEvent() {
} }
@@ -37,37 +44,69 @@ public class OrderEvent extends BaseEntity {
this.type = type; this.type = type;
} }
@JsonIgnore public OrderEvent(OrderEventType type, Order entity) {
this.type = type;
this.entity = entity;
}
@Override
public Long getEventId() { public Long getEventId() {
return id; return eventId;
} }
@Override
public void setEventId(Long id) { public void setEventId(Long id) {
this.id = id; eventId = id;
} }
@Override
public OrderEventType getType() { public OrderEventType getType() {
return type; return type;
} }
@Override
public void setType(OrderEventType type) { public void setType(OrderEventType type) {
this.type = type; this.type = type;
} }
public Order getOrder() { @Override
return order; public Order getEntity() {
return entity;
} }
public void setOrder(Order order) { @Override
this.order = order; 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 @Override
public String toString() { public String toString() {
return "OrderEvent{" + return "OrderEvent{" +
"id=" + id + "eventId=" + eventId +
", type=" + type + ", type=" + type +
", order=" + order + ", entity=" + entity +
", createdAt=" + createdAt +
", lastModified=" + lastModified +
"} " + super.toString(); "} " + super.toString();
} }
} }

View File

@@ -0,0 +1,4 @@
package demo.event;
public interface OrderEventRepository extends EventRepository<OrderEvent, Long> {
}

View File

@@ -4,10 +4,10 @@ import demo.order.Order;
import demo.order.OrderStatus; import demo.order.OrderStatus;
/** /**
* The {@link OrderEventType} represents a collection of possible events that describe * The {@link OrderEventType} represents a collection of possible events that describe state transitions of
* state transitions of {@link OrderStatus} on the {@link Order} aggregate. * {@link OrderStatus} on the {@link Order} aggregate.
* *
* @author kbastani * @author Kenny Bastani
*/ */
public enum OrderEventType { public enum OrderEventType {
ORDER_CREATED, ORDER_CREATED,

View File

@@ -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<OrderEvent> 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<OrderEvent> 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<OrderEvent> 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;
}
}

View File

@@ -1,15 +1,15 @@
package demo.order; package demo.order;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.Value;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType; import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
import java.io.Serializable;
@Entity @Entity
public class LineItem implements Serializable { public class LineItem implements Value<Long> {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)

View File

@@ -1,26 +1,26 @@
package demo.order; package demo.order;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty;
import demo.address.Address; import demo.address.Address;
import demo.address.AddressType; import demo.address.AddressType;
import demo.domain.BaseEntity; import demo.domain.AbstractEntity;
import demo.domain.Command;
import demo.event.OrderEvent; import demo.event.OrderEvent;
import demo.order.action.*;
import demo.order.controller.OrderController;
import org.springframework.hateoas.Link;
import javax.persistence.*; import javax.persistence.*;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@Entity(name = "orders") @Entity(name = "orders")
public class Order extends BaseEntity { public class Order extends AbstractEntity<OrderEvent, Long> {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue
private Long id; private Long id;
private Long accountId;
private Long paymentId;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<OrderEvent> events = new HashSet<>();
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
private OrderStatus status; private OrderStatus status;
@@ -31,6 +31,8 @@ public class Order extends BaseEntity {
@OneToOne(cascade = CascadeType.ALL) @OneToOne(cascade = CascadeType.ALL)
private Address shippingAddress; private Address shippingAddress;
private Long accountId, paymentId;
public Order() { public Order() {
this.status = OrderStatus.ORDER_CREATED; this.status = OrderStatus.ORDER_CREATED;
} }
@@ -43,33 +45,17 @@ public class Order extends BaseEntity {
this.shippingAddress.setAddressType(AddressType.SHIPPING); this.shippingAddress.setAddressType(AddressType.SHIPPING);
} }
@JsonIgnore @JsonProperty("orderId")
public Long getOrderId() { @Override
return id; public Long getIdentity() {
return this.id;
} }
public void setOrderId(Long id) { @Override
public void setIdentity(Long id) {
this.id = 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() { public OrderStatus getStatus() {
return status; return status;
} }
@@ -78,15 +64,6 @@ public class Order extends BaseEntity {
this.status = status; this.status = status;
} }
@JsonIgnore
public Set<OrderEvent> getEvents() {
return events;
}
public void setEvents(Set<OrderEvent> events) {
this.events = events;
}
public Set<LineItem> getLineItems() { public Set<LineItem> getLineItems() {
return lineItems; return lineItems;
} }
@@ -103,19 +80,70 @@ public class Order extends BaseEntity {
this.shippingAddress = shippingAddress; this.shippingAddress = shippingAddress;
} }
public void addLineItem(LineItem lineItem) { public Long getAccountId() {
lineItems.add(lineItem); 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 @Override
public String toString() { public Link getId() {
return "Order{" + return linkTo(OrderController.class)
"id=" + id + .slash("orders")
", accountId=" + accountId + .slash(getIdentity())
", paymentId=" + paymentId + .withSelfRel();
", status=" + status +
", lineItems=" + lineItems +
", shippingAddress=" + shippingAddress +
"} " + super.toString();
} }
} }

View File

@@ -1,9 +0,0 @@
package demo.order;
public enum OrderCommand {
CONNECT_ACCOUNT,
RESERVE_INVENTORY,
CREATE_PAYMENT,
CONNECT_PAYMENT,
PROCESS_PAYMENT
}

View File

@@ -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 {
}

View File

@@ -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<Order> {
private final OrderService orderService;
private final EventService<OrderEvent, Long> eventService;
public OrderProvider(OrderService orderService, EventService<OrderEvent, Long> eventService) {
this.orderService = orderService;
this.eventService = eventService;
}
public OrderService getOrderService() {
return orderService;
}
public EventService<OrderEvent, Long> getEventService() {
return eventService;
}
@Override
public OrderService getDefaultService() {
return orderService;
}
@Override
public EventService<OrderEvent, Long> getDefaultEventService() {
return eventService;
}
}

View File

@@ -1,60 +1,27 @@
package demo.order; package demo.order;
import demo.event.ConsistencyModel; import demo.domain.Service;
import demo.event.EventService;
import demo.event.OrderEvent; import demo.event.OrderEvent;
import demo.event.OrderEventType; 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.util.Assert;
import org.springframework.web.client.RestTemplate;
import java.net.URI; @org.springframework.stereotype.Service
import java.util.Objects; public class OrderService extends Service<Order, Long> {
import java.util.Optional;
@Service
@CacheConfig(cacheNames = {"orders"})
public class OrderService {
private final Logger log = Logger.getLogger(OrderService.class);
private final OrderRepository orderRepository; 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.orderRepository = orderRepository;
this.eventService = eventService;
this.restTemplate = restTemplate;
} }
@CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()")
public Order registerOrder(Order order) { public Order registerOrder(Order order) {
order = createOrder(order); order = create(order);
//cacheManager.getCache("orders").evict(order.getOrderId());
// Trigger the order creation event // Trigger the order creation event
OrderEvent event = appendEvent(order.getOrderId(), order.sendAsyncEvent(new OrderEvent(OrderEventType.ORDER_CREATED, order));
new OrderEvent(OrderEventType.ORDER_CREATED));
// Attach order identifier return order;
event.getOrder().setOrderId(order.getOrderId());
// Return the result
return event.getOrder();
} }
/** /**
@@ -63,8 +30,7 @@ public class OrderService {
* @param order is the {@link Order} to create * @param order is the {@link Order} to create
* @return the newly created {@link Order} * @return the newly created {@link Order}
*/ */
@CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()") public Order create(Order order) {
public Order createOrder(Order order) {
// Save the order to the repository // Save the order to the repository
order = orderRepository.saveAndFlush(order); order = orderRepository.saveAndFlush(order);
@@ -78,34 +44,24 @@ public class OrderService {
* @param id is the unique identifier of a {@link Order} entity * @param id is the unique identifier of a {@link Order} entity
* @return an {@link Order} entity * @return an {@link Order} entity
*/ */
@Cacheable(cacheNames = "orders", key = "#id.toString()") public Order get(Long id) {
public Order getOrder(Long id) {
return orderRepository.findOne(id); return orderRepository.findOne(id);
} }
/** /**
* Update an {@link Order} entity with the supplied identifier. * 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 * @param order is the {@link Order} containing updated fields
* @return the updated {@link Order} entity * @return the updated {@link Order} entity
*/ */
@CachePut(cacheNames = "orders", key = "#id.toString()") public Order update(Order order) {
public Order updateOrder(Long id, Order order) { Assert.notNull(order.getIdentity(), "Order id must be present in the resource URL");
Assert.notNull(id, "Order id must be present in the resource URL");
Assert.notNull(order, "Order request body cannot be null"); Assert.notNull(order, "Order request body cannot be null");
if (order.getOrderId() != null) { Assert.state(orderRepository.exists(order.getIdentity()),
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),
"The order with the supplied id does not exist"); "The order with the supplied id does not exist");
Order currentOrder = orderRepository.findOne(id); Order currentOrder = get(order.getIdentity());
currentOrder.setAccountId(order.getAccountId()); currentOrder.setAccountId(order.getAccountId());
currentOrder.setPaymentId(order.getPaymentId()); currentOrder.setPaymentId(order.getPaymentId());
currentOrder.setLineItems(order.getLineItems()); currentOrder.setLineItems(order.getLineItems());
@@ -120,174 +76,10 @@ public class OrderService {
* *
* @param id is the unique identifier for the {@link Order} * @param id is the unique identifier for the {@link Order}
*/ */
@CacheEvict(cacheNames = "orders", key = "#id.toString()") public boolean delete(Long id) {
public Boolean deleteOrder(Long id) {
Assert.state(orderRepository.exists(id), Assert.state(orderRepository.exists(id),
"The order with the supplied id does not exist"); "The order with the supplied id does not exist");
this.orderRepository.delete(id); this.orderRepository.delete(id);
return true; 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<Resource<Payment>> requestEntity = RequestEntity.post(
URI.create("http://localhost:8082/v1/payments"))
.contentType(MediaTypes.HAL_JSON)
.body(new Resource<Payment>(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();
}
} }

View File

@@ -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<Order> {
public BiConsumer<Order, Long> 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));
};
}
}

View File

@@ -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<Order> {
public BiConsumer<Order, Long> 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));
};
}
}

View File

@@ -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<Order> {
private final Logger log = Logger.getLogger(CreatePayment.class);
private RestTemplate restTemplate;
public CreatePayment(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Consumer<Order> 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<Resource<Payment>> 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);
};
}
}

View File

@@ -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<Order> {
public Consumer<Order> getConsumer() {
return (order) -> {};
}
}

View File

@@ -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<Order> {
public Consumer<Order> getConsumer() {
return (order) -> {};
}
}

View File

@@ -1,15 +1,21 @@
package demo.order; package demo.order.controller;
import demo.event.EventController; import demo.event.EventController;
import demo.event.EventService; import demo.event.EventService;
import demo.event.Events;
import demo.event.OrderEvent; import demo.event.OrderEvent;
import demo.event.OrderEvents; import demo.order.Order;
import org.springframework.hateoas.*; 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.Optional; import java.util.Optional;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@@ -19,9 +25,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
public class OrderController { public class OrderController {
private final OrderService orderService; private final OrderService orderService;
private final EventService eventService; private final EventService<OrderEvent, Long> eventService;
public OrderController(OrderService orderService, EventService eventService) { public OrderController(OrderService orderService, EventService<OrderEvent, Long> eventService) {
this.orderService = orderService; this.orderService = orderService;
this.eventService = eventService; this.eventService = eventService;
} }
@@ -40,7 +46,7 @@ public class OrderController {
.orElseThrow(() -> new RuntimeException("Order update failed")); .orElseThrow(() -> new RuntimeException("Order update failed"));
} }
@GetMapping(path = "/orders/{id}") @RequestMapping(path = "/orders/{id}")
public ResponseEntity getOrder(@PathVariable Long id) { public ResponseEntity getOrder(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(id)) return Optional.ofNullable(getOrderResource(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
@@ -49,12 +55,12 @@ public class OrderController {
@DeleteMapping(path = "/orders/{id}") @DeleteMapping(path = "/orders/{id}")
public ResponseEntity deleteOrder(@PathVariable Long 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)) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
.orElseThrow(() -> new RuntimeException("Order deletion failed")); .orElseThrow(() -> new RuntimeException("Order deletion failed"));
} }
@GetMapping(path = "/orders/{id}/events") @RequestMapping(path = "/orders/{id}/events")
public ResponseEntity getOrderEvents(@PathVariable Long id) { public ResponseEntity getOrderEvents(@PathVariable Long id) {
return Optional.of(getOrderEventResources(id)) return Optional.of(getOrderEventResources(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
@@ -68,48 +74,50 @@ public class OrderController {
.orElseThrow(() -> new RuntimeException("Append order event failed")); .orElseThrow(() -> new RuntimeException("Append order event failed"));
} }
@GetMapping(path = "/orders/{id}/commands") @RequestMapping(path = "/orders/{id}/commands")
public ResponseEntity getOrderCommands(@PathVariable Long id) { public ResponseEntity getCommands(@PathVariable Long id) {
return Optional.ofNullable(getCommandsResource(id)) return Optional.ofNullable(getCommandsResources(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The order could not be found")); .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) { public ResponseEntity connectAccount(@PathVariable Long id, @RequestParam(value = "accountId") Long accountId) {
return Optional.ofNullable(getOrderResource(orderService.connectAccount(id, accountId))) return Optional.ofNullable(orderService.get(id)
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .connectAccount(accountId))
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .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) { public ResponseEntity connectPayment(@PathVariable Long id, @RequestParam(value = "paymentId") Long paymentId) {
return Optional.ofNullable(getOrderResource(orderService.connectPayment(id, paymentId))) return Optional.ofNullable(orderService.get(id)
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .connectPayment(paymentId))
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .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) { public ResponseEntity createPayment(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource( return Optional.ofNullable(orderService.get(id)
orderService.createPayment(id))) .createPayment())
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .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) { public ResponseEntity processPayment(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource( return Optional.ofNullable(orderService.get(id)
orderService.applyCommand(id, OrderCommand.PROCESS_PAYMENT))) .processPayment())
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .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) { public ResponseEntity reserveInventory(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource( return Optional.ofNullable(orderService.get(id)
orderService.applyCommand(id, OrderCommand.RESERVE_INVENTORY))) .reserveInventory(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .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} * @return a hypermedia resource for the fetched {@link Order}
*/ */
private Resource<Order> getOrderResource(Long id) { private Resource<Order> getOrderResource(Long id) {
Resource<Order> orderResource = null;
// Get the order for the provided id // 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 return getOrderResource(order);
if (order != null)
orderResource = getOrderResource(order);
return orderResource;
} }
/** /**
@@ -156,12 +157,13 @@ public class OrderController {
* @return a hypermedia resource for the updated {@link Order} * @return a hypermedia resource for the updated {@link Order}
*/ */
private Resource<Order> updateOrderResource(Long id, Order order) { private Resource<Order> 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} * Appends an {@link OrderEvent} domain event to the event log of the {@link Order} aggregate with the
* aggregate with the specified orderId. * specified orderId.
* *
* @param orderId is the unique identifier for the {@link Order} * @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} * @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<OrderEvent> appendEventResource(Long orderId, OrderEvent event) { private Resource<OrderEvent> appendEventResource(Long orderId, OrderEvent event) {
Resource<OrderEvent> eventResource = null; Resource<OrderEvent> eventResource = null;
event = orderService.appendEvent(orderId, event); orderService.get(orderId)
.sendAsyncEvent(event);
if (event != null) { if (event != null) {
eventResource = new Resource<>(event, eventResource = new Resource<>(event,
@@ -188,75 +191,20 @@ public class OrderController {
return eventResource; return eventResource;
} }
/** private Events getOrderEventResources(Long id) {
* Get the {@link OrderCommand} hypermedia resource that lists the available commands that can be applied return eventService.find(id);
* 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<Order> orderResource = getOrderResource(id);
// Create a new order commands hypermedia resource private LinkBuilder linkBuilder(String name, Long id) {
OrderCommands commandResource = new OrderCommands(); Method method;
// Add order command hypermedia links try {
if (orderResource != null) { method = OrderController.class.getMethod(name, Long.class);
commandResource.add( } catch (NoSuchMethodException e) {
new Link(new UriTemplate( throw new RuntimeException(e);
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")
);
} }
return commandResource; return linkTo(OrderController.class, method, id);
}
/**
* 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");
} }
/** /**
@@ -266,29 +214,26 @@ public class OrderController {
* @return is a hypermedia enriched resource for the supplied {@link Order} entity * @return is a hypermedia enriched resource for the supplied {@link Order} entity
*/ */
private Resource<Order> getOrderResource(Order order) { private Resource<Order> getOrderResource(Order order) {
Resource<Order> orderResource; Assert.notNull(order, "Order must not be null");
// Prepare hypermedia response // Add command link
orderResource = new Resource<>(order, order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands"));
linkTo(OrderController.class)
.slash("orders") // Add get events link
.slash(order.getOrderId()) order.add(linkBuilder("getOrderEvents", order.getIdentity()).withRel("events"));
.withSelfRel(),
linkTo(OrderController.class)
.slash("orders")
.slash(order.getOrderId())
.slash("events")
.withRel("events"),
getCommandLinkBuilder(order.getOrderId())
.withRel("commands")
);
if (order.getAccountId() != null) 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) 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());
} }
} }

View File

@@ -1,8 +1,6 @@
package demo.payment; package demo.payment;
import demo.domain.BaseEntity; public class Payment {
public class Payment extends BaseEntity {
private Double amount; private Double amount;
private PaymentMethod paymentMethod; private PaymentMethod paymentMethod;

View File

@@ -29,6 +29,8 @@ public class EventService {
Order result; Order result;
log.info(orderEvent);
log.info("Order event received: " + orderEvent.getLink("self").getHref()); log.info("Order event received: " + orderEvent.getLink("self").getHref());
// Generate a state machine for computing the state of the order resource // Generate a state machine for computing the state of the order resource

View File

@@ -1,16 +1,19 @@
package demo.domain; package demo.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.event.Event;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners; import javax.persistence.*;
import javax.persistence.MappedSuperclass;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class AbstractEntity<T extends Serializable> extends Aggregate<T> implements Serializable { public abstract class AbstractEntity<E extends Event, T extends Serializable> extends Aggregate<E, T> implements Serializable {
private T identity; private T identity;
@@ -20,6 +23,9 @@ public class AbstractEntity<T extends Serializable> extends Aggregate<T> impleme
@LastModifiedDate @LastModifiedDate
private Long lastModified; private Long lastModified;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<E> events = new ArrayList<>();
public AbstractEntity() { public AbstractEntity() {
} }
@@ -39,6 +45,16 @@ public class AbstractEntity<T extends Serializable> extends Aggregate<T> impleme
this.lastModified = lastModified; this.lastModified = lastModified;
} }
@Override
@JsonIgnore
public List<E> getEvents() {
return events;
}
public void setEvents(List<E> events) {
this.events = events;
}
@Override @Override
public T getIdentity() { public T getIdentity() {
return identity; return identity;

View File

@@ -11,8 +11,6 @@ import demo.payment.controller.PaymentController;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import javax.persistence.*; import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@@ -23,15 +21,12 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
* @author Kenny Bastani * @author Kenny Bastani
*/ */
@Entity @Entity
public class Payment extends AbstractEntity<Long> { public class Payment extends AbstractEntity<PaymentEvent, Long> {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<PaymentEvent> events = new HashSet<>();
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
private PaymentStatus status; private PaymentStatus status;
@@ -61,15 +56,6 @@ public class Payment extends AbstractEntity<Long> {
this.id = id; this.id = id;
} }
@JsonIgnore
public Set<PaymentEvent> getEvents() {
return events;
}
public void setEvents(Set<PaymentEvent> events) {
this.events = events;
}
public PaymentStatus getStatus() { public PaymentStatus getStatus() {
return status; return status;
} }
@@ -131,6 +117,4 @@ public class Payment extends AbstractEntity<Long> {
.slash(getIdentity()) .slash(getIdentity())
.withSelfRel(); .withSelfRel();
} }
} }

View File

@@ -1,19 +1,27 @@
package demo.payment; package demo.payment;
import demo.domain.Provider; import demo.domain.Provider;
import demo.domain.Service; import demo.event.EventService;
import demo.event.PaymentEvent;
@org.springframework.stereotype.Service @org.springframework.stereotype.Service
public class PaymentProvider extends Provider<Payment> { public class PaymentProvider extends Provider<Payment> {
private final PaymentService paymentService; private final PaymentService paymentService;
private final EventService<PaymentEvent, Long> eventService;
public PaymentProvider(PaymentService paymentService) { public PaymentProvider(PaymentService paymentService, EventService<PaymentEvent, Long> eventService) {
this.paymentService = paymentService; this.paymentService = paymentService;
this.eventService = eventService;
} }
@Override @Override
protected Service<? extends Payment> getDefaultService() { public PaymentService getDefaultService() {
return paymentService; return paymentService;
} }
@Override
public EventService<PaymentEvent, Long> getDefaultEventService() {
return eventService;
}
} }

View File

@@ -4,11 +4,8 @@ import demo.domain.Service;
import demo.event.EventService; import demo.event.EventService;
import demo.event.PaymentEvent; import demo.event.PaymentEvent;
import demo.event.PaymentEventType; import demo.event.PaymentEventType;
import demo.util.ConsistencyModel;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.util.Objects;
/** /**
* The {@link PaymentService} provides transactional support for managing {@link Payment} entities. This service also * 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 * 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 * @author Kenny Bastani
*/ */
@org.springframework.stereotype.Service @org.springframework.stereotype.Service
public class PaymentService extends Service<Payment> { public class PaymentService extends Service<Payment, Long> {
private final PaymentRepository paymentRepository; private final PaymentRepository paymentRepository;
private final EventService<PaymentEvent, Long> eventService; private final EventService<PaymentEvent, Long> eventService;
@@ -29,125 +26,50 @@ public class PaymentService extends Service<Payment> {
} }
public Payment registerPayment(Payment payment) { public Payment registerPayment(Payment payment) {
payment = create(payment);
payment = createPayment(payment);
// Trigger the payment creation event // Trigger the payment creation event
PaymentEvent event = appendEvent(payment.getIdentity(), PaymentEvent event = payment.sendEvent(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment));
new PaymentEvent(PaymentEventType.PAYMENT_CREATED));
// Attach payment identifier // Attach payment identifier
event.getEntity().setIdentity(payment.getIdentity()); event.getEntity()
.setIdentity(payment.getIdentity());
event.getEntity().getLinks().clear();
// Return the result // Return the result
return event.getEntity(); return event.getEntity();
} }
/** public Payment get(Long id) {
* 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) {
return paymentRepository.findOne(id); return paymentRepository.findOne(id);
} }
/** public Payment create(Payment payment) {
* Update an {@link Payment} entity with the supplied identifier. // Save the payment to the repository
* return paymentRepository.saveAndFlush(payment);
* @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 update(Payment payment) {
*/
public Payment updatePayment(Long id, Payment payment) {
Assert.notNull(id, "Payment id must be present in the resource URL");
Assert.notNull(payment, "Payment request body cannot be null"); 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.state(paymentRepository.exists(payment.getIdentity()),
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),
"The payment with the supplied id does not exist"); "The payment with the supplied id does not exist");
Payment currentPayment = paymentRepository.findOne(id); Payment currentPayment = get(payment.getIdentity());
currentPayment.setStatus(payment.getStatus()); currentPayment.setStatus(payment.getStatus());
currentPayment.setPaymentMethod(payment.getPaymentMethod());
currentPayment.setOrderId(payment.getOrderId());
currentPayment.setAmount(payment.getAmount());
return paymentRepository.save(currentPayment); return paymentRepository.save(currentPayment);
} }
/** public boolean delete(Long id) {
* Delete the {@link Payment} with the supplied identifier.
*
* @param id is the unique identifier for the {@link Payment}
*/
public Boolean deletePayment(Long id) {
Assert.state(paymentRepository.exists(id), Assert.state(paymentRepository.exists(id),
"The payment with the supplied id does not exist"); "The payment with the supplied id does not exist");
this.paymentRepository.delete(id); this.paymentRepository.delete(id);
return true; 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;
}
} }

View File

@@ -61,7 +61,7 @@ public class PaymentController {
@RequestMapping(path = "/payments/{id}", method = RequestMethod.DELETE) @RequestMapping(path = "/payments/{id}", method = RequestMethod.DELETE)
public ResponseEntity deletePayment(@PathVariable Long id) { 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)) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
.orElseThrow(() -> new RuntimeException("Payment deletion failed")); .orElseThrow(() -> new RuntimeException("Payment deletion failed"));
} }
@@ -74,7 +74,7 @@ public class PaymentController {
} }
@RequestMapping(path = "/payments/{id}/events", method = RequestMethod.POST) @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)) return Optional.ofNullable(appendEventResource(id, event))
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
.orElseThrow(() -> new RuntimeException("Append payment event failed")); .orElseThrow(() -> new RuntimeException("Append payment event failed"));
@@ -89,7 +89,7 @@ public class PaymentController {
@RequestMapping(path = "/payments/{id}/commands/connectOrder") @RequestMapping(path = "/payments/{id}/commands/connectOrder")
public ResponseEntity connectOrder(@PathVariable Long id, @RequestParam(value = "orderId") Long orderId) { 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)) .connectOrder(orderId))
.map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK)) .map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .orElseThrow(() -> new RuntimeException("The command could not be applied"));
@@ -97,7 +97,7 @@ public class PaymentController {
@RequestMapping(path = "/payments/{id}/commands/processPayment") @RequestMapping(path = "/payments/{id}/commands/processPayment")
public ResponseEntity processPayment(@PathVariable Long id) { public ResponseEntity processPayment(@PathVariable Long id) {
return Optional.of(paymentService.getPayment(id) return Optional.of(paymentService.get(id)
.processPayment()) .processPayment())
.map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK)) .map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied")); .orElseThrow(() -> new RuntimeException("The command could not be applied"));
@@ -111,7 +111,7 @@ public class PaymentController {
*/ */
private Resource<Payment> getPaymentResource(Long id) { private Resource<Payment> getPaymentResource(Long id) {
// Get the payment for the provided id // Get the payment for the provided id
Payment payment = paymentService.getPayment(id); Payment payment = paymentService.get(id);
return getPaymentResource(payment); return getPaymentResource(payment);
} }
@@ -128,7 +128,9 @@ public class PaymentController {
// Create the new payment // Create the new payment
payment = paymentService.registerPayment(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} * @return a hypermedia resource for the updated {@link Payment}
*/ */
private Resource<Payment> updatePaymentResource(Long id, Payment payment) { private Resource<Payment> 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<PaymentEvent> appendEventResource(Long paymentId, PaymentEvent event) { private Resource<PaymentEvent> appendEventResource(Long paymentId, PaymentEvent event) {
Resource<PaymentEvent> eventResource = null; Resource<PaymentEvent> eventResource = null;
event = paymentService.appendEvent(paymentId, event); event = paymentService.get(paymentId).sendEvent(event);
if (event != null) { if (event != null) {
eventResource = new Resource<>(event, eventResource = new Resource<>(event,
@@ -196,11 +199,15 @@ public class PaymentController {
private Resource<Payment> getPaymentResource(Payment payment) { private Resource<Payment> getPaymentResource(Payment payment) {
Assert.notNull(payment, "Payment must not be null"); Assert.notNull(payment, "Payment must not be null");
// Add command link if(!payment.hasLink("commands")) {
payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands")); // Add command link
payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands"));
}
// Add get events link if(!payment.hasLink("events")) {
payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events")); // Add get events link
payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events"));
}
return new Resource<>(payment); return new Resource<>(payment);
} }

View File

@@ -40,7 +40,7 @@ public class PaymentControllerTest {
Payment payment = new Payment(42.0, PaymentMethod.CREDIT_CARD); 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 given(this.eventService.find(1L)).willReturn(new Events<>(1L, Collections
.singletonList(new PaymentEvent(PaymentEventType .singletonList(new PaymentEvent(PaymentEventType
.PAYMENT_CREATED)))); .PAYMENT_CREATED))));

View File

@@ -33,7 +33,7 @@ public class PaymentServiceTests {
given(this.paymentRepository.findOne(1L)).willReturn(expected); given(this.paymentRepository.findOne(1L)).willReturn(expected);
Payment actual = paymentService.getPayment(1L); Payment actual = paymentService.get(1L);
assertThat(actual).isNotNull(); assertThat(actual).isNotNull();
assertThat(actual.getPaymentMethod()).isEqualTo(PaymentMethod.CREDIT_CARD); assertThat(actual.getPaymentMethod()).isEqualTo(PaymentMethod.CREDIT_CARD);

View File

@@ -2,9 +2,12 @@ package demo.domain;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import demo.event.Event;
import demo.event.EventService;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.hateoas.*; import org.springframework.hateoas.*;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -24,7 +27,8 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
* *
* @author Kenny Bastani * @author Kenny Bastani
*/ */
public abstract class Aggregate<ID extends Serializable> extends ResourceSupport implements Value<Link> { public abstract class Aggregate<E extends Event, ID extends Serializable> extends ResourceSupport implements
Value<Link> {
@JsonProperty("id") @JsonProperty("id")
abstract ID getIdentity(); abstract ID getIdentity();
@@ -39,6 +43,7 @@ public abstract class Aggregate<ID extends Serializable> extends ResourceSupport
* @throws IllegalArgumentException if the application context is unavailable or the provider does not exist * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@JsonIgnore
protected <T extends Action<A>, A extends Aggregate> T getAction( protected <T extends Action<A>, A extends Aggregate> T getAction(
Class<T> actionType) throws IllegalArgumentException { Class<T> actionType) throws IllegalArgumentException {
Provider provider = getProvider(); Provider provider = getProvider();
@@ -53,7 +58,8 @@ public abstract class Aggregate<ID extends Serializable> extends ResourceSupport
* @throws IllegalArgumentException if the application context is unavailable or the provider does not exist * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected <T extends Provider<A>, A extends Aggregate> T getProvider() throws IllegalArgumentException { @JsonIgnore
public <T extends Provider<A>, A extends Aggregate<E, ID>> T getProvider() throws IllegalArgumentException {
return getProvider((Class<T>) ResolvableType return getProvider((Class<T>) ResolvableType
.forClassWithGenerics(Provider.class, ResolvableType.forInstance(this)) .forClassWithGenerics(Provider.class, ResolvableType.forInstance(this))
.getRawClass()); .getRawClass());
@@ -65,12 +71,50 @@ public abstract class Aggregate<ID extends Serializable> extends ResourceSupport
* @return an instance of the requested {@link Provider} * @return an instance of the requested {@link Provider}
* @throws IllegalArgumentException if the application context is unavailable or the provider does not exist * @throws IllegalArgumentException if the application context is unavailable or the provider does not exist
*/ */
protected <T extends Provider<A>, A extends Aggregate> T getProvider( @JsonIgnore
Class<T> providerType) throws IllegalArgumentException { public <T extends Provider<A>, A extends Aggregate<E, ID>> T getProvider(Class<T> providerType) throws
IllegalArgumentException {
Assert.notNull(applicationContext, "The application context is unavailable"); Assert.notNull(applicationContext, "The application context is unavailable");
T provider = applicationContext.getBean(providerType); T provider = applicationContext.getBean(providerType);
Assert.notNull(provider, "The requested provider is not registered in the application context"); Assert.notNull(provider, "The requested provider is not registered in the application context");
return provider; return (T) provider;
}
@JsonIgnore
public abstract List<E> 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<E, ID> 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 @Override
@@ -79,13 +123,13 @@ public abstract class Aggregate<ID extends Serializable> extends ResourceSupport
.stream() .stream()
.collect(Collectors.toList()); .collect(Collectors.toList());
links.add(getId()); if(!super.hasLink("self"))
links.add(getId());
return links; return links;
} }
@JsonIgnore @JsonIgnore
@SuppressWarnings("unchecked")
public CommandResources getCommands() { public CommandResources getCommands() {
CommandResources commandResources = new CommandResources(); CommandResources commandResources = new CommandResources();
@@ -120,6 +164,18 @@ public abstract class Aggregate<ID extends Serializable> extends ResourceSupport
return commandResources; return commandResources;
} }
@SuppressWarnings("unchecked")
private <A extends Aggregate> Service<A, ID> getEntityService() {
return (Service<A, ID>) getProvider().getDefaultService();
}
@SuppressWarnings("unchecked")
private EventService<E, ID> getEventService() {
return (EventService<E, ID>) getProvider().getDefaultEventService();
}
public static class CommandResources extends ResourceSupport { public static class CommandResources extends ResourceSupport {
} }
} }

View File

@@ -1,5 +1,6 @@
package demo.domain; package demo.domain;
import demo.event.EventService;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
@@ -24,5 +25,7 @@ public abstract class Provider<T extends Aggregate> implements ApplicationContex
Provider.applicationContext = applicationContext; Provider.applicationContext = applicationContext;
} }
protected abstract Service<? extends T> getDefaultService(); public abstract Service<?, ?> getDefaultService();
public abstract EventService<?, ?> getDefaultEventService();
} }

View File

@@ -4,6 +4,8 @@ import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; 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 * 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. * 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 * @author Kenny Bastani
*/ */
@org.springframework.stereotype.Service @org.springframework.stereotype.Service
public abstract class Service<T extends Aggregate> implements ApplicationContextAware { public abstract class Service<T extends Aggregate, ID extends Serializable> implements ApplicationContextAware {
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
@Override @Override
@@ -19,6 +21,11 @@ public abstract class Service<T extends Aggregate> implements ApplicationContext
this.applicationContext = 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") @SuppressWarnings("unchecked")
public <A extends Action<T>> A getAction(Class<? extends A> clazz) { public <A extends Action<T>> A getAction(Class<? extends A> clazz) {
return applicationContext.getBean(clazz); return applicationContext.getBean(clazz);

View File

@@ -1,5 +1,6 @@
package demo.event; package demo.event;
import demo.domain.Aggregate;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.core.EvoInflectorRelProvider; import org.springframework.hateoas.core.EvoInflectorRelProvider;
@@ -21,7 +22,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
* @see org.springframework.stereotype.Repository * @see org.springframework.stereotype.Repository
* @see ResourceSupport * @see ResourceSupport
*/ */
public abstract class Event<T extends ResourceSupport, E, ID extends Serializable> extends ResourceSupport { public abstract class Event<T extends Aggregate, E, ID extends Serializable> extends ResourceSupport {
public Event() { public Event() {
} }

View File

@@ -1,7 +1,7 @@
package demo.event; package demo.event;
import demo.domain.Aggregate;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import java.io.Serializable; import java.io.Serializable;
@@ -19,18 +19,14 @@ public interface EventService<T extends Event, ID extends Serializable> {
* Raises a synchronous domain event. An {@link Event} will be applied to an entity through a chain of HTTP * Raises a synchronous domain event. An {@link Event} will be applied to an entity through a chain of HTTP
* requests/responses. * requests/responses.
* *
* @param event
* @param links
* @return the applied {@link Event} * @return the applied {@link Event}
*/ */
<E extends ResourceSupport, S extends T> S send(S event, Link... links); <E extends Aggregate, S extends T> 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 * Raises an asynchronous domain event. An {@link Event} will be applied to an entity through a chain of AMQP
* messages. * messages.
* *
* @param event
* @param links
* @return a flag indicating if the {@link Event} message was sent successfully * @return a flag indicating if the {@link Event} message was sent successfully
*/ */
<S extends T> Boolean sendAsync(S event, Link... links); <S extends T> Boolean sendAsync(S event, Link... links);
@@ -39,7 +35,6 @@ public interface EventService<T extends Event, ID extends Serializable> {
* Saves a given event entity. Use the returned instance for further operations as the save operation might have * Saves a given event entity. Use the returned instance for further operations as the save operation might have
* changed the entity instance completely. * changed the entity instance completely.
* *
* @param event
* @return the saved event entity * @return the saved event entity
*/ */
<S extends T> S save(S event); <S extends T> S save(S event);
@@ -48,8 +43,6 @@ public interface EventService<T extends Event, ID extends Serializable> {
* Saves a given event entity. Use the returned instance for further operations as the save operation might have * 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. * changed the entity instance completely. The {@link ID} parameter is the unique {@link Event} identifier.
* *
* @param id
* @param event
* @return the saved event entity * @return the saved event entity
*/ */
<S extends T> S save(ID id, S event); <S extends T> S save(ID id, S event);
@@ -57,7 +50,6 @@ public interface EventService<T extends Event, ID extends Serializable> {
/** /**
* Retrieves an {@link Event} entity by its id. * 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 * @return the {@link Event} entity with the given id or {@literal null} if none found
*/ */
<EID extends ID> T findOne(EID id); <EID extends ID> T findOne(EID id);
@@ -65,7 +57,6 @@ public interface EventService<T extends Event, ID extends Serializable> {
/** /**
* Retrieves an entity's {@link Event}s by its id. * Retrieves an entity's {@link Event}s by its id.
* *
* @param entityId
* @return a {@link Events} containing a collection of {@link Event}s * @return a {@link Events} containing a collection of {@link Event}s
*/ */
<E extends Events> E find(ID entityId); <E extends Events> E find(ID entityId);

View File

@@ -1,12 +1,13 @@
package demo.event; package demo.event;
import demo.domain.Aggregate;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.springframework.cloud.stream.messaging.Source; import org.springframework.cloud.stream.messaging.Source;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource; import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport; import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity; import org.springframework.http.RequestEntity;
import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.MessageBuilder;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@@ -38,14 +39,18 @@ class EventServiceImpl<T extends Event, ID extends Serializable> implements Even
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
} }
public <E extends ResourceSupport, S extends T> S send(S event, Link... links) { public <E extends Aggregate, S extends T> S send(S event, Link... links) {
// Assemble request to the event stream processor // Assemble request to the event stream processor
RequestEntity<Resource<T>> requestEntity = RequestEntity.post(URI.create(EVENT_PROCESSOR_URL)) RequestEntity<Resource<T>> requestEntity = RequestEntity.post(URI.create(EVENT_PROCESSOR_URL))
.contentType(MediaTypes.HAL_JSON).body(new Resource<T>(event), Resource.class); .contentType(MediaTypes.HAL_JSON)
.body(new Resource<T>(event), Resource.class);
try { try {
// Send the event to the event stream processor // 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 // Set the applied entity reference to the event
event.setEntity(entity); event.setEntity(entity);
} catch (Exception ex) { } catch (Exception ex) {
@@ -56,7 +61,10 @@ class EventServiceImpl<T extends Event, ID extends Serializable> implements Even
} }
public <S extends T> Boolean sendAsync(S event, Link... links) { public <S extends T> 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 extends T> S save(S event) { public <S extends T> S save(S event) {
@@ -75,6 +83,7 @@ class EventServiceImpl<T extends Event, ID extends Serializable> implements Even
public <E extends Events> E find(ID entityId) { public <E extends Events> E find(ID entityId) {
return (E) new Events(entityId, eventRepository.findEventsByEntityId(entityId, return (E) new Events(entityId, eventRepository.findEventsByEntityId(entityId,
new PageRequest(0, Integer.MAX_VALUE)).getContent()); new PageRequest(0, Integer.MAX_VALUE))
.getContent());
} }
} }

View File

@@ -1,8 +1,8 @@
package demo.event; package demo.event;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.Aggregate;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources; import org.springframework.hateoas.Resources;
import java.io.Serializable; import java.io.Serializable;
@@ -13,7 +13,7 @@ import java.util.List;
* *
* @author Kenny Bastani * @author Kenny Bastani
*/ */
public class Events<T extends ResourceSupport, E, ID extends Serializable> extends Resources<Event<T, E, ID>> { public class Events<T extends Aggregate, E, ID extends Serializable> extends Resources<Event<T, E, ID>> {
private ID entityId; private ID entityId;

View File

@@ -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) {
}
}

View File

@@ -1,5 +1,6 @@
package demo.domain; package demo.domain;
import demo.event.EventService;
import lombok.*; import lombok.*;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@@ -14,6 +15,7 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
@@ -97,7 +99,7 @@ public class ProviderTests {
@NoArgsConstructor @NoArgsConstructor
@Getter @Getter
@Setter @Setter
public static class EmptyAggregate extends Aggregate<Long> { public static class EmptyAggregate extends Aggregate<EmptyEvent, Long> {
@NonNull @NonNull
private Long id; private Long id;
@NonNull @NonNull
@@ -121,6 +123,11 @@ public class ProviderTests {
Long getIdentity() { Long getIdentity() {
return this.id; return this.id;
} }
@Override
public List<EmptyEvent> getEvents() {
return null;
}
} }
@Getter @Getter
@@ -128,15 +135,41 @@ public class ProviderTests {
public static class EmptyProvider extends Provider<EmptyAggregate> { public static class EmptyProvider extends Provider<EmptyAggregate> {
private final EmptyService emptyService; private final EmptyService emptyService;
public Service<? extends EmptyAggregate> getDefaultService() { public Service<? extends EmptyAggregate, Long> getDefaultService() {
return emptyService; return emptyService;
} }
@Override
public EventService<?, ?> getDefaultEventService() {
return null;
}
} }
public static class EmptyService extends Service<EmptyAggregate> { public static class EmptyService extends Service<EmptyAggregate, Long> {
public EmptyAggregate getEmptyAggregate(Long id) { public EmptyAggregate getEmptyAggregate(Long id) {
return new EmptyAggregate(id, AggregateStatus.CREATED); 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<EmptyAggregate> { public static class EmptyAction extends Action<EmptyAggregate> {