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>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.kbastani</groupId>
<artifactId>spring-boot-starter-data-events</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>

View File

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

View File

@@ -1,17 +1,20 @@
package demo.domain;
import demo.event.Event;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.hateoas.ResourceSupport;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity extends ResourceSupport implements Serializable {
public abstract class AbstractEntity<E extends Event, T extends Serializable> extends Aggregate<E, T> implements Serializable {
private T identity;
@CreatedDate
private Long createdAt;
@@ -19,7 +22,10 @@ public class BaseEntity extends ResourceSupport implements Serializable {
@LastModifiedDate
private Long lastModified;
public BaseEntity() {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<E> events = new ArrayList<>();
public AbstractEntity() {
}
public Long getCreatedAt() {
@@ -38,6 +44,24 @@ public class BaseEntity extends ResourceSupport implements Serializable {
this.lastModified = lastModified;
}
@Override
public List<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
public String toString() {
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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.BaseEntity;
import demo.order.Order;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
/**
* The domain event {@link OrderEvent} tracks the type and state of events as
* applied to the {@link Order} domain object. This event resource can be used
* to event source the aggregate state of {@link Order}.
* The domain event {@link OrderEvent} tracks the type and state of events as applied to the {@link Order} domain
* object. This event resource can be used to event source the aggregate state of {@link Order}.
* <p>
* This event resource also provides a transaction log that can be used to append
* actions to the event.
* This event resource also provides a transaction log that can be used to append actions to the event.
*
* @author kbastani
* @author Kenny Bastani
*/
@Entity
public class OrderEvent extends BaseEntity {
@EntityListeners(AuditingEntityListener.class)
public class OrderEvent extends Event<Order, OrderEventType, Long> {
@Id
@GeneratedValue
private Long id;
@GeneratedValue(strategy = GenerationType.AUTO)
private Long eventId;
@Enumerated(EnumType.STRING)
private OrderEventType type;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore
private Order order;
private Order entity;
@CreatedDate
private Long createdAt;
@LastModifiedDate
private Long lastModified;
public OrderEvent() {
}
@@ -37,37 +44,69 @@ public class OrderEvent extends BaseEntity {
this.type = type;
}
@JsonIgnore
public OrderEvent(OrderEventType type, Order entity) {
this.type = type;
this.entity = entity;
}
@Override
public Long getEventId() {
return id;
return eventId;
}
@Override
public void setEventId(Long id) {
this.id = id;
eventId = id;
}
@Override
public OrderEventType getType() {
return type;
}
@Override
public void setType(OrderEventType type) {
this.type = type;
}
public Order getOrder() {
return order;
@Override
public Order getEntity() {
return entity;
}
public void setOrder(Order order) {
this.order = order;
@Override
public void setEntity(Order entity) {
this.entity = entity;
}
@Override
public Long getCreatedAt() {
return createdAt;
}
@Override
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
@Override
public Long getLastModified() {
return lastModified;
}
@Override
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
@Override
public String toString() {
return "OrderEvent{" +
"id=" + id +
"eventId=" + eventId +
", type=" + type +
", order=" + order +
", entity=" + entity +
", createdAt=" + createdAt +
", lastModified=" + lastModified +
"} " + super.toString();
}
}

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;
/**
* The {@link OrderEventType} represents a collection of possible events that describe
* state transitions of {@link OrderStatus} on the {@link Order} aggregate.
* The {@link OrderEventType} represents a collection of possible events that describe state transitions of
* {@link OrderStatus} on the {@link Order} aggregate.
*
* @author kbastani
* @author Kenny Bastani
*/
public enum OrderEventType {
ORDER_CREATED,

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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.Value;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
@Entity
public class LineItem implements Serializable {
public class LineItem implements Value<Long> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)

View File

@@ -1,26 +1,26 @@
package demo.order;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import demo.address.Address;
import demo.address.AddressType;
import demo.domain.BaseEntity;
import demo.domain.AbstractEntity;
import demo.domain.Command;
import demo.event.OrderEvent;
import demo.order.action.*;
import demo.order.controller.OrderController;
import org.springframework.hateoas.Link;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@Entity(name = "orders")
public class Order extends BaseEntity {
public class Order extends AbstractEntity<OrderEvent, Long> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue
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)
private OrderStatus status;
@@ -31,6 +31,8 @@ public class Order extends BaseEntity {
@OneToOne(cascade = CascadeType.ALL)
private Address shippingAddress;
private Long accountId, paymentId;
public Order() {
this.status = OrderStatus.ORDER_CREATED;
}
@@ -43,33 +45,17 @@ public class Order extends BaseEntity {
this.shippingAddress.setAddressType(AddressType.SHIPPING);
}
@JsonIgnore
public Long getOrderId() {
return id;
@JsonProperty("orderId")
@Override
public Long getIdentity() {
return this.id;
}
public void setOrderId(Long id) {
@Override
public void setIdentity(Long id) {
this.id = id;
}
@JsonIgnore
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
@JsonIgnore
public Long getPaymentId() {
return paymentId;
}
public void setPaymentId(Long paymentId) {
this.paymentId = paymentId;
}
public OrderStatus getStatus() {
return status;
}
@@ -78,15 +64,6 @@ public class Order extends BaseEntity {
this.status = status;
}
@JsonIgnore
public Set<OrderEvent> getEvents() {
return events;
}
public void setEvents(Set<OrderEvent> events) {
this.events = events;
}
public Set<LineItem> getLineItems() {
return lineItems;
}
@@ -103,19 +80,70 @@ public class Order extends BaseEntity {
this.shippingAddress = shippingAddress;
}
public void addLineItem(LineItem lineItem) {
lineItems.add(lineItem);
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public Long getPaymentId() {
return paymentId;
}
public void setPaymentId(Long paymentId) {
this.paymentId = paymentId;
}
@Command(method = "connectAccount", controller = OrderController.class)
public Order connectAccount(Long accountId) {
getAction(ConnectAccount.class)
.getConsumer()
.accept(this, accountId);
return this;
}
@Command(method = "connectPayment", controller = OrderController.class)
public Order connectPayment(Long paymentId) {
getAction(ConnectPayment.class)
.getConsumer()
.accept(this, paymentId);
return this;
}
@Command(method = "createPayment", controller = OrderController.class)
public Order createPayment() {
getAction(CreatePayment.class)
.getConsumer()
.accept(this);
return this;
}
@Command(method = "processPayment", controller = OrderController.class)
public Order processPayment() {
getAction(ProcessPayment.class)
.getConsumer()
.accept(this);
return this;
}
@Command(method = "reserveInventory", controller = OrderController.class)
public Order reserveInventory(Long paymentId) {
getAction(ReserveInventory.class)
.getConsumer()
.accept(this);
return this;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/
@Override
public String toString() {
return "Order{" +
"id=" + id +
", accountId=" + accountId +
", paymentId=" + paymentId +
", status=" + status +
", lineItems=" + lineItems +
", shippingAddress=" + shippingAddress +
"} " + super.toString();
public Link getId() {
return linkTo(OrderController.class)
.slash("orders")
.slash(getIdentity())
.withSelfRel();
}
}

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;
import demo.event.ConsistencyModel;
import demo.event.EventService;
import demo.domain.Service;
import demo.event.OrderEvent;
import demo.event.OrderEventType;
import demo.payment.Payment;
import demo.payment.PaymentMethod;
import org.apache.log4j.Logger;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;
@Service
@CacheConfig(cacheNames = {"orders"})
public class OrderService {
private final Logger log = Logger.getLogger(OrderService.class);
@org.springframework.stereotype.Service
public class OrderService extends Service<Order, Long> {
private final OrderRepository orderRepository;
private final EventService eventService;
private final RestTemplate restTemplate;
public OrderService(OrderRepository orderRepository, EventService eventService, RestTemplate restTemplate) {
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
this.eventService = eventService;
this.restTemplate = restTemplate;
}
@CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()")
public Order registerOrder(Order order) {
order = createOrder(order);
//cacheManager.getCache("orders").evict(order.getOrderId());
order = create(order);
// Trigger the order creation event
OrderEvent event = appendEvent(order.getOrderId(),
new OrderEvent(OrderEventType.ORDER_CREATED));
order.sendAsyncEvent(new OrderEvent(OrderEventType.ORDER_CREATED, order));
// Attach order identifier
event.getOrder().setOrderId(order.getOrderId());
// Return the result
return event.getOrder();
return order;
}
/**
@@ -63,8 +30,7 @@ public class OrderService {
* @param order is the {@link Order} to create
* @return the newly created {@link Order}
*/
@CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()")
public Order createOrder(Order order) {
public Order create(Order order) {
// Save the order to the repository
order = orderRepository.saveAndFlush(order);
@@ -78,34 +44,24 @@ public class OrderService {
* @param id is the unique identifier of a {@link Order} entity
* @return an {@link Order} entity
*/
@Cacheable(cacheNames = "orders", key = "#id.toString()")
public Order getOrder(Long id) {
public Order get(Long id) {
return orderRepository.findOne(id);
}
/**
* Update an {@link Order} entity with the supplied identifier.
*
* @param id is the unique identifier of the {@link Order} entity
* @param order is the {@link Order} containing updated fields
* @return the updated {@link Order} entity
*/
@CachePut(cacheNames = "orders", key = "#id.toString()")
public Order updateOrder(Long id, Order order) {
Assert.notNull(id, "Order id must be present in the resource URL");
public Order update(Order order) {
Assert.notNull(order.getIdentity(), "Order id must be present in the resource URL");
Assert.notNull(order, "Order request body cannot be null");
if (order.getOrderId() != null) {
Assert.isTrue(Objects.equals(id, order.getOrderId()),
"The order id in the request body must match the resource URL");
} else {
order.setOrderId(id);
}
Assert.state(orderRepository.exists(id),
Assert.state(orderRepository.exists(order.getIdentity()),
"The order with the supplied id does not exist");
Order currentOrder = orderRepository.findOne(id);
Order currentOrder = get(order.getIdentity());
currentOrder.setAccountId(order.getAccountId());
currentOrder.setPaymentId(order.getPaymentId());
currentOrder.setLineItems(order.getLineItems());
@@ -120,174 +76,10 @@ public class OrderService {
*
* @param id is the unique identifier for the {@link Order}
*/
@CacheEvict(cacheNames = "orders", key = "#id.toString()")
public Boolean deleteOrder(Long id) {
public boolean delete(Long id) {
Assert.state(orderRepository.exists(id),
"The order with the supplied id does not exist");
this.orderRepository.delete(id);
return true;
}
/**
* Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier.
*
* @param orderId is the unique identifier for the {@link Order}
* @param event is the {@link OrderEvent} to append to the {@link Order} entity
* @param links is the optional {@link Link} to embed in the {@link org.springframework.hateoas.Resource}
* @return the newly appended {@link OrderEvent}
*/
public OrderEvent appendEvent(Long orderId, OrderEvent event, Link... links) {
return appendEvent(orderId, event, ConsistencyModel.ACID, links);
}
/**
* Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier.
*
* @param orderId is the unique identifier for the {@link Order}
* @param event is the {@link OrderEvent} to append to the {@link Order} entity
* @return the newly appended {@link OrderEvent}
*/
public OrderEvent appendEvent(Long orderId, OrderEvent event) {
return appendEvent(orderId, event, ConsistencyModel.ACID);
}
/**
* Append a new {@link OrderEvent} to the {@link Order} reference for the supplied identifier.
*
* @param orderId is the unique identifier for the {@link Order}
* @param event is the {@link OrderEvent} to append to the {@link Order} entity
* @return the newly appended {@link OrderEvent}
*/
public OrderEvent appendEvent(Long orderId, OrderEvent event, ConsistencyModel consistencyModel, Link... links) {
Order order = getOrder(orderId);
Assert.notNull(order, "The order with the supplied id does not exist");
event.setOrder(order);
event = eventService.createEvent(orderId, event);
order.getEvents().add(event);
order = orderRepository.saveAndFlush(order);
event.setOrder(order);
eventService.raiseEvent(event, consistencyModel, links);
return event;
}
/**
* Apply an {@link OrderCommand} to the {@link Order} with a specified identifier.
*
* @param id is the unique identifier of the {@link Order}
* @param orderCommand is the command to apply to the {@link Order}
* @return a hypermedia resource containing the updated {@link Order}
*/
@CachePut(cacheNames = "orders", key = "#id.toString()")
public Order applyCommand(Long id, OrderCommand orderCommand) {
Order order = getOrder(id);
Assert.notNull(order, "The order for the supplied id could not be found");
OrderStatus status = order.getStatus();
// TODO: Implement apply command
return order;
}
public Order connectAccount(Long id, Long accountId) {
// Get the order
Order order = getOrder(id);
// Connect the account
order.setAccountId(accountId);
order.setStatus(OrderStatus.ACCOUNT_CONNECTED);
order = updateOrder(id, order);
//cacheManager.getCache("orders").evict(id);
// Trigger the account connected event
OrderEvent event = appendEvent(order.getOrderId(),
new OrderEvent(OrderEventType.ACCOUNT_CONNECTED));
// Set non-serializable fields
event.getOrder().setAccountId(order.getAccountId());
event.getOrder().setPaymentId(order.getPaymentId());
event.getOrder().setOrderId(order.getOrderId());
// Return the result
return event.getOrder();
}
public Order connectPayment(Long id, Long paymentId) {
// Get the order
Order order = getOrder(id);
// Connect the account
order.setPaymentId(paymentId);
order.setStatus(OrderStatus.PAYMENT_CONNECTED);
order = updateOrder(id, order);
// cacheManager.getCache("orders").evict(id);
// Trigger the account connected event
OrderEvent event = appendEvent(order.getOrderId(),
new OrderEvent(OrderEventType.PAYMENT_CONNECTED));
// Set non-serializable fields
event.getOrder().setAccountId(order.getAccountId());
event.getOrder().setPaymentId(order.getPaymentId());
event.getOrder().setOrderId(order.getOrderId());
// Return the result
return event.getOrder();
}
public Order createPayment(Long id) {
// Get the order
Order order = getOrder(id);
Payment payment = new Payment();
// Calculate payment amount
payment.setAmount(order.getLineItems()
.stream()
.mapToDouble(a -> (a.getPrice() + a.getTax()) * a.getQuantity())
.sum());
// Set payment method
payment.setPaymentMethod(PaymentMethod.CREDIT_CARD);
// Create a new request entity
RequestEntity<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.EventService;
import demo.event.Events;
import demo.event.OrderEvent;
import demo.event.OrderEvents;
import org.springframework.hateoas.*;
import demo.order.Order;
import demo.order.OrderService;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.Optional;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@@ -19,9 +25,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
public class OrderController {
private final OrderService orderService;
private final EventService eventService;
private final EventService<OrderEvent, Long> eventService;
public OrderController(OrderService orderService, EventService eventService) {
public OrderController(OrderService orderService, EventService<OrderEvent, Long> eventService) {
this.orderService = orderService;
this.eventService = eventService;
}
@@ -40,7 +46,7 @@ public class OrderController {
.orElseThrow(() -> new RuntimeException("Order update failed"));
}
@GetMapping(path = "/orders/{id}")
@RequestMapping(path = "/orders/{id}")
public ResponseEntity getOrder(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
@@ -49,12 +55,12 @@ public class OrderController {
@DeleteMapping(path = "/orders/{id}")
public ResponseEntity deleteOrder(@PathVariable Long id) {
return Optional.ofNullable(orderService.deleteOrder(id))
return Optional.ofNullable(orderService.delete(id))
.map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
.orElseThrow(() -> new RuntimeException("Order deletion failed"));
}
@GetMapping(path = "/orders/{id}/events")
@RequestMapping(path = "/orders/{id}/events")
public ResponseEntity getOrderEvents(@PathVariable Long id) {
return Optional.of(getOrderEventResources(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
@@ -68,48 +74,50 @@ public class OrderController {
.orElseThrow(() -> new RuntimeException("Append order event failed"));
}
@GetMapping(path = "/orders/{id}/commands")
public ResponseEntity getOrderCommands(@PathVariable Long id) {
return Optional.ofNullable(getCommandsResource(id))
@RequestMapping(path = "/orders/{id}/commands")
public ResponseEntity getCommands(@PathVariable Long id) {
return Optional.ofNullable(getCommandsResources(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The order could not be found"));
}
@GetMapping(path = "/orders/{id}/commands/connectAccount")
@RequestMapping(path = "/orders/{id}/commands/connectAccount")
public ResponseEntity connectAccount(@PathVariable Long id, @RequestParam(value = "accountId") Long accountId) {
return Optional.ofNullable(getOrderResource(orderService.connectAccount(id, accountId)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
return Optional.ofNullable(orderService.get(id)
.connectAccount(accountId))
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/orders/{id}/commands/connectPayment")
@RequestMapping(path = "/orders/{id}/commands/connectPayment")
public ResponseEntity connectPayment(@PathVariable Long id, @RequestParam(value = "paymentId") Long paymentId) {
return Optional.ofNullable(getOrderResource(orderService.connectPayment(id, paymentId)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
return Optional.ofNullable(orderService.get(id)
.connectPayment(paymentId))
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/orders/{id}/commands/createPayment")
@RequestMapping(path = "/orders/{id}/commands/createPayment")
public ResponseEntity createPayment(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(
orderService.createPayment(id)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
return Optional.ofNullable(orderService.get(id)
.createPayment())
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/orders/{id}/commands/processPayment")
@RequestMapping(path = "/orders/{id}/commands/processPayment")
public ResponseEntity processPayment(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(
orderService.applyCommand(id, OrderCommand.PROCESS_PAYMENT)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
return Optional.ofNullable(orderService.get(id)
.processPayment())
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/orders/{id}/commands/reserveInventory")
@RequestMapping(path = "/orders/{id}/commands/reserveInventory")
public ResponseEntity reserveInventory(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(
orderService.applyCommand(id, OrderCommand.RESERVE_INVENTORY)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
return Optional.ofNullable(orderService.get(id)
.reserveInventory(id))
.map(e -> new ResponseEntity<>(getOrderResource(e), HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@@ -120,17 +128,10 @@ public class OrderController {
* @return a hypermedia resource for the fetched {@link Order}
*/
private Resource<Order> getOrderResource(Long id) {
Resource<Order> orderResource = null;
// Get the order for the provided id
Order order = orderService.getOrder(id);
Order order = orderService.get(id);
// If the order exists, wrap the hypermedia response
if (order != null)
orderResource = getOrderResource(order);
return orderResource;
return getOrderResource(order);
}
/**
@@ -156,12 +157,13 @@ public class OrderController {
* @return a hypermedia resource for the updated {@link Order}
*/
private Resource<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}
* aggregate with the specified orderId.
* Appends an {@link OrderEvent} domain event to the event log of the {@link Order} aggregate with the
* specified orderId.
*
* @param orderId is the unique identifier for the {@link Order}
* @param event is the {@link OrderEvent} that attempts to alter the state of the {@link Order}
@@ -170,7 +172,8 @@ public class OrderController {
private Resource<OrderEvent> appendEventResource(Long orderId, OrderEvent event) {
Resource<OrderEvent> eventResource = null;
event = orderService.appendEvent(orderId, event);
orderService.get(orderId)
.sendAsyncEvent(event);
if (event != null) {
eventResource = new Resource<>(event,
@@ -188,75 +191,20 @@ public class OrderController {
return eventResource;
}
/**
* Get the {@link OrderCommand} hypermedia resource that lists the available commands that can be applied
* to an {@link Order} entity.
*
* @param id is the {@link Order} identifier to provide command links for
* @return an {@link OrderCommands} with a collection of embedded command links
*/
private OrderCommands getCommandsResource(Long id) {
// Get the order resource for the identifier
Resource<Order> orderResource = getOrderResource(id);
private Events getOrderEventResources(Long id) {
return eventService.find(id);
}
// Create a new order commands hypermedia resource
OrderCommands commandResource = new OrderCommands();
private LinkBuilder linkBuilder(String name, Long id) {
Method method;
// Add order command hypermedia links
if (orderResource != null) {
commandResource.add(
new Link(new UriTemplate(
getCommandLinkBuilder(id)
.slash("connectAccount")
.toUri()
.toString(),
new TemplateVariables(
new TemplateVariable("accountId",
TemplateVariable.VariableType.REQUEST_PARAM))), "connectAccount"),
getCommandLinkBuilder(id)
.slash("reserveInventory")
.withRel("reserveInventory"),
getCommandLinkBuilder(id)
.slash("createPayment")
.withRel("createPayment"),
new Link(new UriTemplate(
getCommandLinkBuilder(id)
.slash("connectPayment")
.toUri()
.toString(),
new TemplateVariables(
new TemplateVariable("paymentId",
TemplateVariable.VariableType.REQUEST_PARAM))), "connectPayment"),
getCommandLinkBuilder(id)
.slash("processPayment")
.withRel("processPayment")
);
try {
method = OrderController.class.getMethod(name, Long.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
return commandResource;
}
/**
* Get {@link OrderEvents} for the supplied {@link Order} identifier.
*
* @param id is the unique identifier of the {@link Order}
* @return a list of {@link OrderEvent} wrapped in a hypermedia {@link OrderEvents} resource
*/
private OrderEvents getOrderEventResources(Long id) {
return new OrderEvents(id, eventService.getOrderEvents(id));
}
/**
* Generate a {@link LinkBuilder} for generating the {@link OrderCommands}.
*
* @param id is the unique identifier for a {@link Order}
* @return a {@link LinkBuilder} for the {@link OrderCommands}
*/
private LinkBuilder getCommandLinkBuilder(Long id) {
return linkTo(OrderController.class)
.slash("orders")
.slash(id)
.slash("commands");
return linkTo(OrderController.class, method, id);
}
/**
@@ -266,29 +214,26 @@ public class OrderController {
* @return is a hypermedia enriched resource for the supplied {@link Order} entity
*/
private Resource<Order> getOrderResource(Order order) {
Resource<Order> orderResource;
Assert.notNull(order, "Order must not be null");
// Prepare hypermedia response
orderResource = new Resource<>(order,
linkTo(OrderController.class)
.slash("orders")
.slash(order.getOrderId())
.withSelfRel(),
linkTo(OrderController.class)
.slash("orders")
.slash(order.getOrderId())
.slash("events")
.withRel("events"),
getCommandLinkBuilder(order.getOrderId())
.withRel("commands")
);
// Add command link
order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands"));
// Add get events link
order.add(linkBuilder("getOrderEvents", order.getIdentity()).withRel("events"));
if (order.getAccountId() != null)
orderResource.add(new Link("http://account-service/v1/accounts/" + order.getAccountId(), "account"));
order.add(new Link("http://account-service/v1/accounts/" + order.getAccountId(), "account"));
if (order.getPaymentId() != null)
orderResource.add(new Link("http://localhost:8082/v1/payments/" + order.getPaymentId(), "payment"));
order.add(new Link("http://localhost:8082/v1/payments/" + order.getPaymentId(), "payment"));
return orderResource;
return new Resource<>(order);
}
private ResourceSupport getCommandsResources(Long id) {
Order order = new Order();
order.setIdentity(id);
return new Resource<>(order.getCommands());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,27 @@
package demo.payment;
import demo.domain.Provider;
import demo.domain.Service;
import demo.event.EventService;
import demo.event.PaymentEvent;
@org.springframework.stereotype.Service
public class PaymentProvider extends Provider<Payment> {
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.eventService = eventService;
}
@Override
protected Service<? extends Payment> getDefaultService() {
public PaymentService getDefaultService() {
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.PaymentEvent;
import demo.event.PaymentEventType;
import demo.util.ConsistencyModel;
import org.springframework.util.Assert;
import java.util.Objects;
/**
* The {@link PaymentService} provides transactional support for managing {@link Payment} entities. This service also
* provides event sourcing support for {@link PaymentEvent}. Events can be appended to an {@link Payment}, which
@@ -18,7 +15,7 @@ import java.util.Objects;
* @author Kenny Bastani
*/
@org.springframework.stereotype.Service
public class PaymentService extends Service<Payment> {
public class PaymentService extends Service<Payment, Long> {
private final PaymentRepository paymentRepository;
private final EventService<PaymentEvent, Long> eventService;
@@ -29,125 +26,50 @@ public class PaymentService extends Service<Payment> {
}
public Payment registerPayment(Payment payment) {
payment = createPayment(payment);
payment = create(payment);
// Trigger the payment creation event
PaymentEvent event = appendEvent(payment.getIdentity(),
new PaymentEvent(PaymentEventType.PAYMENT_CREATED));
PaymentEvent event = payment.sendEvent(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment));
// Attach payment identifier
event.getEntity().setIdentity(payment.getIdentity());
event.getEntity()
.setIdentity(payment.getIdentity());
event.getEntity().getLinks().clear();
// Return the result
return event.getEntity();
}
/**
* Create a new {@link Payment} entity.
*
* @param payment is the {@link Payment} to create
* @return the newly created {@link Payment}
*/
public Payment createPayment(Payment payment) {
// Save the payment to the repository
payment = paymentRepository.saveAndFlush(payment);
return payment;
}
/**
* Get an {@link Payment} entity for the supplied identifier.
*
* @param id is the unique identifier of a {@link Payment} entity
* @return an {@link Payment} entity
*/
public Payment getPayment(Long id) {
public Payment get(Long id) {
return paymentRepository.findOne(id);
}
/**
* Update an {@link Payment} entity with the supplied identifier.
*
* @param id is the unique identifier of the {@link Payment} entity
* @param payment is the {@link Payment} containing updated fields
* @return the updated {@link Payment} entity
*/
public Payment updatePayment(Long id, Payment payment) {
Assert.notNull(id, "Payment id must be present in the resource URL");
public Payment create(Payment payment) {
// Save the payment to the repository
return paymentRepository.saveAndFlush(payment);
}
public Payment update(Payment payment) {
Assert.notNull(payment, "Payment request body cannot be null");
Assert.notNull(payment.getIdentity(), "Payment id must be present in the resource URL");
if (payment.getIdentity() != null) {
Assert.isTrue(Objects.equals(id, payment.getIdentity()),
"The payment id in the request body must match the resource URL");
} else {
payment.setIdentity(id);
}
Assert.state(paymentRepository.exists(id),
Assert.state(paymentRepository.exists(payment.getIdentity()),
"The payment with the supplied id does not exist");
Payment currentPayment = paymentRepository.findOne(id);
Payment currentPayment = get(payment.getIdentity());
currentPayment.setStatus(payment.getStatus());
currentPayment.setPaymentMethod(payment.getPaymentMethod());
currentPayment.setOrderId(payment.getOrderId());
currentPayment.setAmount(payment.getAmount());
return paymentRepository.save(currentPayment);
}
/**
* Delete the {@link Payment} with the supplied identifier.
*
* @param id is the unique identifier for the {@link Payment}
*/
public Boolean deletePayment(Long id) {
public boolean delete(Long id) {
Assert.state(paymentRepository.exists(id),
"The payment with the supplied id does not exist");
this.paymentRepository.delete(id);
return true;
}
/**
* Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier.
*
* @param paymentId is the unique identifier for the {@link Payment}
* @param event is the {@link PaymentEvent} to append to the {@link Payment} entity
* @return the newly appended {@link PaymentEvent}
*/
public PaymentEvent appendEvent(Long paymentId, PaymentEvent event) {
return appendEvent(paymentId, event, ConsistencyModel.BASE);
}
/**
* Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier.
*
* @param paymentId is the unique identifier for the {@link Payment}
* @param event is the {@link PaymentEvent} to append to the {@link Payment} entity
* @return the newly appended {@link PaymentEvent}
*/
public PaymentEvent appendEvent(Long paymentId, PaymentEvent event, ConsistencyModel consistencyModel) {
// Get the entity
Payment payment = getPayment(paymentId);
Assert.notNull(payment, "The payment with the supplied id does not exist");
// Add the entity to the event
event.setEntity(payment);
event = eventService.save(paymentId, event);
// Add the event to the entity
payment.getEvents().add(event);
paymentRepository.saveAndFlush(payment);
// Applies the event for the chosen consistency model
switch (consistencyModel) {
case BASE:
eventService.sendAsync(event);
break;
case ACID:
event = eventService.send(event);
break;
}
return event;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package demo.domain;
import demo.event.EventService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -24,5 +25,7 @@ public abstract class Provider<T extends Aggregate> implements ApplicationContex
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.ApplicationContextAware;
import java.io.Serializable;
/**
* A {@link Service} is a functional unit that provides a need. Services are immutable and often stateless. Services
* always consume or produce {@link Commodity} objects. Services are addressable and discoverable by other services.
@@ -11,7 +13,7 @@ import org.springframework.context.ApplicationContextAware;
* @author Kenny Bastani
*/
@org.springframework.stereotype.Service
public abstract class Service<T extends Aggregate> implements ApplicationContextAware {
public abstract class Service<T extends Aggregate, ID extends Serializable> implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
@@ -19,6 +21,11 @@ public abstract class Service<T extends Aggregate> implements ApplicationContext
this.applicationContext = applicationContext;
}
public abstract T get(ID id);
public abstract T create(T entity);
public abstract T update(T entity);
public abstract boolean delete(ID id);
@SuppressWarnings("unchecked")
public <A extends Action<T>> A getAction(Class<? extends A> clazz) {
return applicationContext.getBean(clazz);

View File

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

View File

@@ -1,7 +1,7 @@
package demo.event;
import demo.domain.Aggregate;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import java.io.Serializable;
@@ -19,18 +19,14 @@ public interface EventService<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
* requests/responses.
*
* @param event
* @param links
* @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
* messages.
*
* @param event
* @param links
* @return a flag indicating if the {@link Event} message was sent successfully
*/
<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
* changed the entity instance completely.
*
* @param event
* @return the saved event entity
*/
<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
* changed the entity instance completely. The {@link ID} parameter is the unique {@link Event} identifier.
*
* @param id
* @param event
* @return the saved event entity
*/
<S 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.
*
* @param id
* @return the {@link Event} entity with the given id or {@literal null} if none found
*/
<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.
*
* @param entityId
* @return a {@link Events} containing a collection of {@link Event}s
*/
<E extends Events> E find(ID entityId);

View File

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

View File

@@ -1,8 +1,8 @@
package demo.event;
import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.Aggregate;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import java.io.Serializable;
@@ -13,7 +13,7 @@ import java.util.List;
*
* @author Kenny Bastani
*/
public class Events<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;

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