Pattern language for mapping commands to domain model
This commit is contained in:
@@ -57,6 +57,10 @@
|
|||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package demo.domain;
|
|||||||
import org.springframework.data.annotation.CreatedDate;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.springframework.data.annotation.LastModifiedDate;
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
import org.springframework.hateoas.ResourceSupport;
|
|
||||||
|
|
||||||
import javax.persistence.EntityListeners;
|
import javax.persistence.EntityListeners;
|
||||||
import javax.persistence.MappedSuperclass;
|
import javax.persistence.MappedSuperclass;
|
||||||
@@ -11,7 +10,9 @@ import java.io.Serializable;
|
|||||||
|
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class BaseEntity extends ResourceSupport implements Serializable {
|
public class AbstractEntity<T extends Serializable> extends Aggregate<T> implements Serializable {
|
||||||
|
|
||||||
|
private T identity;
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
private Long createdAt;
|
private Long createdAt;
|
||||||
@@ -19,7 +20,7 @@ public class BaseEntity extends ResourceSupport implements Serializable {
|
|||||||
@LastModifiedDate
|
@LastModifiedDate
|
||||||
private Long lastModified;
|
private Long lastModified;
|
||||||
|
|
||||||
public BaseEntity() {
|
public AbstractEntity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getCreatedAt() {
|
public Long getCreatedAt() {
|
||||||
@@ -38,6 +39,15 @@ public class BaseEntity extends ResourceSupport implements Serializable {
|
|||||||
this.lastModified = lastModified;
|
this.lastModified = lastModified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T getIdentity() {
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentity(T id) {
|
||||||
|
this.identity = id;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "BaseEntity{" +
|
return "BaseEntity{" +
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
package demo.payment;
|
package demo.payment;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import demo.domain.BaseEntity;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import demo.domain.AbstractEntity;
|
||||||
|
import demo.domain.Command;
|
||||||
import demo.event.PaymentEvent;
|
import demo.event.PaymentEvent;
|
||||||
|
import demo.payment.action.ConnectOrder;
|
||||||
|
import demo.payment.action.ProcessPayment;
|
||||||
|
import demo.payment.controller.PaymentController;
|
||||||
import org.springframework.hateoas.Link;
|
import org.springframework.hateoas.Link;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
@@ -18,7 +23,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
|||||||
* @author Kenny Bastani
|
* @author Kenny Bastani
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
public class Payment extends BaseEntity {
|
public class Payment extends AbstractEntity<Long> {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
@@ -41,16 +46,18 @@ public class Payment extends BaseEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Payment(Double amount, PaymentMethod paymentMethod) {
|
public Payment(Double amount, PaymentMethod paymentMethod) {
|
||||||
this();
|
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.paymentMethod = paymentMethod;
|
this.paymentMethod = paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getPaymentId() {
|
@JsonProperty("paymentId")
|
||||||
return id;
|
@Override
|
||||||
|
public Long getIdentity() {
|
||||||
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPaymentId(Long id) {
|
@Override
|
||||||
|
public void setIdentity(Long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +103,24 @@ public class Payment extends BaseEntity {
|
|||||||
this.orderId = orderId;
|
this.orderId = orderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Command(method = "connectOrder", controller = PaymentController.class)
|
||||||
|
public Payment connectOrder(Long orderId) {
|
||||||
|
getAction(ConnectOrder.class)
|
||||||
|
.getConsumer()
|
||||||
|
.accept(this, orderId);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(method = "processPayment", controller = PaymentController.class)
|
||||||
|
public Payment processPayment() {
|
||||||
|
getAction(ProcessPayment.class)
|
||||||
|
.getConsumer()
|
||||||
|
.accept(this);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
|
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
|
||||||
*/
|
*/
|
||||||
@@ -103,16 +128,9 @@ public class Payment extends BaseEntity {
|
|||||||
public Link getId() {
|
public Link getId() {
|
||||||
return linkTo(PaymentController.class)
|
return linkTo(PaymentController.class)
|
||||||
.slash("payments")
|
.slash("payments")
|
||||||
.slash(getPaymentId())
|
.slash(getIdentity())
|
||||||
.withSelfRel();
|
.withSelfRel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Payment{" +
|
|
||||||
"id=" + id +
|
|
||||||
", events=" + events +
|
|
||||||
", status=" + status +
|
|
||||||
"} " + super.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package demo.payment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link PaymentCommand} represents an action that can be performed to an {@link Payment} aggregate. Commands
|
|
||||||
* initiate an action that can mutate the state of an payment entity as it transitions between {@link PaymentStatus}
|
|
||||||
* values.
|
|
||||||
*
|
|
||||||
* @author Kenneth Bastani
|
|
||||||
*/
|
|
||||||
public enum PaymentCommand {
|
|
||||||
CONNECT_ORDER,
|
|
||||||
PROCESS_PAYMENT
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package demo.payment;
|
|
||||||
|
|
||||||
import org.springframework.hateoas.ResourceSupport;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hypermedia resource that describes the collection of commands that can be applied to a {@link Payment} aggregate.
|
|
||||||
*
|
|
||||||
* @author Kenny Bastani
|
|
||||||
*/
|
|
||||||
public class PaymentCommands extends ResourceSupport {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package demo.payment;
|
||||||
|
|
||||||
|
import demo.domain.Provider;
|
||||||
|
import demo.domain.Service;
|
||||||
|
|
||||||
|
@org.springframework.stereotype.Service
|
||||||
|
public class PaymentProvider extends Provider<Payment> {
|
||||||
|
|
||||||
|
private final PaymentService paymentService;
|
||||||
|
|
||||||
|
public PaymentProvider(PaymentService paymentService) {
|
||||||
|
this.paymentService = paymentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Service<? extends Payment> getDefaultService() {
|
||||||
|
return paymentService;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package demo.payment;
|
package demo.payment;
|
||||||
|
|
||||||
|
import demo.domain.Service;
|
||||||
import demo.event.EventService;
|
import demo.event.EventService;
|
||||||
import demo.event.PaymentEvent;
|
import demo.event.PaymentEvent;
|
||||||
import demo.event.PaymentEventType;
|
import demo.event.PaymentEventType;
|
||||||
import demo.util.ConsistencyModel;
|
import demo.util.ConsistencyModel;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -17,8 +17,8 @@ import java.util.Objects;
|
|||||||
*
|
*
|
||||||
* @author Kenny Bastani
|
* @author Kenny Bastani
|
||||||
*/
|
*/
|
||||||
@Service
|
@org.springframework.stereotype.Service
|
||||||
public class PaymentService {
|
public class PaymentService extends Service<Payment> {
|
||||||
|
|
||||||
private final PaymentRepository paymentRepository;
|
private final PaymentRepository paymentRepository;
|
||||||
private final EventService<PaymentEvent, Long> eventService;
|
private final EventService<PaymentEvent, Long> eventService;
|
||||||
@@ -33,11 +33,11 @@ public class PaymentService {
|
|||||||
payment = createPayment(payment);
|
payment = createPayment(payment);
|
||||||
|
|
||||||
// Trigger the payment creation event
|
// Trigger the payment creation event
|
||||||
PaymentEvent event = appendEvent(payment.getPaymentId(),
|
PaymentEvent event = appendEvent(payment.getIdentity(),
|
||||||
new PaymentEvent(PaymentEventType.PAYMENT_CREATED));
|
new PaymentEvent(PaymentEventType.PAYMENT_CREATED));
|
||||||
|
|
||||||
// Attach payment identifier
|
// Attach payment identifier
|
||||||
event.getEntity().setPaymentId(payment.getPaymentId());
|
event.getEntity().setIdentity(payment.getIdentity());
|
||||||
|
|
||||||
// Return the result
|
// Return the result
|
||||||
return event.getEntity();
|
return event.getEntity();
|
||||||
@@ -78,11 +78,11 @@ public class PaymentService {
|
|||||||
Assert.notNull(id, "Payment id must be present in the resource URL");
|
Assert.notNull(id, "Payment id must be present in the resource URL");
|
||||||
Assert.notNull(payment, "Payment request body cannot be null");
|
Assert.notNull(payment, "Payment request body cannot be null");
|
||||||
|
|
||||||
if (payment.getPaymentId() != null) {
|
if (payment.getIdentity() != null) {
|
||||||
Assert.isTrue(Objects.equals(id, payment.getPaymentId()),
|
Assert.isTrue(Objects.equals(id, payment.getIdentity()),
|
||||||
"The payment id in the request body must match the resource URL");
|
"The payment id in the request body must match the resource URL");
|
||||||
} else {
|
} else {
|
||||||
payment.setPaymentId(id);
|
payment.setIdentity(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.state(paymentRepository.exists(id),
|
Assert.state(paymentRepository.exists(id),
|
||||||
@@ -150,22 +150,4 @@ public class PaymentService {
|
|||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply an {@link PaymentCommand} to the {@link Payment} with a specified identifier.
|
|
||||||
*
|
|
||||||
* @param id is the unique identifier of the {@link Payment}
|
|
||||||
* @param paymentCommand is the command to apply to the {@link Payment}
|
|
||||||
* @return a hypermedia resource containing the updated {@link Payment}
|
|
||||||
*/
|
|
||||||
public Payment applyCommand(Long id, PaymentCommand paymentCommand) {
|
|
||||||
Payment payment = getPayment(id);
|
|
||||||
Assert.notNull(payment, "The payment for the supplied id could not be found");
|
|
||||||
|
|
||||||
PaymentStatus status = payment.getStatus();
|
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
|
|
||||||
return payment;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package demo.payment.action;
|
||||||
|
|
||||||
|
import demo.domain.Action;
|
||||||
|
import demo.payment.Payment;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ConnectOrder extends Action<Payment> {
|
||||||
|
public BiConsumer<Payment, Long> getConsumer() {
|
||||||
|
return Payment::setOrderId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package demo.payment.action;
|
||||||
|
|
||||||
|
import demo.domain.Action;
|
||||||
|
import demo.payment.Payment;
|
||||||
|
import demo.payment.PaymentStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ProcessPayment extends Action<Payment> {
|
||||||
|
public Consumer<Payment> getConsumer() {
|
||||||
|
return payment -> payment.setStatus(PaymentStatus.PAYMENT_PROCESSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
package demo.payment;
|
package demo.payment.controller;
|
||||||
|
|
||||||
import demo.event.*;
|
import demo.event.EventController;
|
||||||
|
import demo.event.EventService;
|
||||||
|
import demo.event.Events;
|
||||||
|
import demo.event.PaymentEvent;
|
||||||
|
import demo.payment.Payment;
|
||||||
|
import demo.payment.PaymentService;
|
||||||
import org.springframework.hateoas.ExposesResourceFor;
|
import org.springframework.hateoas.ExposesResourceFor;
|
||||||
import org.springframework.hateoas.LinkBuilder;
|
import org.springframework.hateoas.LinkBuilder;
|
||||||
import org.springframework.hateoas.Resource;
|
import org.springframework.hateoas.Resource;
|
||||||
|
import org.springframework.hateoas.ResourceSupport;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||||
@@ -31,68 +38,68 @@ public class PaymentController {
|
|||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/payments")
|
@RequestMapping(path = "/payments", method = RequestMethod.POST)
|
||||||
public ResponseEntity createPayment(@RequestBody Payment payment) {
|
public ResponseEntity createPayment(@RequestBody Payment payment) {
|
||||||
return Optional.ofNullable(createPaymentResource(payment))
|
return Optional.ofNullable(createPaymentResource(payment))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
||||||
.orElseThrow(() -> new RuntimeException("Payment creation failed"));
|
.orElseThrow(() -> new RuntimeException("Payment creation failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(path = "/payments/{id}")
|
@RequestMapping(path = "/payments/{id}", method = RequestMethod.PUT)
|
||||||
public ResponseEntity updatePayment(@RequestBody Payment payment, @PathVariable Long id) {
|
public ResponseEntity updatePayment(@RequestBody Payment payment, @PathVariable Long id) {
|
||||||
return Optional.ofNullable(updatePaymentResource(id, payment))
|
return Optional.ofNullable(updatePaymentResource(id, payment))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||||
.orElseThrow(() -> new RuntimeException("Payment update failed"));
|
.orElseThrow(() -> new RuntimeException("Payment update failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/payments/{id}")
|
@RequestMapping(path = "/payments/{id}", method = RequestMethod.GET)
|
||||||
public ResponseEntity getPayment(@PathVariable Long id) {
|
public ResponseEntity getPayment(@PathVariable Long id) {
|
||||||
return Optional.ofNullable(getPaymentResource(id))
|
return Optional.ofNullable(getPaymentResource(id))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||||
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
|
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(path = "/payments/{id}")
|
@RequestMapping(path = "/payments/{id}", method = RequestMethod.DELETE)
|
||||||
public ResponseEntity deletePayment(@PathVariable Long id) {
|
public ResponseEntity deletePayment(@PathVariable Long id) {
|
||||||
return Optional.ofNullable(paymentService.deletePayment(id))
|
return Optional.ofNullable(paymentService.deletePayment(id))
|
||||||
.map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
|
.map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
|
||||||
.orElseThrow(() -> new RuntimeException("Payment deletion failed"));
|
.orElseThrow(() -> new RuntimeException("Payment deletion failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/payments/{id}/events")
|
@RequestMapping(path = "/payments/{id}/events", method = RequestMethod.GET)
|
||||||
public ResponseEntity getPaymentEvents(@PathVariable Long id) {
|
public ResponseEntity getPaymentEvents(@PathVariable Long id) {
|
||||||
return Optional.of(getPaymentEventResources(id))
|
return Optional.ofNullable(getPaymentEventResources(id))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||||
.orElseThrow(() -> new RuntimeException("Could not get payment events"));
|
.orElseThrow(() -> new RuntimeException("Could not get payment events"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/payments/{id}/events")
|
@RequestMapping(path = "/payments/{id}/events", method = RequestMethod.POST)
|
||||||
public ResponseEntity createPayment(@PathVariable Long id, @RequestBody PaymentEvent event) {
|
public ResponseEntity createPayment(@PathVariable Long id, @RequestBody PaymentEvent event) {
|
||||||
return Optional.ofNullable(appendEventResource(id, event))
|
return Optional.ofNullable(appendEventResource(id, event))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
|
||||||
.orElseThrow(() -> new RuntimeException("Append payment event failed"));
|
.orElseThrow(() -> new RuntimeException("Append payment event failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/payments/{id}/commands")
|
@RequestMapping(path = "/payments/{id}/commands")
|
||||||
public ResponseEntity getPaymentCommands(@PathVariable Long id) {
|
public ResponseEntity getCommands(@PathVariable Long id) {
|
||||||
return Optional.ofNullable(getCommandsResource(id))
|
return Optional.ofNullable(getCommandsResources(id))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
||||||
.orElseThrow(() -> new RuntimeException("The payment could not be found"));
|
.orElseThrow(() -> new RuntimeException("The payment could not be found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/payments/{id}/commands/connectOrder")
|
@RequestMapping(path = "/payments/{id}/commands/connectOrder")
|
||||||
public ResponseEntity connectOrder(@PathVariable Long id) {
|
public ResponseEntity connectOrder(@PathVariable Long id, @RequestParam(value = "orderId") Long orderId) {
|
||||||
return Optional.ofNullable(getPaymentResource(
|
return Optional.of(paymentService.getPayment(id)
|
||||||
paymentService.applyCommand(id, PaymentCommand.CONNECT_ORDER)))
|
.connectOrder(orderId))
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK))
|
||||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/payments/{id}/commands/processPayment")
|
@RequestMapping(path = "/payments/{id}/commands/processPayment")
|
||||||
public ResponseEntity processPayment(@PathVariable Long id) {
|
public ResponseEntity processPayment(@PathVariable Long id) {
|
||||||
return Optional.ofNullable(getPaymentResource(
|
return Optional.of(paymentService.getPayment(id)
|
||||||
paymentService.applyCommand(id, PaymentCommand.PROCESS_PAYMENT)))
|
.processPayment())
|
||||||
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
|
.map(e -> new ResponseEntity<>(getPaymentResource(e), HttpStatus.OK))
|
||||||
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,16 +110,10 @@ public class PaymentController {
|
|||||||
* @return a hypermedia resource for the fetched {@link Payment}
|
* @return a hypermedia resource for the fetched {@link Payment}
|
||||||
*/
|
*/
|
||||||
private Resource<Payment> getPaymentResource(Long id) {
|
private Resource<Payment> getPaymentResource(Long id) {
|
||||||
Resource<Payment> paymentResource = null;
|
|
||||||
|
|
||||||
// Get the payment for the provided id
|
// Get the payment for the provided id
|
||||||
Payment payment = paymentService.getPayment(id);
|
Payment payment = paymentService.getPayment(id);
|
||||||
|
|
||||||
// If the payment exists, wrap the hypermedia response
|
return getPaymentResource(payment);
|
||||||
if (payment != null)
|
|
||||||
paymentResource = getPaymentResource(payment);
|
|
||||||
|
|
||||||
return paymentResource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,50 +171,20 @@ public class PaymentController {
|
|||||||
return eventResource;
|
return eventResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the {@link PaymentCommand} hypermedia resource that lists the available commands that can be applied to a
|
|
||||||
* {@link Payment} entity.
|
|
||||||
*
|
|
||||||
* @param id is the {@link Payment} identifier to provide command links for
|
|
||||||
* @return an {@link PaymentCommands} with a collection of embedded command links
|
|
||||||
*/
|
|
||||||
private PaymentCommands getCommandsResource(Long id) {
|
|
||||||
// Get the payment resource for the identifier
|
|
||||||
Resource<Payment> paymentResource = getPaymentResource(id);
|
|
||||||
|
|
||||||
// Create a new payment commands hypermedia resource
|
|
||||||
PaymentCommands commandResource = new PaymentCommands();
|
|
||||||
|
|
||||||
// Add payment command hypermedia links
|
|
||||||
if (paymentResource != null) {
|
|
||||||
commandResource.add(
|
|
||||||
getCommandLinkBuilder(id)
|
|
||||||
.slash("connectOrder")
|
|
||||||
.withRel("connectOrder"),
|
|
||||||
getCommandLinkBuilder(id)
|
|
||||||
.slash("processPayment")
|
|
||||||
.withRel("processPayment")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return commandResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Events getPaymentEventResources(Long id) {
|
private Events getPaymentEventResources(Long id) {
|
||||||
return eventService.find(id);
|
return eventService.find(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private LinkBuilder linkBuilder(String name, Long id) {
|
||||||
* Generate a {@link LinkBuilder} for generating the {@link PaymentCommands}.
|
Method method;
|
||||||
*
|
|
||||||
* @param id is the unique identifier for a {@link Payment}
|
try {
|
||||||
* @return a {@link LinkBuilder} for the {@link PaymentCommands}
|
method = PaymentController.class.getMethod(name, Long.class);
|
||||||
*/
|
} catch (NoSuchMethodException e) {
|
||||||
private LinkBuilder getCommandLinkBuilder(Long id) {
|
throw new RuntimeException(e);
|
||||||
return linkTo(PaymentController.class)
|
}
|
||||||
.slash("payments")
|
|
||||||
.slash(id)
|
return linkTo(PaymentController.class, method, id);
|
||||||
.slash("commands");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,23 +194,20 @@ public class PaymentController {
|
|||||||
* @return is a hypermedia enriched resource for the supplied {@link Payment} entity
|
* @return is a hypermedia enriched resource for the supplied {@link Payment} entity
|
||||||
*/
|
*/
|
||||||
private Resource<Payment> getPaymentResource(Payment payment) {
|
private Resource<Payment> getPaymentResource(Payment payment) {
|
||||||
Resource<Payment> paymentResource;
|
Assert.notNull(payment, "Payment must not be null");
|
||||||
|
|
||||||
// Prepare hypermedia response
|
// Add command link
|
||||||
paymentResource = new Resource<>(payment,
|
payment.add(linkBuilder("getCommands", payment.getIdentity()).withRel("commands"));
|
||||||
linkTo(PaymentController.class)
|
|
||||||
.slash("payments")
|
|
||||||
.slash(payment.getPaymentId())
|
|
||||||
.withSelfRel(),
|
|
||||||
linkTo(PaymentController.class)
|
|
||||||
.slash("payments")
|
|
||||||
.slash(payment.getPaymentId())
|
|
||||||
.slash("events")
|
|
||||||
.withRel("events"),
|
|
||||||
getCommandLinkBuilder(payment.getPaymentId())
|
|
||||||
.withRel("commands")
|
|
||||||
);
|
|
||||||
|
|
||||||
return paymentResource;
|
// Add get events link
|
||||||
|
payment.add(linkBuilder("getPaymentEvents", payment.getIdentity()).withRel("events"));
|
||||||
|
|
||||||
|
return new Resource<>(payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceSupport getCommandsResources(Long id) {
|
||||||
|
Payment payment = new Payment();
|
||||||
|
payment.setIdentity(id);
|
||||||
|
return new Resource<>(payment.getCommands());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ public class EventServiceTests {
|
|||||||
Payment payment = new Payment(11.0, PaymentMethod.CREDIT_CARD);
|
Payment payment = new Payment(11.0, PaymentMethod.CREDIT_CARD);
|
||||||
payment = paymentRepository.saveAndFlush(payment);
|
payment = paymentRepository.saveAndFlush(payment);
|
||||||
eventService.save(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment));
|
eventService.save(new PaymentEvent(PaymentEventType.PAYMENT_CREATED, payment));
|
||||||
Events events = eventService.find(payment.getPaymentId());
|
Events events = eventService.find(payment.getIdentity());
|
||||||
Assert.notNull(events);
|
Assert.notNull(events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package demo.payment;
|
package demo.payment;
|
||||||
|
|
||||||
import demo.event.EventService;
|
import demo.event.EventService;
|
||||||
|
import demo.event.Events;
|
||||||
import demo.event.PaymentEvent;
|
import demo.event.PaymentEvent;
|
||||||
|
import demo.event.PaymentEventType;
|
||||||
|
import demo.payment.controller.PaymentController;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -11,6 +14,8 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.test.context.junit4.SpringRunner;
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
@@ -35,10 +40,13 @@ public class PaymentControllerTest {
|
|||||||
|
|
||||||
Payment payment = new Payment(42.0, PaymentMethod.CREDIT_CARD);
|
Payment payment = new Payment(42.0, PaymentMethod.CREDIT_CARD);
|
||||||
|
|
||||||
given(this.paymentService.getPayment(1L))
|
given(this.paymentService.getPayment(1L)).willReturn(payment);
|
||||||
.willReturn(payment);
|
given(this.eventService.find(1L)).willReturn(new Events<>(1L, Collections
|
||||||
|
.singletonList(new PaymentEvent(PaymentEventType
|
||||||
|
.PAYMENT_CREATED))));
|
||||||
|
|
||||||
this.mvc.perform(get("/v1/payments/1").accept(MediaType.APPLICATION_JSON))
|
this.mvc.perform(get("/v1/payments/1").accept(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk()).andExpect(content().json(content));
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().json(content));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,10 @@
|
|||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-commons</artifactId>
|
<artifactId>spring-data-commons</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link Action} is a reference of a method. A function contains an address to the location of a method. A function
|
||||||
|
* may contain meta-data that describes the inputs and outputs of a method. An action invokes a method annotated with
|
||||||
|
* {@link Command}.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public abstract class Action<A extends Aggregate> implements ApplicationContextAware {
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.hateoas.*;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link Aggregate} is an entity that contains references to one or more other {@link Value} objects. Aggregates
|
||||||
|
* may contain a collection of references to a {@link Command}. All command references on an aggregate should be
|
||||||
|
* explicitly typed.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
public abstract class Aggregate<ID extends Serializable> extends ResourceSupport implements Value<Link> {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
abstract ID getIdentity();
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext = Optional.ofNullable(Provider.getApplicationContext())
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an {@link Action} for this {@link Provider}
|
||||||
|
*
|
||||||
|
* @return the action for this provider
|
||||||
|
* @throws IllegalArgumentException if the application context is unavailable or the provider does not exist
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T extends Action<A>, A extends Aggregate> T getAction(
|
||||||
|
Class<T> actionType) throws IllegalArgumentException {
|
||||||
|
Provider provider = getProvider();
|
||||||
|
Service service = provider.getDefaultService();
|
||||||
|
return (T) service.getAction(actionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an instance of the {@link Provider} for this instance
|
||||||
|
*
|
||||||
|
* @return the provider for this instance
|
||||||
|
* @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 {
|
||||||
|
return getProvider((Class<T>) ResolvableType
|
||||||
|
.forClassWithGenerics(Provider.class, ResolvableType.forInstance(this))
|
||||||
|
.getRawClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an instance of a {@link Provider} with the supplied type
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Link> getLinks() {
|
||||||
|
List<Link> links = super.getLinks()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
links.add(getId());
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public CommandResources getCommands() {
|
||||||
|
CommandResources commandResources = new CommandResources();
|
||||||
|
|
||||||
|
// Get command annotations on the aggregate
|
||||||
|
List<Command> commands = Arrays.stream(this.getClass()
|
||||||
|
.getMethods())
|
||||||
|
.filter(a -> a.isAnnotationPresent(Command.class))
|
||||||
|
.map(a -> a.getAnnotation(Command.class))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Compile the collection of command links
|
||||||
|
List<Link> commandLinks = commands.stream()
|
||||||
|
.map(a -> Arrays.stream(ReflectionUtils.getAllDeclaredMethods(a.controller()))
|
||||||
|
.filter(m -> m.getName()
|
||||||
|
.equalsIgnoreCase(a.method()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseGet(null))
|
||||||
|
.map(m -> {
|
||||||
|
String uri = linkTo(m, getIdentity()).withRel(m.getName())
|
||||||
|
.getHref();
|
||||||
|
|
||||||
|
return new Link(new UriTemplate(uri, new TemplateVariables(Arrays.stream(m.getParameters())
|
||||||
|
.filter(p -> p.isAnnotationPresent(RequestParam.class))
|
||||||
|
.map(p -> new TemplateVariable(p.getAnnotation(RequestParam.class)
|
||||||
|
.value(), TemplateVariable.VariableType.REQUEST_PARAM))
|
||||||
|
.toArray(TemplateVariable[]::new))), m.getName());
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
commandResources.add(commandLinks);
|
||||||
|
|
||||||
|
return commandResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CommandResources extends ResourceSupport {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Required;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Command} is an annotated method that contains a reference to a function in the context of an
|
||||||
|
* {@link Aggregate}. A command maps a method reference on an {@link Aggregate} to a function invocation. Commands
|
||||||
|
* are discoverable.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Component
|
||||||
|
public @interface Command {
|
||||||
|
|
||||||
|
String description() default "";
|
||||||
|
|
||||||
|
@Required
|
||||||
|
String method() default "";
|
||||||
|
|
||||||
|
@Required
|
||||||
|
Class controller();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Commodity} object is a {@link Value} object that is also an {@link Aggregate} root. A commodity object
|
||||||
|
* describes all aspects of an aggregate and is both stateless and immutable. A commodity is a locator that connects
|
||||||
|
* relationships of a value object to a {@link Provider}.
|
||||||
|
* <p>
|
||||||
|
* The key difference between a commodity object and an aggregate is that a commodity object is a distributed
|
||||||
|
* representation of an aggregate root that combines together references from multiple bounded contexts.
|
||||||
|
* <p>
|
||||||
|
* Commodities are used by a discovery service to create a reverse-proxy that translates the relationships of an
|
||||||
|
* aggregate into URIs.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
public abstract class Commodity extends Aggregate {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Provider} is a collection of {@link Service} references that are used to consume and/or produce
|
||||||
|
* {@link org.springframework.hateoas.Resource}s. Providers transfer a resource into a {@link Commodity}.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public abstract class Provider<T extends Aggregate> implements ApplicationContextAware {
|
||||||
|
private static ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
public static ApplicationContext getApplicationContext() {
|
||||||
|
return applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
Provider.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Service<? extends T> getDefaultService();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
@org.springframework.stereotype.Service
|
||||||
|
public abstract class Service<T extends Aggregate> implements ApplicationContextAware {
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <A extends Action<T>> A getAction(Class<? extends A> clazz) {
|
||||||
|
return applicationContext.getBean(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.hateoas.Identifiable;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Value} objects are wrappers that contain the serializable properties that uniquely identify an entity.
|
||||||
|
* Value objects contain a collection of relationships. Value objects contain a collection of comparison operators.
|
||||||
|
* The default identity comparator evaluates true if the compared objects have the same identifier.
|
||||||
|
*
|
||||||
|
* @author Kenny Bastani
|
||||||
|
*/
|
||||||
|
public interface Value<ID extends Serializable> extends Identifiable<ID> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package demo.domain;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.util.EnvironmentTestUtils;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.hateoas.Link;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertEquals;
|
||||||
|
import static junit.framework.TestCase.assertNotNull;
|
||||||
|
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(classes = {ProviderTests.class, ProviderTests.EmptyConfiguration.class, ProviderTests.EmptyProvider.class, ProviderTests.EmptyService.class, ProviderTests.EmptyAction.class})
|
||||||
|
public class ProviderTests {
|
||||||
|
|
||||||
|
private AnnotationConfigApplicationContext context;
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
if (this.context != null) {
|
||||||
|
this.context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
load(EmptyProvider.class);
|
||||||
|
assertNotNull(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetProviderReturnsProvider() {
|
||||||
|
assertNotNull(new EmptyAggregate().getProvider(EmptyProvider.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetServiceReturnsService() {
|
||||||
|
EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class);
|
||||||
|
assertNotNull(provider.getEmptyService());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetActionReturnsAction() {
|
||||||
|
EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class);
|
||||||
|
EmptyService service = provider.getEmptyService();
|
||||||
|
assertNotNull(service.getAction(EmptyAction.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommandChangesStatus() {
|
||||||
|
EmptyAggregate aggregate = new EmptyAggregate(0L, AggregateStatus.CREATED);
|
||||||
|
EmptyProvider provider = new EmptyAggregate().getProvider(EmptyProvider.class);
|
||||||
|
EmptyService service = provider.getEmptyService();
|
||||||
|
EmptyAction emptyAction = service.getAction(EmptyAction.class);
|
||||||
|
emptyAction.getConsumer().accept(aggregate);
|
||||||
|
assertEquals(aggregate.getStatus(), AggregateStatus.PROCESSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessEmptyAggregateChangesStatus() {
|
||||||
|
EmptyAggregate aggregate = new EmptyAggregate(0L, AggregateStatus.CREATED);
|
||||||
|
aggregate.emptyAction();
|
||||||
|
assertEquals(aggregate.getStatus(), AggregateStatus.PROCESSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAggregateProducesCommandLinks() {
|
||||||
|
EmptyAggregate aggregate = new EmptyAggregate(0L, AggregateStatus.CREATED);
|
||||||
|
Assert.notEmpty(aggregate.getLinks());
|
||||||
|
Assert.notEmpty(aggregate.getCommands().getLinks());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load(Class<?> provider, String... environment) {
|
||||||
|
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||||
|
EnvironmentTestUtils.addEnvironment(applicationContext, environment);
|
||||||
|
applicationContext.register(EmptyConfiguration.class, provider, EmptyService.class, EmptyAction.class, EmptyController.class);
|
||||||
|
applicationContext.refresh();
|
||||||
|
this.context = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public static class EmptyConfiguration {
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class EmptyAggregate extends Aggregate<Long> {
|
||||||
|
@NonNull
|
||||||
|
private Long id;
|
||||||
|
@NonNull
|
||||||
|
private AggregateStatus status;
|
||||||
|
|
||||||
|
@Command(controller = EmptyController.class, method = "emptyAction")
|
||||||
|
public void emptyAction() {
|
||||||
|
EmptyProvider emptyProvider = this.getProvider();
|
||||||
|
emptyProvider.getEmptyService()
|
||||||
|
.getAction(EmptyAction.class)
|
||||||
|
.getConsumer()
|
||||||
|
.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Link getId() {
|
||||||
|
return new Link("example.com").withSelfRel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Long getIdentity() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class EmptyProvider extends Provider<EmptyAggregate> {
|
||||||
|
private final EmptyService emptyService;
|
||||||
|
|
||||||
|
public Service<? extends EmptyAggregate> getDefaultService() {
|
||||||
|
return emptyService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmptyService extends Service<EmptyAggregate> {
|
||||||
|
public EmptyAggregate getEmptyAggregate(Long id) {
|
||||||
|
return new EmptyAggregate(id, AggregateStatus.CREATED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmptyAction extends Action<EmptyAggregate> {
|
||||||
|
|
||||||
|
public Consumer<EmptyAggregate> getConsumer() {
|
||||||
|
return a -> a.setStatus(AggregateStatus.PROCESSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v1")
|
||||||
|
public static class EmptyController {
|
||||||
|
|
||||||
|
private EmptyProvider provider;
|
||||||
|
|
||||||
|
public EmptyController(EmptyProvider provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/empty/{id}", method = RequestMethod.GET)
|
||||||
|
public EmptyAggregate emptyAction(@PathVariable("id") Long id, @RequestParam("q") String query) {
|
||||||
|
return provider.getEmptyService().getEmptyAggregate(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AggregateStatus {
|
||||||
|
CREATED,
|
||||||
|
PROCESSED
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user