Eventing through aggregate
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{" +
|
||||
@@ -1,6 +0,0 @@
|
||||
package demo.event;
|
||||
|
||||
public enum ConsistencyModel {
|
||||
BASE,
|
||||
ACID
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package demo.event;
|
||||
|
||||
public interface OrderEventRepository extends EventRepository<OrderEvent, Long> {
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package demo.order;
|
||||
|
||||
public enum OrderCommand {
|
||||
CONNECT_ACCOUNT,
|
||||
RESERVE_INVENTORY,
|
||||
CREATE_PAYMENT,
|
||||
CONNECT_PAYMENT,
|
||||
PROCESS_PAYMENT
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
35
order/order-web/src/main/java/demo/order/OrderProvider.java
Normal file
35
order/order-web/src/main/java/demo/order/OrderProvider.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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) -> {};
|
||||
}
|
||||
}
|
||||
@@ -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) -> {};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user