diff --git a/order/order-web/src/main/java/demo/config/CacheConfig.java b/order/order-web/src/main/java/demo/config/CacheConfig.java index 91fddaa..e84e06c 100644 --- a/order/order-web/src/main/java/demo/config/CacheConfig.java +++ b/order/order-web/src/main/java/demo/config/CacheConfig.java @@ -2,9 +2,7 @@ package demo.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -12,8 +10,6 @@ import org.springframework.data.redis.core.RedisTemplate; import java.util.Arrays; -@Configuration -@EnableCaching public class CacheConfig { @Bean diff --git a/order/order-web/src/main/java/demo/event/EventService.java b/order/order-web/src/main/java/demo/event/EventService.java index d344a70..09f0e92 100644 --- a/order/order-web/src/main/java/demo/event/EventService.java +++ b/order/order-web/src/main/java/demo/event/EventService.java @@ -8,6 +8,7 @@ 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; @@ -68,7 +69,10 @@ public class EventService { * @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) { + public OrderEvent raiseEvent(OrderEvent event, ConsistencyModel consistencyModel, Link... links) { + // Add embedded links + event.add(links); + switch (consistencyModel) { case BASE: asyncRaiseEvent(event); @@ -189,7 +193,7 @@ public class EventService { * @return a hypermedia resource for the supplied {@link OrderEvent} entity */ private Resource getOrderEventResource(OrderEvent event) { - return new Resource(event, Arrays.asList( + event.add(Arrays.asList( linkTo(OrderController.class) .slash("events") .slash(event.getEventId()) @@ -198,6 +202,7 @@ public class EventService { .slash("orders") .slash(event.getOrder().getOrderId()) .withRel("order"))); + return new Resource(event, event.getLinks()); } /** diff --git a/order/order-web/src/main/java/demo/order/Order.java b/order/order-web/src/main/java/demo/order/Order.java index dcb2b04..c5398c8 100644 --- a/order/order-web/src/main/java/demo/order/Order.java +++ b/order/order-web/src/main/java/demo/order/Order.java @@ -16,7 +16,8 @@ public class Order extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; - private String accountNumber; + private Long accountId; + private Long paymentId; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Set events = new HashSet<>(); @@ -34,9 +35,9 @@ public class Order extends BaseEntity { this.status = OrderStatus.ORDER_CREATED; } - public Order(String accountNumber, Address shippingAddress) { + public Order(Long accountId, Address shippingAddress) { this(); - this.accountNumber = accountNumber; + this.accountId = accountId; this.shippingAddress = shippingAddress; if (shippingAddress.getAddressType() == null) this.shippingAddress.setAddressType(AddressType.SHIPPING); @@ -51,12 +52,22 @@ public class Order extends BaseEntity { this.id = id; } - public String getAccountNumber() { - return accountNumber; + @JsonIgnore + public Long getAccountId() { + return accountId; } - public void setAccountNumber(String accountNumber) { - this.accountNumber = accountNumber; + 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() { @@ -100,8 +111,8 @@ public class Order extends BaseEntity { public String toString() { return "Order{" + "id=" + id + - ", accountNumber='" + accountNumber + '\'' + - ", events=" + events + + ", accountId=" + accountId + + ", paymentId=" + paymentId + ", status=" + status + ", lineItems=" + lineItems + ", shippingAddress=" + shippingAddress + diff --git a/order/order-web/src/main/java/demo/order/OrderController.java b/order/order-web/src/main/java/demo/order/OrderController.java index 2238d3e..b7140ea 100644 --- a/order/order-web/src/main/java/demo/order/OrderController.java +++ b/order/order-web/src/main/java/demo/order/OrderController.java @@ -1,11 +1,10 @@ package demo.order; -import demo.event.OrderEvent; -import demo.event.OrderEvents; import demo.event.EventController; import demo.event.EventService; -import org.springframework.hateoas.LinkBuilder; -import org.springframework.hateoas.Resource; +import demo.event.OrderEvent; +import demo.event.OrderEvents; +import org.springframework.hateoas.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; @@ -77,17 +76,15 @@ public class OrderController { } @GetMapping(path = "/orders/{id}/commands/connectAccount") - public ResponseEntity connectAccount(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource( - orderService.applyCommand(id, OrderCommand.CONNECT_ACCOUNT))) + 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)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @GetMapping(path = "/orders/{id}/commands/connectPayment") - public ResponseEntity connectPayment(@PathVariable Long id) { - return Optional.ofNullable(getOrderResource( - orderService.applyCommand(id, OrderCommand.CONNECT_PAYMENT))) + 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)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @@ -95,7 +92,7 @@ public class OrderController { @GetMapping(path = "/orders/{id}/commands/createPayment") public ResponseEntity createPayment(@PathVariable Long id) { return Optional.ofNullable(getOrderResource( - orderService.applyCommand(id, OrderCommand.CREATE_PAYMENT))) + orderService.createPayment(id))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @@ -154,7 +151,7 @@ public class OrderController { /** * Update a {@link Order} entity for the provided identifier. * - * @param id is the unique identifier for the {@link Order} update + * @param id is the unique identifier for the {@link Order} update * @param order is the entity representation containing any updated {@link Order} fields * @return a hypermedia resource for the updated {@link Order} */ @@ -167,7 +164,7 @@ public class OrderController { * 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} + * @param event is the {@link OrderEvent} that attempts to alter the state of the {@link Order} * @return a hypermedia resource for the newly appended {@link OrderEvent} */ private Resource appendEventResource(Long orderId, OrderEvent event) { @@ -208,18 +205,28 @@ public class OrderController { // Add order command hypermedia links if (orderResource != null) { commandResource.add( - getCommandLinkBuilder(id) - .slash("connectAccount") - .withRel("connectAccount"), + 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"), - getCommandLinkBuilder(id) - .slash("connectPayment") - .withRel("connectPayment"), + 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") @@ -276,6 +283,12 @@ public class OrderController { .withRel("commands") ); + if (order.getAccountId() != null) + orderResource.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")); + return orderResource; } } diff --git a/order/order-web/src/main/java/demo/order/OrderService.java b/order/order-web/src/main/java/demo/order/OrderService.java index 4e074be..551fc76 100644 --- a/order/order-web/src/main/java/demo/order/OrderService.java +++ b/order/order-web/src/main/java/demo/order/OrderService.java @@ -4,28 +4,39 @@ import demo.event.ConsistencyModel; import demo.event.EventService; import demo.event.OrderEvent; import demo.event.OrderEventType; -import org.springframework.cache.CacheManager; +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); + private final OrderRepository orderRepository; private final EventService eventService; - private final CacheManager cacheManager; + private final RestTemplate restTemplate; - public OrderService(OrderRepository orderRepository, EventService eventService, CacheManager cacheManager) { + public OrderService(OrderRepository orderRepository, EventService eventService, RestTemplate restTemplate) { this.orderRepository = orderRepository; this.eventService = eventService; - this.cacheManager = cacheManager; + this.restTemplate = restTemplate; } @CacheEvict(cacheNames = "orders", key = "#order.getOrderId().toString()") @@ -33,8 +44,7 @@ public class OrderService { order = createOrder(order); - cacheManager.getCache("orders") - .evict(order.getOrderId()); + //cacheManager.getCache("orders").evict(order.getOrderId()); // Trigger the order creation event OrderEvent event = appendEvent(order.getOrderId(), @@ -76,7 +86,7 @@ public class OrderService { /** * Update an {@link Order} entity with the supplied identifier. * - * @param id is the unique identifier of the {@link Order} entity + * @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 */ @@ -96,12 +106,13 @@ public class OrderService { "The order with the supplied id does not exist"); Order currentOrder = orderRepository.findOne(id); - currentOrder.setAccountNumber(order.getAccountNumber()); + currentOrder.setAccountId(order.getAccountId()); + currentOrder.setPaymentId(order.getPaymentId()); currentOrder.setLineItems(order.getLineItems()); currentOrder.setShippingAddress(order.getShippingAddress()); currentOrder.setStatus(order.getStatus()); - return orderRepository.save(currentOrder); + return orderRepository.saveAndFlush(currentOrder); } /** @@ -121,7 +132,19 @@ public class OrderService { * 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 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) { @@ -132,24 +155,25 @@ public class OrderService { * 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 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) { + 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); - orderRepository.saveAndFlush(order); - eventService.raiseEvent(event, consistencyModel); + 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 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} */ @@ -165,4 +189,105 @@ public class OrderService { return order; } + + public Order connectAccount(Long id, Long accountId) { + // Get the order + Order order = getOrder(id); + + // Connect the account + order.setAccountId(accountId); + order.setStatus(OrderStatus.ACCOUNT_CONNECTED); + order = updateOrder(id, order); + + //cacheManager.getCache("orders").evict(id); + + // Trigger the account connected event + OrderEvent event = appendEvent(order.getOrderId(), + new OrderEvent(OrderEventType.ACCOUNT_CONNECTED)); + + // Set non-serializable fields + event.getOrder().setAccountId(order.getAccountId()); + event.getOrder().setPaymentId(order.getPaymentId()); + event.getOrder().setOrderId(order.getOrderId()); + + // Return the result + return event.getOrder(); + } + + public Order connectPayment(Long id, Long paymentId) { + // Get the order + Order order = getOrder(id); + + // Connect the account + order.setPaymentId(paymentId); + order.setStatus(OrderStatus.PAYMENT_CONNECTED); + order = updateOrder(id, order); + + // cacheManager.getCache("orders").evict(id); + + // Trigger the account connected event + OrderEvent event = appendEvent(order.getOrderId(), + new OrderEvent(OrderEventType.PAYMENT_CONNECTED)); + + // Set non-serializable fields + event.getOrder().setAccountId(order.getAccountId()); + event.getOrder().setPaymentId(order.getPaymentId()); + event.getOrder().setOrderId(order.getOrderId()); + + // Return the result + return event.getOrder(); + } + + public Order createPayment(Long id) { + // Get the order + Order order = getOrder(id); + + Payment payment = new Payment(); + + // Calculate payment amount + payment.setAmount(order.getLineItems() + .stream() + .mapToDouble(a -> (a.getPrice() + a.getTax()) * a.getQuantity()) + .sum()); + + // Set payment method + payment.setPaymentMethod(PaymentMethod.CREDIT_CARD); + + // Create a new request entity + RequestEntity> requestEntity = RequestEntity.post( + URI.create("http://localhost:8082/v1/payments")) + .contentType(MediaTypes.HAL_JSON) + .body(new Resource(payment), Resource.class); + + // Update the order entity's status + payment = restTemplate.exchange(requestEntity, Payment.class) + .getBody(); + + log.info(payment); + + // Update the status + order.setStatus(OrderStatus.PAYMENT_CREATED); + order = updateOrder(id, order); + + // cacheManager.getCache("orders").evict(id); + + // Trigger the account connected event + OrderEvent event = appendEvent(order.getOrderId(), + new OrderEvent(OrderEventType.PAYMENT_CREATED), + new Link(payment.getId().getHref(), "payment")); + + // Set non-serializable fields + event.getOrder() + .setAccountId(Optional.ofNullable(event.getOrder().getAccountId()) + .orElse(order.getAccountId())); + + event.getOrder() + .setPaymentId(Optional.ofNullable(event.getOrder().getPaymentId()) + .orElse(order.getPaymentId())); + + event.getOrder().setOrderId(order.getOrderId()); + + // Return the result + return event.getOrder(); + } } diff --git a/order/order-web/src/main/java/demo/payment/Payment.java b/order/order-web/src/main/java/demo/payment/Payment.java new file mode 100644 index 0000000..13324e0 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/Payment.java @@ -0,0 +1,51 @@ +package demo.payment; + +import demo.domain.BaseEntity; + +public class Payment extends BaseEntity { + + private Double amount; + private PaymentMethod paymentMethod; + private PaymentStatus status; + + public Payment() { + } + + public Payment(Double amount, PaymentMethod paymentMethod) { + this.amount = amount; + this.paymentMethod = paymentMethod; + } + + public PaymentStatus getStatus() { + return status; + } + + public void setStatus(PaymentStatus status) { + this.status = status; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public PaymentMethod getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + } + + @Override + public String toString() { + return "Payment{" + + "amount=" + amount + + ", paymentMethod=" + paymentMethod + + ", status=" + status + + "} " + super.toString(); + } +} diff --git a/order/order-web/src/main/java/demo/payment/PaymentMethod.java b/order/order-web/src/main/java/demo/payment/PaymentMethod.java new file mode 100644 index 0000000..1591721 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/PaymentMethod.java @@ -0,0 +1,5 @@ +package demo.payment; + +public enum PaymentMethod { + CREDIT_CARD +} diff --git a/order/order-web/src/main/java/demo/payment/PaymentStatus.java b/order/order-web/src/main/java/demo/payment/PaymentStatus.java new file mode 100644 index 0000000..ef13e22 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/PaymentStatus.java @@ -0,0 +1,9 @@ +package demo.payment; + +public enum PaymentStatus { + PAYMENT_CREATED, + PAYMENT_PENDING, + PAYMENT_PROCESSED, + PAYMENT_FAILED, + PAYMENT_SUCCEEDED +} diff --git a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java index fc68cd4..f6fa652 100644 --- a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java +++ b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java @@ -5,6 +5,7 @@ import demo.event.OrderEventType; import demo.function.*; import demo.order.Order; import demo.order.OrderStatus; +import demo.payment.Payment; import demo.stream.OrderStream; import org.apache.log4j.Logger; import org.springframework.context.annotation.Bean; @@ -21,6 +22,8 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionCo import java.net.URI; import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; /** * A configuration adapter for describing a {@link StateMachine} factory that maps actions to functional @@ -99,7 +102,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter { log.info(event.getType() + ": " + event.getLink("order").getHref()); // Get the account resource for the event - Traverson traverson = new Traverson( + Traverson paymentResource = new Traverson( + URI.create(event.getLink("payment").getHref()), + MediaTypes.HAL_JSON + ); + + Traverson orderResource = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON ); - return traverson.follow("self") + Payment payment = paymentResource.follow("self") + .toEntity(Payment.class) + .getBody(); + + Order order = orderResource.follow("self") .toEntity(Order.class) .getBody(); + + Map template = new HashMap(); + template.put("paymentId", payment.getPaymentId()); + return orderResource.follow("commands", "connectPayment") + .withTemplateParameters(template) + .toObject(Order.class); + })); } diff --git a/order/order-worker/src/main/java/demo/order/Order.java b/order/order-worker/src/main/java/demo/order/Order.java index d493f19..eb8c22b 100644 --- a/order/order-worker/src/main/java/demo/order/Order.java +++ b/order/order-worker/src/main/java/demo/order/Order.java @@ -11,7 +11,6 @@ import java.util.Set; public class Order extends BaseEntity { private Long orderId; - private String accountNumber; private OrderStatus status; @@ -26,7 +25,6 @@ public class Order extends BaseEntity { public Order(String accountNumber, Address shippingAddress) { this(); - this.accountNumber = accountNumber; this.shippingAddress = shippingAddress; if (shippingAddress.getAddressType() == null) this.shippingAddress.setAddressType(AddressType.SHIPPING); @@ -40,14 +38,6 @@ public class Order extends BaseEntity { this.orderId = orderId; } - public String getAccountNumber() { - return accountNumber; - } - - public void setAccountNumber(String accountNumber) { - this.accountNumber = accountNumber; - } - public OrderStatus getStatus() { return status; } @@ -87,9 +77,9 @@ public class Order extends BaseEntity { @Override public String toString() { return "Order{" + - "orderId='" + orderId + '\'' + - ", accountNumber='" + accountNumber + '\'' + + "orderId=" + orderId + ", status=" + status + + ", events=" + events + ", lineItems=" + lineItems + ", shippingAddress=" + shippingAddress + "} " + super.toString(); diff --git a/order/order-worker/src/main/java/demo/payment/Payment.java b/order/order-worker/src/main/java/demo/payment/Payment.java new file mode 100644 index 0000000..3e5b96e --- /dev/null +++ b/order/order-worker/src/main/java/demo/payment/Payment.java @@ -0,0 +1,56 @@ +package demo.payment; + +import demo.domain.BaseEntity; + +public class Payment extends BaseEntity { + + private Long paymentId; + private Double amount; + private PaymentMethod paymentMethod; + private PaymentStatus status; + + public Payment() { + } + + public Long getPaymentId() { + return paymentId; + } + + public void setPaymentId(Long paymentId) { + this.paymentId = paymentId; + } + + public PaymentStatus getStatus() { + return status; + } + + public void setStatus(PaymentStatus status) { + this.status = status; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public PaymentMethod getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + } + + @Override + public String toString() { + return "Payment{" + + "paymentId=" + paymentId + + ", amount=" + amount + + ", paymentMethod=" + paymentMethod + + ", status=" + status + + "} " + super.toString(); + } +} diff --git a/order/order-worker/src/main/java/demo/payment/PaymentMethod.java b/order/order-worker/src/main/java/demo/payment/PaymentMethod.java new file mode 100644 index 0000000..1591721 --- /dev/null +++ b/order/order-worker/src/main/java/demo/payment/PaymentMethod.java @@ -0,0 +1,5 @@ +package demo.payment; + +public enum PaymentMethod { + CREDIT_CARD +} diff --git a/order/order-worker/src/main/java/demo/payment/PaymentStatus.java b/order/order-worker/src/main/java/demo/payment/PaymentStatus.java new file mode 100644 index 0000000..ef13e22 --- /dev/null +++ b/order/order-worker/src/main/java/demo/payment/PaymentStatus.java @@ -0,0 +1,9 @@ +package demo.payment; + +public enum PaymentStatus { + PAYMENT_CREATED, + PAYMENT_PENDING, + PAYMENT_PROCESSED, + PAYMENT_FAILED, + PAYMENT_SUCCEEDED +} diff --git a/order/order-worker/src/main/java/demo/stream/OrderStream.java b/order/order-worker/src/main/java/demo/stream/OrderStream.java index 1713e0d..6245be0 100644 --- a/order/order-worker/src/main/java/demo/stream/OrderStream.java +++ b/order/order-worker/src/main/java/demo/stream/OrderStream.java @@ -27,7 +27,7 @@ public class OrderStream { } @StreamListener(Sink.INPUT) - public void streamListerner(OrderEvent orderEvent) { + public void streamListener(OrderEvent orderEvent) { eventService.apply(orderEvent); } } diff --git a/payment/payment-web/pom.xml b/payment/payment-web/pom.xml index 6b0f333..63b7afd 100644 --- a/payment/payment-web/pom.xml +++ b/payment/payment-web/pom.xml @@ -51,6 +51,11 @@ org.springframework.boot spring-boot-starter-integration + + org.kbastani + spring-boot-starter-data-events + 1.0-SNAPSHOT + com.h2database diff --git a/payment/payment-web/src/main/java/demo/config/CacheConfig.java b/payment/payment-web/src/main/java/demo/config/CacheConfig.java index 0279460..e8b0e39 100644 --- a/payment/payment-web/src/main/java/demo/config/CacheConfig.java +++ b/payment/payment-web/src/main/java/demo/config/CacheConfig.java @@ -2,9 +2,7 @@ package demo.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -12,8 +10,6 @@ import org.springframework.data.redis.core.RedisTemplate; import java.util.Arrays; -@Configuration -@EnableCaching public class CacheConfig { @Bean diff --git a/payment/payment-web/src/main/java/demo/event/EventController.java b/payment/payment-web/src/main/java/demo/event/EventController.java deleted file mode 100644 index 18e5e31..0000000 --- a/payment/payment-web/src/main/java/demo/event/EventController.java +++ /dev/null @@ -1,39 +0,0 @@ -package demo.event; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Optional; - -@RestController -@RequestMapping("/v1") -public class EventController { - - private final EventService eventService; - - public EventController(EventService eventService) { - this.eventService = eventService; - } - - @PostMapping(path = "/events/{id}") - public ResponseEntity createEvent(@RequestBody PaymentEvent 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 PaymentEvent event, @PathVariable Long id) { - return Optional.ofNullable(eventService.updateEvent(id, event)) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("Event update failed")); - } - - @GetMapping(path = "/events/{id}") - public ResponseEntity getEvent(@PathVariable Long id) { - return Optional.ofNullable(eventService.getEvent(id)) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } -} diff --git a/payment/payment-web/src/main/java/demo/event/EventRepository.java b/payment/payment-web/src/main/java/demo/event/EventRepository.java deleted file mode 100644 index e906d2c..0000000 --- a/payment/payment-web/src/main/java/demo/event/EventRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package demo.event; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.query.Param; - -public interface EventRepository extends JpaRepository { - Page findPaymentEventsByPaymentId(@Param("paymentId") Long paymentId, Pageable pageable); -} diff --git a/payment/payment-web/src/main/java/demo/event/EventService.java b/payment/payment-web/src/main/java/demo/event/EventService.java deleted file mode 100644 index b46e642..0000000 --- a/payment/payment-web/src/main/java/demo/event/EventService.java +++ /dev/null @@ -1,214 +0,0 @@ -package demo.event; - -import demo.payment.Payment; -import demo.payment.PaymentController; -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.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 PaymentEvent} - * entities of the Payment Service. Payment domain events are generated with a {@link PaymentEventType}, - * and action logs are appended to the {@link PaymentEvent}. - * - * @author kbastani - */ -@Service -@CacheConfig(cacheNames = {"payment-events"}) -public class EventService { - - private final Logger log = Logger.getLogger(EventService.class); - - private final EventRepository eventRepository; - private final Source paymentStreamSource; - private final RestTemplate restTemplate; - - public EventService(EventRepository eventRepository, Source paymentStreamSource, RestTemplate restTemplate) { - this.eventRepository = eventRepository; - this.paymentStreamSource = paymentStreamSource; - this.restTemplate = restTemplate; - } - - /** - * Create a new {@link PaymentEvent} and append it to the event log of the referenced {@link Payment}. - * After the {@link PaymentEvent} has been persisted, send the event to the payment stream. Events can - * be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}. - * - * @param paymentId is the unique identifier for the {@link Payment} - * @param event is the {@link PaymentEvent} to create - * @param consistencyModel is the desired consistency model for the response - * @return an {@link PaymentEvent} that has been appended to the {@link Payment}'s event log - */ - public PaymentEvent createEvent(Long paymentId, PaymentEvent event, ConsistencyModel consistencyModel) { - event = createEvent(paymentId, event); - return raiseEvent(event, consistencyModel); - } - - /** - * Raise an {@link PaymentEvent} that attempts to transition the state of an {@link Payment}. - * - * @param event is an {@link PaymentEvent} that will be raised - * @param consistencyModel is the consistency model for this request - * @return an {@link PaymentEvent} that has been appended to the {@link Payment}'s event log - */ - public PaymentEvent raiseEvent(PaymentEvent event, ConsistencyModel consistencyModel) { - switch (consistencyModel) { - case BASE: - asyncRaiseEvent(event); - break; - case ACID: - event = raiseEvent(event); - break; - } - - return event; - } - - /** - * Raise an asynchronous {@link PaymentEvent} by sending an AMQP message to the payment stream. Any - * state changes will be applied to the {@link Payment} outside of the current HTTP request context. - *

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

- * Use this operation when the result of a workflow must be returned within the current HTTP request context. - * - * @param event is an {@link PaymentEvent} that will be raised - * @return an {@link PaymentEvent} which contains the consistent state of an {@link Payment} - */ - private PaymentEvent raiseEvent(PaymentEvent event) { - try { - // Create a new request entity - RequestEntity> requestEntity = RequestEntity.post( - URI.create("http://localhost:8081/v1/events")) - .contentType(MediaTypes.HAL_JSON) - .body(getPaymentEventResource(event), Resource.class); - - // Update the payment entity's status - Payment result = restTemplate.exchange(requestEntity, Payment.class) - .getBody(); - - log.info(result); - event.setPayment(result); - } catch (Exception ex) { - log.error(ex); - } - - return event; - } - - - /** - * Create a new {@link PaymentEvent} and publish it to the payment stream. - * - * @param event is the {@link PaymentEvent} to publish to the payment stream - * @return a hypermedia {@link PaymentEvent} resource - */ - @CacheEvict(cacheNames = "payment-events", key = "#id.toString()") - public PaymentEvent createEvent(Long id, PaymentEvent event) { - // Save new event - event = addEvent(event); - Assert.notNull(event, "The event could not be appended to the payment"); - - return event; - } - - /** - * Get an {@link PaymentEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link PaymentEvent} - * @return an {@link PaymentEvent} - */ - public Resource getEvent(Long id) { - return getPaymentEventResource(eventRepository.findOne(id)); - } - - /** - * Update an {@link PaymentEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link PaymentEvent} - * @param event is the {@link PaymentEvent} to update - * @return the updated {@link PaymentEvent} - */ - @CacheEvict(cacheNames = "payment-events", key = "#event.getPayment().getPaymentId().toString()") - public PaymentEvent updateEvent(Long id, PaymentEvent event) { - Assert.notNull(id); - Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId())); - - return eventRepository.save(event); - } - - /** - * Get {@link PaymentEvents} for the supplied {@link Payment} identifier. - * - * @param id is the unique identifier of the {@link Payment} - * @return a list of {@link PaymentEvent} wrapped in a hypermedia {@link PaymentEvents} resource - */ - @Cacheable(cacheNames = "payment-events", key = "#id.toString()") - public List getPaymentEvents(Long id) { - return eventRepository.findPaymentEventsByPaymentId(id, - new PageRequest(0, Integer.MAX_VALUE)).getContent(); - } - - /** - * Gets a hypermedia resource for a {@link PaymentEvent} entity. - * - * @param event is the {@link PaymentEvent} to enrich with hypermedia - * @return a hypermedia resource for the supplied {@link PaymentEvent} entity - */ - private Resource getPaymentEventResource(PaymentEvent event) { - return new Resource(event, Arrays.asList( - linkTo(PaymentController.class) - .slash("events") - .slash(event.getEventId()) - .withSelfRel(), - linkTo(PaymentController.class) - .slash("payments") - .slash(event.getPayment().getPaymentId()) - .withRel("payment"))); - } - - /** - * Add a {@link PaymentEvent} to an {@link Payment} entity. - * - * @param event is the {@link PaymentEvent} to append to an {@link Payment} entity - * @return the newly appended {@link PaymentEvent} entity - */ - @CacheEvict(cacheNames = "payment-events", key = "#event.getPayment().getPaymentId().toString()") - private PaymentEvent addEvent(PaymentEvent event) { - event = eventRepository.saveAndFlush(event); - return event; - } -} diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEvent.java b/payment/payment-web/src/main/java/demo/event/PaymentEvent.java index 9f2e37b..85387a5 100644 --- a/payment/payment-web/src/main/java/demo/event/PaymentEvent.java +++ b/payment/payment-web/src/main/java/demo/event/PaymentEvent.java @@ -2,33 +2,40 @@ package demo.event; import com.fasterxml.jackson.annotation.JsonIgnore; import demo.payment.Payment; -import demo.domain.BaseEntity; +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 PaymentEvent} tracks the type and state of events as - * applied to the {@link Payment} domain object. This event resource can be used - * to event source the aggregate state of {@link Payment}. + * The domain event {@link PaymentEvent} tracks the type and state of events as applied to the {@link Payment} domain + * object. This event resource can be used to event source the aggregate state of {@link Payment}. *

- * 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 */ @Entity -public class PaymentEvent extends BaseEntity { +@EntityListeners(AuditingEntityListener.class) +public class PaymentEvent extends Event { @Id - @GeneratedValue - private Long id; + @GeneratedValue(strategy = GenerationType.AUTO) + private Long eventId; @Enumerated(EnumType.STRING) private PaymentEventType type; - @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @OneToOne(cascade = CascadeType.MERGE, fetch = FetchType.LAZY) @JsonIgnore - private Payment payment; + private Payment entity; + + @CreatedDate + private Long createdAt; + + @LastModifiedDate + private Long lastModified; public PaymentEvent() { } @@ -37,37 +44,59 @@ public class PaymentEvent extends BaseEntity { this.type = type; } - @JsonIgnore + public PaymentEvent(PaymentEventType type, Payment 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 PaymentEventType getType() { return type; } + @Override public void setType(PaymentEventType type) { this.type = type; } - public Payment getPayment() { - return payment; - } - - public void setPayment(Payment payment) { - this.payment = payment; + @Override + public Payment getEntity() { + return entity; } @Override - public String toString() { - return "PaymentEvent{" + - "id=" + id + - ", type=" + type + - ", payment=" + payment + - "} " + super.toString(); + public void setEntity(Payment 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; } } + diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEventRepository.java b/payment/payment-web/src/main/java/demo/event/PaymentEventRepository.java new file mode 100644 index 0000000..122e137 --- /dev/null +++ b/payment/payment-web/src/main/java/demo/event/PaymentEventRepository.java @@ -0,0 +1,4 @@ +package demo.event; + +public interface PaymentEventRepository extends EventRepository { +} diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEvents.java b/payment/payment-web/src/main/java/demo/event/PaymentEvents.java deleted file mode 100644 index ff0d4cf..0000000 --- a/payment/payment-web/src/main/java/demo/event/PaymentEvents.java +++ /dev/null @@ -1,74 +0,0 @@ -package demo.event; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import demo.payment.Payment; -import demo.payment.PaymentController; -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 PaymentEvents} is a hypermedia collection of {@link PaymentEvent} resources. - * - * @author kbastani - */ -public class PaymentEvents extends Resources implements Serializable { - - private Long paymentId; - - /** - * Create a new {@link PaymentEvents} hypermedia resources collection for an {@link Payment}. - * - * @param paymentId is the unique identifier for the {@link Payment} - * @param content is the collection of {@link PaymentEvents} attached to the {@link Payment} - */ - public PaymentEvents(Long paymentId, List content) { - this(content); - this.paymentId = paymentId; - - // Add hypermedia links to resources parent - add(linkTo(PaymentController.class) - .slash("payments") - .slash(paymentId) - .slash("events") - .withSelfRel(), - linkTo(PaymentController.class) - .slash("payments") - .slash(paymentId) - .withRel("payment")); - - 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 PaymentEvents(Iterable content, Link... links) { - super(content, links); - } - - /** - * Get the {@link Payment} identifier that the {@link PaymentEvents} apply to. - * - * @return the payment identifier - */ - @JsonIgnore - public Long getPaymentId() { - return paymentId; - } -} diff --git a/payment/payment-web/src/main/java/demo/payment/Payment.java b/payment/payment-web/src/main/java/demo/payment/Payment.java index 146563a..9b8c250 100644 --- a/payment/payment-web/src/main/java/demo/payment/Payment.java +++ b/payment/payment-web/src/main/java/demo/payment/Payment.java @@ -3,11 +3,14 @@ package demo.payment; import com.fasterxml.jackson.annotation.JsonIgnore; import demo.domain.BaseEntity; import demo.event.PaymentEvent; +import org.springframework.hateoas.Link; import javax.persistence.*; import java.util.HashSet; import java.util.Set; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + /** * The {@link Payment} domain object contains information related to * a user's payment. The status of an payment is event sourced using @@ -40,11 +43,11 @@ public class Payment extends BaseEntity { } public Payment(Double amount, PaymentMethod paymentMethod) { + this(); this.amount = amount; this.paymentMethod = paymentMethod; } - @JsonIgnore public Long getPaymentId() { return id; } @@ -95,6 +98,17 @@ public class Payment extends BaseEntity { this.orderId = orderId; } + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ + @Override + public Link getId() { + return linkTo(PaymentController.class) + .slash("payments") + .slash(getPaymentId()) + .withSelfRel(); + } + @Override public String toString() { return "Payment{" + diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentController.java b/payment/payment-web/src/main/java/demo/payment/PaymentController.java index e74df77..362339d 100644 --- a/payment/payment-web/src/main/java/demo/payment/PaymentController.java +++ b/payment/payment-web/src/main/java/demo/payment/PaymentController.java @@ -1,9 +1,7 @@ package demo.payment; -import demo.event.PaymentEvent; -import demo.event.PaymentEvents; -import demo.event.EventController; -import demo.event.EventService; +import demo.event.*; +import org.springframework.hateoas.ExposesResourceFor; import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.Resource; import org.springframework.http.HttpStatus; @@ -17,12 +15,13 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @RestController @RequestMapping("/v1") +@ExposesResourceFor(Payment.class) public class PaymentController { private final PaymentService paymentService; - private final EventService eventService; + private final EventService eventService; - public PaymentController(PaymentService paymentService, EventService eventService) { + public PaymentController(PaymentService paymentService, EventService eventService) { this.paymentService = paymentService; this.eventService = eventService; } @@ -196,14 +195,8 @@ public class PaymentController { return commandResource; } - /** - * Get {@link PaymentEvents} for the supplied {@link Payment} identifier. - * - * @param id is the unique identifier of the {@link Payment} - * @return a list of {@link PaymentEvent} wrapped in a hypermedia {@link PaymentEvents} resource - */ - private PaymentEvents getPaymentEventResources(Long id) { - return new PaymentEvents(id, eventService.getPaymentEvents(id)); + private Events getPaymentEventResources(Long id) { + return eventService.find(id); } /** diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java b/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java index 5975dc4..3a75b1b 100644 --- a/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java +++ b/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java @@ -4,6 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; public interface PaymentRepository extends JpaRepository { - Payment findPaymentByOrderId(@Param("orderId") Long orderId); } diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentService.java b/payment/payment-web/src/main/java/demo/payment/PaymentService.java index 4b1a88b..3ce9e73 100644 --- a/payment/payment-web/src/main/java/demo/payment/PaymentService.java +++ b/payment/payment-web/src/main/java/demo/payment/PaymentService.java @@ -1,10 +1,9 @@ package demo.payment; -import demo.event.ConsistencyModel; import demo.event.EventService; +import demo.util.ConsistencyModel; import demo.event.PaymentEvent; import demo.event.PaymentEventType; -import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; @@ -28,13 +27,11 @@ import java.util.Objects; public class PaymentService { private final PaymentRepository paymentRepository; - private final EventService eventService; - private final CacheManager cacheManager; + private final EventService eventService; - public PaymentService(PaymentRepository paymentRepository, EventService eventService, CacheManager cacheManager) { + public PaymentService(PaymentRepository paymentRepository, EventService eventService) { this.paymentRepository = paymentRepository; this.eventService = eventService; - this.cacheManager = cacheManager; } @CacheEvict(cacheNames = "payments", key = "#payment.getPaymentId().toString()") @@ -42,18 +39,18 @@ public class PaymentService { payment = createPayment(payment); - cacheManager.getCache("payments") - .evict(payment.getPaymentId()); +// cacheManager.getCache("payments") +// .evict(payment.getPaymentId()); // Trigger the payment creation event PaymentEvent event = appendEvent(payment.getPaymentId(), new PaymentEvent(PaymentEventType.PAYMENT_CREATED)); // Attach payment identifier - event.getPayment().setPaymentId(payment.getPaymentId()); + event.getEntity().setPaymentId(payment.getPaymentId()); // Return the result - return event.getPayment(); + return event.getEntity(); } /** @@ -131,7 +128,7 @@ public class PaymentService { * @return the newly appended {@link PaymentEvent} */ public PaymentEvent appendEvent(Long paymentId, PaymentEvent event) { - return appendEvent(paymentId, event, ConsistencyModel.ACID); + return appendEvent(paymentId, event, ConsistencyModel.BASE); } /** @@ -142,13 +139,27 @@ public class PaymentService { * @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"); - event.setPayment(payment); - event = eventService.createEvent(paymentId, event); + + event.setEntity(payment); + event = eventService.save(paymentId, event); + payment.getEvents().add(event); paymentRepository.saveAndFlush(payment); - eventService.raiseEvent(event, consistencyModel); + + // Raise the event using the supplied consistency model + switch (consistencyModel) { + case BASE: + eventService.sendAsync(event); + break; + case ACID: + event = eventService.send(event); + break; + } + return event; } diff --git a/payment/payment-web/src/main/java/demo/event/ConsistencyModel.java b/payment/payment-web/src/main/java/demo/util/ConsistencyModel.java similarity index 72% rename from payment/payment-web/src/main/java/demo/event/ConsistencyModel.java rename to payment/payment-web/src/main/java/demo/util/ConsistencyModel.java index 8bef081..b017f9d 100644 --- a/payment/payment-web/src/main/java/demo/event/ConsistencyModel.java +++ b/payment/payment-web/src/main/java/demo/util/ConsistencyModel.java @@ -1,4 +1,4 @@ -package demo.event; +package demo.util; public enum ConsistencyModel { BASE, diff --git a/payment/payment-web/src/main/resources/application.yml b/payment/payment-web/src/main/resources/application.yml index 42ae822..5e66ac4 100644 --- a/payment/payment-web/src/main/resources/application.yml +++ b/payment/payment-web/src/main/resources/application.yml @@ -14,4 +14,4 @@ spring: host: localhost port: 6379 server: - port: 8080 \ No newline at end of file + port: 8082 \ No newline at end of file diff --git a/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java b/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java new file mode 100644 index 0000000..8b338c5 --- /dev/null +++ b/payment/payment-web/src/test/java/demo/payment/EventServiceTests.java @@ -0,0 +1,29 @@ +package demo.payment; + +import demo.event.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.Assert; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class EventServiceTests { + + @Autowired + private PaymentRepository paymentRepository; + + @Autowired + private EventService eventService; + + @Test + public void getPaymentReturnsPayment() throws Exception { + Payment payment = new Payment(11.0, PaymentMethod.CREDIT_CARD); + payment = paymentRepository.saveAndFlush(payment); + eventService.save(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment)); + Events events = eventService.find(payment.getPaymentId()); + Assert.notNull(events); + } +} \ No newline at end of file diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java index 82d4668..e709f44 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java @@ -1,6 +1,7 @@ package demo.payment; import demo.event.EventService; +import demo.event.PaymentEvent; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -26,7 +27,7 @@ public class PaymentControllerTest { private PaymentService paymentService; @MockBean - private EventService eventService; + private EventService eventService; @Test public void getUserPaymentResourceShouldReturnPayment() throws Exception { diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java index e14a6e3..72f50ef 100644 --- a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java +++ b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java @@ -1,11 +1,11 @@ package demo.payment; import demo.event.EventService; +import demo.event.PaymentEvent; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.cache.CacheManager; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -15,19 +15,16 @@ import static org.mockito.BDDMockito.given; public class PaymentServiceTests { @MockBean - private EventService eventService; + private EventService eventService; @MockBean private PaymentRepository paymentRepository; - @MockBean - private CacheManager cacheManager; - private PaymentService paymentService; @Before public void before() { - paymentService = new PaymentService(paymentRepository, eventService, cacheManager); + paymentService = new PaymentService(paymentRepository, eventService); } @Test diff --git a/payment/payment-worker/src/main/java/demo/event/PaymentEventStream.java b/payment/payment-worker/src/main/java/demo/event/PaymentEventStream.java index 78173cd..acc15c4 100644 --- a/payment/payment-worker/src/main/java/demo/event/PaymentEventStream.java +++ b/payment/payment-worker/src/main/java/demo/event/PaymentEventStream.java @@ -34,7 +34,7 @@ public class PaymentEventStream { * @param paymentEvent is the {@link Payment} domain event to process */ @StreamListener(Sink.INPUT) - public void streamListerner(PaymentEvent paymentEvent) { + public void streamListener(PaymentEvent paymentEvent) { eventService.apply(paymentEvent); } } diff --git a/payment/payment-worker/src/main/java/demo/payment/Payment.java b/payment/payment-worker/src/main/java/demo/payment/Payment.java index c5f10c2..2ecce59 100644 --- a/payment/payment-worker/src/main/java/demo/payment/Payment.java +++ b/payment/payment-worker/src/main/java/demo/payment/Payment.java @@ -4,6 +4,7 @@ import demo.domain.BaseEntity; public class Payment extends BaseEntity { + private Long paymentId; private Double amount; private PaymentMethod paymentMethod; private PaymentStatus status; @@ -11,6 +12,14 @@ public class Payment extends BaseEntity { public Payment() { } + public Long getPaymentId() { + return paymentId; + } + + public void setPaymentId(Long paymentId) { + this.paymentId = paymentId; + } + public PaymentStatus getStatus() { return status; } diff --git a/payment/payment-worker/src/main/resources/application.yml b/payment/payment-worker/src/main/resources/application.yml index f3fe334..becdec6 100644 --- a/payment/payment-worker/src/main/resources/application.yml +++ b/payment/payment-worker/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: consumer: durableSubscription: true server: - port: 8081 + port: 8083 amazon: aws: access-key-id: replace diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index 4b21d6a..c9da68d 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -17,7 +17,14 @@ ../ + + UTF-8 + UTF-8 + 1.8 + + spring-boot-starter-aws-lambda + spring-boot-starter-data-events \ No newline at end of file diff --git a/spring-boot-starters/spring-boot-starter-data-events/README.md b/spring-boot-starters/spring-boot-starter-data-events/README.md new file mode 100644 index 0000000..571401e --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/README.md @@ -0,0 +1,121 @@ +# Spring Boot Starter Data Events + +This starter project provides auto-configuration support classes for building event-driven Spring Data applications. + +* Uses a familiar _Spring Data_ repository pattern for creating an `EventRepository` +* The `EventRepository` provides trait specific features for managing an event log that is attached to an existing domain entity +* Provides a set of event abstractions that can be extended to use any Spring Data repository (JPA, Mongo, Neo4j, Redis..) +* Provides an `EventService` bean that can be used to publish events to a _Spring Cloud Stream_ output channel + +## Usage + +In your Spring Boot project, add the starter project dependency to your class path. For Maven, add the following dependency to your `pom.xml`. + +```xml + + + org.kbastani + spring-boot-starter-data-events + 1.0-SNAPSHOT + + ... + +``` + +Next, configure your _Spring Cloud Stream_ output bindings. Add the following snippet to the `application.properties|yaml` file of your Spring Boot application. Replace the destination value with the name of your message channel for the event stream. + +```yaml +spring: + cloud: + stream: + bindings: + output: + destination: payment +``` + +Next, you'll need to create a custom `Event` entity. The snippet below extends the provided `Event` interface. This example uses Spring Data JPA, but you can use any Spring Data project for implementing your event entities. + +```java +@Entity +@EntityListeners(AuditingEntityListener.class) +public class PaymentEvent extends Event { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long eventId; + + @Enumerated(EnumType.STRING) + private PaymentEventType type; + + @OneToOne(cascade = CascadeType.MERGE, fetch = FetchType.LAZY) + @JsonIgnore + private Payment entity; + + @CreatedDate + private Long createdAt; + + @LastModifiedDate + private Long lastModified; + + ... +} +``` + +To start managing events you'll need to extend the `EventRepository` interface. The `PaymentEvent` is the JPA entity we defined in the last snippet. + +```java +public interface PaymentEventRepository extends EventRepository { +} +``` + +That's it! You're ready to start sending domain events to the stream binding's output channel using the auto-configured `EventService`. The example snippet below shows how to create and append a new `PaymentEvent` to a `Payment` entity before publishing the event over AMQP to the configured event stream's output channel. + +```java +@Service +public class PaymentService { + private final EventService eventService; + + public PaymentController(EventService eventService) { + this.eventService = eventService; + } + + public PaymentEvent appendCreateEvent(Payment payment) { + PaymentEvent paymentEvent = new PaymentEvent(PaymentEventType.PAYMENT_CREATED); + paymentEvent.setEntity(payment); + paymentEvent = eventService.save(event); + + // Send the event to the Spring Cloud stream binding + eventService.sendAsync(paymentEvent); + } + + ... +} + +``` + +A default `EventController` is also provided with the starter project. The `EventController` provides a basic REST API with hypermedia resource support for managing the `Event` log of a domain entity over HTTP. The following cURL snippet gets the `PaymentEvent` we created in the last example from the `EventController`. + +```bash +curl -X GET "http://localhost:8082/v1/events/1" +``` + +Response: + +```json +{ + "eventId": 1, + "type": "PAYMENT_CREATED", + "createdAt": 1482749707006, + "lastModified": 1482749707006, + "_links": { + "self": { + "href": "http://localhost:8082/v1/events/1" + }, + "payment": { + "href": "http://localhost:8082/v1/payments/1" + } + } +} +``` + +In the snippet above we can see the `EventController` responded with a `hal+json` formatted resource. Since the `PaymentEvent` has a reference to the `Payment` entity, we see a _payment_ link is available to fetch the related resource. \ No newline at end of file diff --git a/spring-boot-starters/spring-boot-starter-data-events/pom.xml b/spring-boot-starters/spring-boot-starter-data-events/pom.xml new file mode 100755 index 0000000..04f92f4 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + spring-boot-starter-data-events + jar + + + org.kbastani + spring-boot-starters + 1.0-SNAPSHOT + ../ + + + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.cloud + spring-cloud-stream + + + + org.springframework.data + spring-data-commons + + + diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java new file mode 100644 index 0000000..c5e2abe --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Event.java @@ -0,0 +1,69 @@ +package demo.event; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.core.EvoInflectorRelProvider; +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +/** + * Abstract implementation of the {@link Event} entity. + * + * @param is the entity this {@link Event} applies to + * @param is the type of event, typically an {@link Enum} + * @param is the unique identifier type used to persist the {@link Event} + * @author Kenny Bastani + * @see org.springframework.stereotype.Repository + * @see ResourceSupport + */ +public abstract class Event extends ResourceSupport { + + public Event() { + } + + public abstract ID getEventId(); + + public abstract void setEventId(ID eventId); + + public abstract E getType(); + + public abstract void setType(E type); + + public abstract T getEntity(); + + public abstract void setEntity(T entity); + + public abstract Long getCreatedAt(); + + public abstract void setCreatedAt(Long createdAt); + + public abstract Long getLastModified(); + + public abstract void setLastModified(Long lastModified); + + @Override + @SuppressWarnings("unchecked") + public List getLinks() { + List links = super.getLinks().stream().collect(Collectors.toList()); + links.add(getId()); + Class clazz = (Class) ((ParameterizedTypeImpl) + this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + links.add(getEntity().getId().withRel(new EvoInflectorRelProvider().getItemResourceRelFor(clazz))); + return links; + } + + @Override + public String toString() { + return String.format("links: %s", getLinks().toString()); + } + + @Override + public Link getId() { + return linkTo(EventController.class).slash("events").slash(getEventId()).withSelfRel(); + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventAutoConfig.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventAutoConfig.java new file mode 100755 index 0000000..afa3fa1 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventAutoConfig.java @@ -0,0 +1,35 @@ +package demo.event; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * This class auto-configures a {@link EventServiceImpl} bean. + * + * @author Kenny Bastani + */ +@Configuration +@ConditionalOnClass({ EventRepository.class, Source.class, RestTemplate.class }) +@EnableConfigurationProperties(EventProperties.class) +public class EventAutoConfig { + + private EventRepository eventRepository; + private Source source; + private RestTemplate restTemplate; + + public EventAutoConfig(EventRepository eventRepository, Source source, RestTemplate restTemplate) { + this.eventRepository = eventRepository; + this.source = source; + this.restTemplate = restTemplate; + } + + @SuppressWarnings("unchecked") + @Bean + public EventService eventService() { + return new EventServiceImpl(eventRepository, source, restTemplate); + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventController.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventController.java new file mode 100644 index 0000000..1a509ee --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventController.java @@ -0,0 +1,45 @@ +package demo.event; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.Serializable; +import java.util.Optional; + +/** + * The default controller for managing {@link Event} entities. + * + * @author Kenny Bastani + */ +@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 T event, @PathVariable Long id) { + return Optional.ofNullable(eventService.save(id, event)) + .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) + .orElseThrow(() -> new RuntimeException("Event creation failed")); + } + + @PutMapping(path = "/events/{id}") + public ResponseEntity updateEvent(@RequestBody T event, @PathVariable Long id) { + return Optional.ofNullable(eventService.save(id, event)) + .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + .orElseThrow(() -> new RuntimeException("Event update failed")); + } + + @GetMapping(path = "/events/{id}") + public ResponseEntity getEvent(@PathVariable Long id) { + return Optional.ofNullable(eventService.findOne(id)) + .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventProperties.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventProperties.java new file mode 100755 index 0000000..e11d5d8 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventProperties.java @@ -0,0 +1,25 @@ +package demo.event; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "event") +public class EventProperties { + + @NestedConfigurationProperty + private Props props; + + public Props getProps() { + return props; + } + + public void setProps(Props props) { + this.props = props; + } + + public static class Props { + // TODO: Implement + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventRepository.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventRepository.java new file mode 100644 index 0000000..98dc3f4 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventRepository.java @@ -0,0 +1,21 @@ +package demo.event; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; + +import java.io.Serializable; + +/** + * Extension of {@link PagingAndSortingRepository} to provide additional support for persisting event logs to entities. + * + * @author Kenny Bastani + * @see Event + * @see EventService + */ +@NoRepositoryBean +public interface EventRepository extends PagingAndSortingRepository { + Page findEventsByEntityId(@Param("entityId") ID entityId, Pageable pageable); +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java new file mode 100644 index 0000000..1a73a45 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventService.java @@ -0,0 +1,72 @@ +package demo.event; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceSupport; + +import java.io.Serializable; + +/** + * Service interface for managing {@link Event} entities. + * + * @author Kenny Bastani + * @see Event + * @see Events + * @see EventServiceImpl + */ +public interface EventService { + + /** + * Raises a synchronous domain event. An {@link Event} will be applied to an entity through a chain of HTTP + * requests/responses. + * + * @param event + * @param links + * @return the applied {@link Event} + */ + S send(S event, Link... links); + + /** + * Raises an asynchronous domain event. An {@link Event} will be applied to an entity through a chain of AMQP + * messages. + * + * @param event + * @param links + * @return a flag indicating if the {@link Event} message was sent successfully + */ + Boolean sendAsync(S event, Link... links); + + /** + * Saves a given event entity. Use the returned instance for further operations as the save operation might have + * changed the entity instance completely. + * + * @param event + * @return the saved event entity + */ + S save(S event); + + /** + * Saves a given event entity. Use the returned instance for further operations as the save operation might have + * changed the entity instance completely. The {@link ID} parameter is the unique {@link Event} identifier. + * + * @param id + * @param event + * @return the saved event entity + */ + S save(ID id, S event); + + /** + * Retrieves an {@link Event} entity by its id. + * + * @param id + * @return the {@link Event} entity with the given id or {@literal null} if none found + */ + T findOne(EID id); + + /** + * Retrieves an entity's {@link Event}s by its id. + * + * @param entityId + * @return a {@link Events} containing a collection of {@link Event}s + */ + E find(ID entityId); +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java new file mode 100755 index 0000000..3eeec65 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/EventServiceImpl.java @@ -0,0 +1,80 @@ +package demo.event; + +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.RequestEntity; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.web.client.RestTemplate; + +import java.io.Serializable; +import java.net.URI; + +/** + * Event service implementation of {@link EventService} for managing {@link Event} entities. + * + * @author Kenny Bastani + * @see Event + * @see Events + * @see EventService + */ +@SuppressWarnings("unchecked") +class EventServiceImpl implements EventService { + + private static final Logger log = Logger.getLogger(EventServiceImpl.class); + private static final String EVENT_PROCESSOR_URL = "http://localhost:8083/v1/events"; + + private final EventRepository eventRepository; + private final Source eventStream; + private final RestTemplate restTemplate; + + EventServiceImpl(EventRepository eventRepository, Source eventStream, RestTemplate restTemplate) { + this.eventRepository = eventRepository; + this.eventStream = eventStream; + this.restTemplate = restTemplate; + } + + public S send(S event, Link... links) { + // Assemble request to the event stream processor + RequestEntity> requestEntity = RequestEntity.post(URI.create(EVENT_PROCESSOR_URL)) + .contentType(MediaTypes.HAL_JSON).body(new Resource(event), Resource.class); + + try { + // Send the event to the event stream processor + E entity = (E) restTemplate.exchange(requestEntity, event.getEntity().getClass()).getBody(); + // Set the applied entity reference to the event + event.setEntity(entity); + } catch (Exception ex) { + log.error(ex); + } + + return event; + } + + public Boolean sendAsync(S event, Link... links) { + return eventStream.output().send(MessageBuilder.withPayload(event).build()); + } + + public S save(S event) { + event = eventRepository.save(event); + return event; + } + + public S save(ID id, S event) { + event.setEventId(id); + return save(event); + } + + public T findOne(S id) { + return eventRepository.findOne(id); + } + + public E find(ID entityId) { + return (E) new Events(entityId, eventRepository.findEventsByEntityId(entityId, + new PageRequest(0, Integer.MAX_VALUE)).getContent()); + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java new file mode 100644 index 0000000..9513e4e --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/event/Events.java @@ -0,0 +1,33 @@ +package demo.event; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.Resources; + +import java.io.Serializable; +import java.util.List; + +/** + * General helper to easily create a wrapper for a collection of {@link Event} entities. + * + * @author Kenny Bastani + */ +public class Events extends Resources> { + + private ID entityId; + + public Events(ID entityId, List> content) { + this(content); + this.entityId = entityId; + } + + public Events(Iterable> content, Link... links) { + super(content, links); + } + + @JsonIgnore + public ID getEntityId() { + return entityId; + } +} diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100755 index 0000000..7db2067 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + { + "name": "event", + "type": "demo.event.EventProperties", + "sourceType": "demo.event.EventProperties" + } + ] +} \ No newline at end of file diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring.factories b/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring.factories new file mode 100755 index 0000000..ad5f396 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=demo.event.EventAutoConfig \ No newline at end of file diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/application.properties b/spring-boot-starters/spring-boot-starter-data-events/src/main/resources/application.properties new file mode 100755 index 0000000..e69de29 diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/event/ConfigurationTest.java b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/event/ConfigurationTest.java new file mode 100755 index 0000000..11d401a --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-data-events/src/test/java/demo/event/ConfigurationTest.java @@ -0,0 +1,39 @@ +package demo.event; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static junit.framework.TestCase.assertNotNull; + +public class ConfigurationTest { + + private AnnotationConfigApplicationContext context; + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void contextLoads() { + load(EmptyConfiguration.class); + assertNotNull(context); + } + + @Configuration + static class EmptyConfiguration { + } + + private void load(Class config, String... environment) { + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(applicationContext, environment); + applicationContext.register(config); + applicationContext.refresh(); + this.context = applicationContext; + } +}