diff --git a/account/account-web/pom.xml b/account/account-web/pom.xml index fde7703..398da12 100644 --- a/account/account-web/pom.xml +++ b/account/account-web/pom.xml @@ -51,6 +51,12 @@ org.springframework.boot spring-boot-starter-integration + + org.kbastani + spring-boot-starter-data-events + 1.0-SNAPSHOT + + com.h2database diff --git a/account/account-web/src/main/java/demo/account/Account.java b/account/account-web/src/main/java/demo/account/Account.java index 4987f6b..a740f95 100644 --- a/account/account-web/src/main/java/demo/account/Account.java +++ b/account/account-web/src/main/java/demo/account/Account.java @@ -1,23 +1,22 @@ package demo.account; -import com.fasterxml.jackson.annotation.JsonIgnore; -import demo.domain.BaseEntity; +import com.fasterxml.jackson.annotation.JsonProperty; +import demo.account.action.ActivateAccount; +import demo.account.action.ArchiveAccount; +import demo.account.action.ConfirmAccount; +import demo.account.action.SuspendAccount; +import demo.account.controller.AccountController; +import demo.domain.AbstractEntity; +import demo.domain.Command; import demo.event.AccountEvent; +import org.springframework.hateoas.Link; import javax.persistence.*; -import java.util.HashSet; -import java.util.Set; -/** - * The {@link Account} domain object contains information related to - * a user's account. The status of an account is event sourced using - * events logged to the {@link AccountEvent} collection attached to - * this resource. - * - * @author kbastani - */ +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + @Entity -public class Account extends BaseEntity { +public class Account extends AbstractEntity { @Id @GeneratedValue @@ -27,9 +26,6 @@ public class Account extends BaseEntity { private String lastName; private String email; - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Set events = new HashSet<>(); - @Enumerated(value = EnumType.STRING) private AccountStatus status; @@ -44,12 +40,14 @@ public class Account extends BaseEntity { this.email = email; } - @JsonIgnore - public Long getAccountId() { - return id; + @JsonProperty("accountId") + @Override + public Long getIdentity() { + return this.id; } - public void setAccountId(Long id) { + @Override + public void setIdentity(Long id) { this.id = id; } @@ -77,15 +75,6 @@ public class Account extends BaseEntity { this.email = email; } - @JsonIgnore - public Set getEvents() { - return events; - } - - public void setEvents(Set events) { - this.events = events; - } - public AccountStatus getStatus() { return status; } @@ -94,6 +83,49 @@ public class Account extends BaseEntity { this.status = status; } + @Command(method = "activate", controller = AccountController.class) + public Account activate() { + getAction(ActivateAccount.class) + .getConsumer() + .accept(this); + return this; + } + + @Command(method = "archive", controller = AccountController.class) + public Account archive() { + getAction(ArchiveAccount.class) + .getConsumer() + .accept(this); + return this; + } + + @Command(method = "confirm", controller = AccountController.class) + public Account confirm() { + getAction(ConfirmAccount.class) + .getConsumer() + .accept(this); + return this; + } + + @Command(method = "suspend", controller = AccountController.class) + public Account suspend() { + getAction(SuspendAccount.class) + .getConsumer() + .accept(this); + return this; + } + + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ + @Override + public Link getId() { + return linkTo(AccountController.class) + .slash("accounts") + .slash(getIdentity()) + .withSelfRel(); + } + @Override public String toString() { return "Account{" + @@ -101,7 +133,6 @@ public class Account extends BaseEntity { ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + - ", events=" + events + ", status=" + status + "} " + super.toString(); } diff --git a/account/account-web/src/main/java/demo/account/AccountCommand.java b/account/account-web/src/main/java/demo/account/AccountCommand.java deleted file mode 100644 index f391d91..0000000 --- a/account/account-web/src/main/java/demo/account/AccountCommand.java +++ /dev/null @@ -1,15 +0,0 @@ -package demo.account; - -/** - * The {@link AccountCommand} represents an action that can be performed to an - * {@link Account} aggregate. Commands initiate an action that can mutate the state of - * an account entity as it transitions between {@link AccountStatus} values. - * - * @author kbastani - */ -public enum AccountCommand { - CONFIRM_ACCOUNT, - ACTIVATE_ACCOUNT, - SUSPEND_ACCOUNT, - ARCHIVE_ACCOUNT -} diff --git a/account/account-web/src/main/java/demo/account/AccountCommandsResource.java b/account/account-web/src/main/java/demo/account/AccountCommandsResource.java deleted file mode 100644 index 14390d0..0000000 --- a/account/account-web/src/main/java/demo/account/AccountCommandsResource.java +++ /dev/null @@ -1,12 +0,0 @@ -package demo.account; - -import org.springframework.hateoas.ResourceSupport; - -/** - * A hypermedia resource that describes the collection of commands that - * can be applied to a {@link Account} aggregate. - * - * @author kbastani - */ -public class AccountCommandsResource extends ResourceSupport { -} diff --git a/account/account-web/src/main/java/demo/account/AccountProvider.java b/account/account-web/src/main/java/demo/account/AccountProvider.java new file mode 100644 index 0000000..f7c3420 --- /dev/null +++ b/account/account-web/src/main/java/demo/account/AccountProvider.java @@ -0,0 +1,35 @@ +package demo.account; + +import demo.domain.Provider; +import demo.event.AccountEvent; +import demo.event.EventService; + +@org.springframework.stereotype.Service +public class AccountProvider extends Provider { + + private final AccountService accountService; + private final EventService eventService; + + public AccountProvider(AccountService accountService, EventService eventService) { + this.accountService = accountService; + this.eventService = eventService; + } + + public AccountService getAccountService() { + return accountService; + } + + public EventService getEventService() { + return eventService; + } + + @Override + public AccountService getDefaultService() { + return accountService; + } + + @Override + public EventService getDefaultEventService() { + return eventService; + } +} diff --git a/account/account-web/src/main/java/demo/account/AccountRepository.java b/account/account-web/src/main/java/demo/account/AccountRepository.java index 2a04aa1..6a2118c 100644 --- a/account/account-web/src/main/java/demo/account/AccountRepository.java +++ b/account/account-web/src/main/java/demo/account/AccountRepository.java @@ -4,6 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; public interface AccountRepository extends JpaRepository { - Account findAccountByEmail(@Param("email") String email); } diff --git a/account/account-web/src/main/java/demo/account/AccountService.java b/account/account-web/src/main/java/demo/account/AccountService.java index 8a2f049..f2104e3 100644 --- a/account/account-web/src/main/java/demo/account/AccountService.java +++ b/account/account-web/src/main/java/demo/account/AccountService.java @@ -1,62 +1,27 @@ package demo.account; +import demo.domain.Service; import demo.event.AccountEvent; import demo.event.AccountEventType; -import demo.event.EventService; -import demo.event.ConsistencyModel; -import org.springframework.cache.CacheManager; -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.stereotype.Service; import org.springframework.util.Assert; -import java.util.Arrays; -import java.util.Objects; - -import static demo.account.AccountStatus.*; - -/** - * The {@link AccountService} provides transactional support for managing {@link Account} - * entities. This service also provides event sourcing support for {@link AccountEvent}. - * Events can be appended to an {@link Account}, which contains a append-only log of - * actions that can be used to support remediation for distributed transactions that encountered - * a partial failure. - * - * @author kbastani - */ -@Service -@CacheConfig(cacheNames = {"accounts"}) -public class AccountService { +@org.springframework.stereotype.Service +public class AccountService extends Service { private final AccountRepository accountRepository; - private final EventService eventService; - private final CacheManager cacheManager; - public AccountService(AccountRepository accountRepository, EventService eventService, CacheManager cacheManager) { + public AccountService(AccountRepository accountRepository) { this.accountRepository = accountRepository; - this.eventService = eventService; - this.cacheManager = cacheManager; } - @CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()") public Account registerAccount(Account account) { - - account = createAccount(account); - - cacheManager.getCache("accounts") - .evict(account.getAccountId()); + account = create(account); // Trigger the account creation event - AccountEvent event = appendEvent(account.getAccountId(), - new AccountEvent(AccountEventType.ACCOUNT_CREATED)); - - // Attach account identifier - event.getAccount().setAccountId(account.getAccountId()); + account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_CREATED, account)); // Return the result - return event.getAccount(); + return account; } /** @@ -65,8 +30,7 @@ public class AccountService { * @param account is the {@link Account} to create * @return the newly created {@link Account} */ - @CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()") - public Account createAccount(Account account) { + public Account create(Account account) { // Assert for uniqueness constraint Assert.isNull(accountRepository.findAccountByEmail(account.getEmail()), @@ -84,34 +48,24 @@ public class AccountService { * @param id is the unique identifier of a {@link Account} entity * @return an {@link Account} entity */ - @Cacheable(cacheNames = "accounts", key = "#id.toString()") - public Account getAccount(Long id) { + public Account get(Long id) { return accountRepository.findOne(id); } /** * Update an {@link Account} entity with the supplied identifier. * - * @param id is the unique identifier of the {@link Account} entity * @param account is the {@link Account} containing updated fields * @return the updated {@link Account} entity */ - @CachePut(cacheNames = "accounts", key = "#id.toString()") - public Account updateAccount(Long id, Account account) { - Assert.notNull(id, "Account id must be present in the resource URL"); + public Account update(Account account) { + Assert.notNull(account.getIdentity(), "Account id must be present in the resource URL"); Assert.notNull(account, "Account request body cannot be null"); - if (account.getAccountId() != null) { - Assert.isTrue(Objects.equals(id, account.getAccountId()), - "The account id in the request body must match the resource URL"); - } else { - account.setAccountId(id); - } - - Assert.state(accountRepository.exists(id), + Assert.state(accountRepository.exists(account.getIdentity()), "The account with the supplied id does not exist"); - Account currentAccount = accountRepository.findOne(id); + Account currentAccount = get(account.getIdentity()); currentAccount.setEmail(account.getEmail()); currentAccount.setFirstName(account.getFirstName()); currentAccount.setLastName(account.getLastName()); @@ -125,101 +79,10 @@ public class AccountService { * * @param id is the unique identifier for the {@link Account} */ - @CacheEvict(cacheNames = "accounts", key = "#id.toString()") - public Boolean deleteAccount(Long id) { + public boolean delete(Long id) { Assert.state(accountRepository.exists(id), "The account with the supplied id does not exist"); this.accountRepository.delete(id); return true; } - - /** - * Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier. - * - * @param accountId is the unique identifier for the {@link Account} - * @param event is the {@link AccountEvent} to append to the {@link Account} entity - * @return the newly appended {@link AccountEvent} - */ - public AccountEvent appendEvent(Long accountId, AccountEvent event) { - return appendEvent(accountId, event, ConsistencyModel.ACID); - } - - /** - * Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier. - * - * @param accountId is the unique identifier for the {@link Account} - * @param event is the {@link AccountEvent} to append to the {@link Account} entity - * @return the newly appended {@link AccountEvent} - */ - public AccountEvent appendEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) { - Account account = getAccount(accountId); - Assert.notNull(account, "The account with the supplied id does not exist"); - event.setAccount(account); - event = eventService.createEvent(accountId, event); - account.getEvents().add(event); - accountRepository.saveAndFlush(account); - eventService.raiseEvent(event, consistencyModel); - return event; - } - - /** - * Apply an {@link AccountCommand} to the {@link Account} with a specified identifier. - * - * @param id is the unique identifier of the {@link Account} - * @param accountCommand is the command to apply to the {@link Account} - * @return a hypermedia resource containing the updated {@link Account} - */ - @CachePut(cacheNames = "accounts", key = "#id.toString()") - public Account applyCommand(Long id, AccountCommand accountCommand) { - Account account = getAccount(id); - - Assert.notNull(account, "The account for the supplied id could not be found"); - - AccountStatus status = account.getStatus(); - - switch (accountCommand) { - case CONFIRM_ACCOUNT: - Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed"); - - // Confirm the account - Account updateAccount = account; - updateAccount.setStatus(ACCOUNT_CONFIRMED); - this.updateAccount(id, updateAccount); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)) - .getAccount(); - break; - case ACTIVATE_ACCOUNT: - Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active"); - Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED) - .contains(status), "The account cannot be activated"); - - // Activate the account - account.setStatus(ACCOUNT_ACTIVE); - this.updateAccount(id, account); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)) - .getAccount(); - break; - case SUSPEND_ACCOUNT: - Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); - - // Suspend the account - account.setStatus(ACCOUNT_SUSPENDED); - account = this.updateAccount(id, account); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)); - break; - case ARCHIVE_ACCOUNT: - Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); - - // Archive the account - account.setStatus(ACCOUNT_ARCHIVED); - account = this.updateAccount(id, account); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED)); - break; - default: - Assert.notNull(accountCommand, - "The provided command cannot be applied to this account in its current state"); - } - - return account; - } } diff --git a/account/account-web/src/main/java/demo/account/action/ActivateAccount.java b/account/account-web/src/main/java/demo/account/action/ActivateAccount.java new file mode 100644 index 0000000..bcadd7f --- /dev/null +++ b/account/account-web/src/main/java/demo/account/action/ActivateAccount.java @@ -0,0 +1,43 @@ +package demo.account.action; + +import demo.account.Account; +import demo.account.AccountProvider; +import demo.account.AccountService; +import demo.account.AccountStatus; +import demo.domain.Action; +import demo.event.AccountEvent; +import demo.event.AccountEventType; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.Arrays; +import java.util.function.Consumer; + +import static demo.account.AccountStatus.*; + +/** + * Connects an {@link Account} to an Account. + * + * @author Kenny Bastani + */ +@Service +public class ActivateAccount extends Action { + + public Consumer getConsumer() { + return (account) -> { + Assert.isTrue(account.getStatus() != ACCOUNT_ACTIVE, "The account is already active"); + Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED) + .contains(account.getStatus()), "The account cannot be activated"); + + AccountService accountService = account.getProvider(AccountProvider.class) + .getDefaultService(); + + // Activate the account + account.setStatus(AccountStatus.ACCOUNT_ACTIVE); + account = accountService.update(account); + + // Trigger the account activated event + account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account)); + }; + } +} diff --git a/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java b/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java new file mode 100644 index 0000000..bd7f0b6 --- /dev/null +++ b/account/account-web/src/main/java/demo/account/action/ArchiveAccount.java @@ -0,0 +1,40 @@ +package demo.account.action; + +import demo.account.Account; +import demo.account.AccountProvider; +import demo.account.AccountService; +import demo.account.AccountStatus; +import demo.domain.Action; +import demo.event.AccountEvent; +import demo.event.AccountEventType; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.function.Consumer; + +import static demo.account.AccountStatus.ACCOUNT_ACTIVE; + +/** + * Connects an {@link Account} to an Account. + * + * @author Kenny Bastani + */ +@Service +public class ArchiveAccount extends Action { + + public Consumer getConsumer() { + return (account) -> { + Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); + + AccountService accountService = account.getProvider(AccountProvider.class) + .getDefaultService(); + + // Archive the account + account.setStatus(AccountStatus.ACCOUNT_ARCHIVED); + account = accountService.update(account); + + // Trigger the account archived event + account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED, account)); + }; + } +} diff --git a/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java b/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java new file mode 100644 index 0000000..690ea42 --- /dev/null +++ b/account/account-web/src/main/java/demo/account/action/ConfirmAccount.java @@ -0,0 +1,40 @@ +package demo.account.action; + +import demo.account.Account; +import demo.account.AccountProvider; +import demo.account.AccountService; +import demo.account.AccountStatus; +import demo.domain.Action; +import demo.event.AccountEvent; +import demo.event.AccountEventType; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.function.Consumer; + +import static demo.account.AccountStatus.ACCOUNT_PENDING; + +/** + * Connects an {@link Account} to an Account. + * + * @author Kenny Bastani + */ +@Service +public class ConfirmAccount extends Action { + + public Consumer getConsumer() { + return (account) -> { + Assert.isTrue(account.getStatus() == ACCOUNT_PENDING, "The account has already been confirmed"); + + AccountService accountService = account.getProvider(AccountProvider.class) + .getDefaultService(); + + // Confirm the account + account.setStatus(AccountStatus.ACCOUNT_CONFIRMED); + account = accountService.update(account); + + // Trigger the account confirmed + account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED, account)); + }; + } +} diff --git a/account/account-web/src/main/java/demo/account/action/SuspendAccount.java b/account/account-web/src/main/java/demo/account/action/SuspendAccount.java new file mode 100644 index 0000000..b9c6e41 --- /dev/null +++ b/account/account-web/src/main/java/demo/account/action/SuspendAccount.java @@ -0,0 +1,40 @@ +package demo.account.action; + +import demo.account.Account; +import demo.account.AccountProvider; +import demo.account.AccountService; +import demo.account.AccountStatus; +import demo.domain.Action; +import demo.event.AccountEvent; +import demo.event.AccountEventType; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.function.Consumer; + +import static demo.account.AccountStatus.ACCOUNT_ACTIVE; + +/** + * Connects an {@link Account} to an Account. + * + * @author Kenny Bastani + */ +@Service +public class SuspendAccount extends Action { + + public Consumer getConsumer() { + return (account) -> { + Assert.isTrue(account.getStatus() == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); + + AccountService accountService = account.getProvider(AccountProvider.class) + .getDefaultService(); + + // Suspend the account + account.setStatus(AccountStatus.ACCOUNT_SUSPENDED); + account = accountService.update(account); + + // Trigger the account suspended event + account.sendAsyncEvent(new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED, account)); + }; + } +} diff --git a/account/account-web/src/main/java/demo/account/AccountController.java b/account/account-web/src/main/java/demo/account/controller/AccountController.java similarity index 53% rename from account/account-web/src/main/java/demo/account/AccountController.java rename to account/account-web/src/main/java/demo/account/controller/AccountController.java index d48bcb0..adf9e7a 100644 --- a/account/account-web/src/main/java/demo/account/AccountController.java +++ b/account/account-web/src/main/java/demo/account/controller/AccountController.java @@ -1,16 +1,20 @@ -package demo.account; +package demo.account.controller; +import demo.account.Account; +import demo.account.AccountService; import demo.event.AccountEvent; -import demo.event.AccountEvents; import demo.event.EventController; import demo.event.EventService; +import demo.event.Events; import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceSupport; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; +import java.lang.reflect.Method; import java.util.Optional; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @@ -20,9 +24,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; public class AccountController { private final AccountService accountService; - private final EventService eventService; + private final EventService eventService; - public AccountController(AccountService accountService, EventService eventService) { + public AccountController(AccountService accountService, EventService eventService) { this.accountService = accountService; this.eventService = eventService; } @@ -41,7 +45,7 @@ public class AccountController { .orElseThrow(() -> new RuntimeException("Account update failed")); } - @GetMapping(path = "/accounts/{id}") + @RequestMapping(path = "/accounts/{id}") public ResponseEntity getAccount(@PathVariable Long id) { return Optional.ofNullable(getAccountResource(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) @@ -50,12 +54,12 @@ public class AccountController { @DeleteMapping(path = "/accounts/{id}") public ResponseEntity deleteAccount(@PathVariable Long id) { - return Optional.ofNullable(accountService.deleteAccount(id)) + return Optional.ofNullable(accountService.delete(id)) .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) .orElseThrow(() -> new RuntimeException("Account deletion failed")); } - @GetMapping(path = "/accounts/{id}/events") + @RequestMapping(path = "/accounts/{id}/events") public ResponseEntity getAccountEvents(@PathVariable Long id) { return Optional.of(getAccountEventResources(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) @@ -63,47 +67,47 @@ public class AccountController { } @PostMapping(path = "/accounts/{id}/events") - public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) { + public ResponseEntity appendAccountEvent(@PathVariable Long id, @RequestBody AccountEvent event) { return Optional.ofNullable(appendEventResource(id, event)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) .orElseThrow(() -> new RuntimeException("Append account event failed")); } - @GetMapping(path = "/accounts/{id}/commands") - public ResponseEntity getAccountCommands(@PathVariable Long id) { + @RequestMapping(path = "/accounts/{id}/commands") + public ResponseEntity getCommands(@PathVariable Long id) { return Optional.ofNullable(getCommandsResource(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The account could not be found")); } - @GetMapping(path = "/accounts/{id}/commands/confirm") - public ResponseEntity confirmAccount(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource( - accountService.applyCommand(id, AccountCommand.CONFIRM_ACCOUNT))) + @RequestMapping(path = "/accounts/{id}/commands/confirm") + public ResponseEntity confirm(@PathVariable Long id) { + return Optional.ofNullable(getAccountResource(accountService.get(id) + .confirm())) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/accounts/{id}/commands/activate") - public ResponseEntity activateAccount(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource( - accountService.applyCommand(id, AccountCommand.ACTIVATE_ACCOUNT))) + @RequestMapping(path = "/accounts/{id}/commands/activate") + public ResponseEntity activate(@PathVariable Long id) { + return Optional.ofNullable(getAccountResource(accountService.get(id) + .activate())) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/accounts/{id}/commands/suspend") - public ResponseEntity suspendAccount(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource( - accountService.applyCommand(id, AccountCommand.SUSPEND_ACCOUNT))) + @RequestMapping(path = "/accounts/{id}/commands/suspend") + public ResponseEntity suspend(@PathVariable Long id) { + return Optional.ofNullable(getAccountResource(accountService.get(id) + .suspend())) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - @GetMapping(path = "/accounts/{id}/commands/archive") - public ResponseEntity archiveAccount(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource( - accountService.applyCommand(id, AccountCommand.ARCHIVE_ACCOUNT))) + @RequestMapping(path = "/accounts/{id}/commands/archive") + public ResponseEntity archive(@PathVariable Long id) { + return Optional.ofNullable(getAccountResource(accountService.get(id) + .archive())) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @@ -115,17 +119,10 @@ public class AccountController { * @return a hypermedia resource for the fetched {@link Account} */ private Resource getAccountResource(Long id) { - Resource accountResource = null; - // Get the account for the provided id - Account account = accountService.getAccount(id); + Account account = accountService.get(id); - // If the account exists, wrap the hypermedia response - if (account != null) - accountResource = getAccountResource(account); - - - return accountResource; + return getAccountResource(account); } /** @@ -154,7 +151,8 @@ public class AccountController { * @return a hypermedia resource for the updated {@link Account} */ private Resource updateAccountResource(Long id, Account account) { - return getAccountResource(accountService.updateAccount(id, account)); + account.setIdentity(id); + return getAccountResource(accountService.update(account)); } /** @@ -166,82 +164,40 @@ public class AccountController { * @return a hypermedia resource for the newly appended {@link AccountEvent} */ private Resource appendEventResource(Long accountId, AccountEvent event) { - Resource eventResource = null; + Assert.notNull(event, "Event body must be provided"); + + Account account = accountService.get(accountId); + Assert.notNull(account, "Account could not be found"); - event = accountService.appendEvent(accountId, event); + event.setEntity(account); + account.sendAsyncEvent(event); - if (event != null) { - eventResource = new Resource<>(event, - linkTo(EventController.class) - .slash("events") - .slash(event.getEventId()) - .withSelfRel(), - linkTo(AccountController.class) - .slash("accounts") - .slash(accountId) - .withRel("account") - ); + return new Resource<>(event, + linkTo(EventController.class) + .slash("events") + .slash(event.getEventId()) + .withSelfRel(), + linkTo(AccountController.class) + .slash("accounts") + .slash(accountId) + .withRel("account") + ); + } + + private Events getAccountEventResources(Long id) { + return eventService.find(id); + } + + private LinkBuilder linkBuilder(String name, Long id) { + Method method; + + try { + method = AccountController.class.getMethod(name, Long.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); } - return eventResource; - } - - /** - * Get the {@link AccountCommand} hypermedia resource that lists the available commands that can be applied - * to an {@link Account} entity. - * - * @param id is the {@link Account} identifier to provide command links for - * @return an {@link AccountCommandsResource} with a collection of embedded command links - */ - private AccountCommandsResource getCommandsResource(Long id) { - // Get the account resource for the identifier - Resource accountResource = getAccountResource(id); - - // Create a new account commands hypermedia resource - AccountCommandsResource commandResource = new AccountCommandsResource(); - - // Add account command hypermedia links - if (accountResource != null) { - commandResource.add( - getCommandLinkBuilder(id) - .slash("confirm") - .withRel("confirm"), - getCommandLinkBuilder(id) - .slash("activate") - .withRel("activate"), - getCommandLinkBuilder(id) - .slash("suspend") - .withRel("suspend"), - getCommandLinkBuilder(id) - .slash("archive") - .withRel("archive") - ); - } - - return commandResource; - } - - /** - * Get {@link AccountEvents} for the supplied {@link Account} identifier. - * - * @param id is the unique identifier of the {@link Account} - * @return a list of {@link AccountEvent} wrapped in a hypermedia {@link AccountEvents} resource - */ - private AccountEvents getAccountEventResources(Long id) { - return new AccountEvents(id, eventService.getAccountEvents(id)); - } - - /** - * Generate a {@link LinkBuilder} for generating the {@link AccountCommandsResource}. - * - * @param id is the unique identifier for a {@link Account} - * @return a {@link LinkBuilder} for the {@link AccountCommandsResource} - */ - private LinkBuilder getCommandLinkBuilder(Long id) { - return linkTo(AccountController.class) - .slash("accounts") - .slash(id) - .slash("commands"); + return linkTo(AccountController.class, method, id); } /** @@ -251,23 +207,20 @@ public class AccountController { * @return is a hypermedia enriched resource for the supplied {@link Account} entity */ private Resource getAccountResource(Account account) { - Resource accountResource; + Assert.notNull(account, "Account must not be null"); - // Prepare hypermedia response - accountResource = new Resource<>(account, - linkTo(AccountController.class) - .slash("accounts") - .slash(account.getAccountId()) - .withSelfRel(), - linkTo(AccountController.class) - .slash("accounts") - .slash(account.getAccountId()) - .slash("events") - .withRel("events"), - getCommandLinkBuilder(account.getAccountId()) - .withRel("commands") - ); + // Add command link + account.add(linkBuilder("getCommands", account.getIdentity()).withRel("commands")); - return accountResource; + // Add get events link + account.add(linkBuilder("getAccountEvents", account.getIdentity()).withRel("events")); + + return new Resource<>(account); + } + + private ResourceSupport getCommandsResource(Long id) { + Account account = new Account(); + account.setIdentity(id); + return new Resource<>(account.getCommands()); } } diff --git a/account/account-web/src/main/java/demo/config/CacheConfig.java b/account/account-web/src/main/java/demo/config/CacheConfig.java deleted file mode 100644 index 07b27c7..0000000 --- a/account/account-web/src/main/java/demo/config/CacheConfig.java +++ /dev/null @@ -1,46 +0,0 @@ -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; -import org.springframework.data.redis.core.RedisTemplate; - -import java.util.Arrays; - -@Configuration -@EnableCaching -public class CacheConfig { - - @Bean - public JedisConnectionFactory redisConnectionFactory( - @Value("${spring.redis.port}") Integer redisPort, - @Value("${spring.redis.host}") String redisHost) { - JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(); - - redisConnectionFactory.setHostName(redisHost); - redisConnectionFactory.setPort(redisPort); - - return redisConnectionFactory; - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory cf) { - RedisTemplate redisTemplate = new RedisTemplate(); - redisTemplate.setConnectionFactory(cf); - return redisTemplate; - } - - @Bean - public CacheManager cacheManager(RedisTemplate redisTemplate) { - RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); - cacheManager.setDefaultExpiration(50000); - cacheManager.setCacheNames(Arrays.asList("accounts", "events")); - cacheManager.setUsePrefix(true); - return cacheManager; - } -} \ No newline at end of file diff --git a/account/account-web/src/main/java/demo/domain/BaseEntity.java b/account/account-web/src/main/java/demo/domain/AbstractEntity.java similarity index 54% rename from account/account-web/src/main/java/demo/domain/BaseEntity.java rename to account/account-web/src/main/java/demo/domain/AbstractEntity.java index fb472ef..e98f45d 100644 --- a/account/account-web/src/main/java/demo/domain/BaseEntity.java +++ b/account/account-web/src/main/java/demo/domain/AbstractEntity.java @@ -1,17 +1,21 @@ package demo.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; +import demo.event.Event; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.springframework.hateoas.ResourceSupport; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import javax.persistence.*; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class BaseEntity extends ResourceSupport implements Serializable { +public abstract class AbstractEntity extends Aggregate implements Serializable { + + private T identity; @CreatedDate private Long createdAt; @@ -19,7 +23,10 @@ public class BaseEntity extends ResourceSupport implements Serializable { @LastModifiedDate private Long lastModified; - public BaseEntity() { + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List events = new ArrayList<>(); + + public AbstractEntity() { } public Long getCreatedAt() { @@ -38,6 +45,25 @@ public class BaseEntity extends ResourceSupport implements Serializable { this.lastModified = lastModified; } + @Override + @JsonIgnore + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + @Override + public T getIdentity() { + return identity; + } + + public void setIdentity(T id) { + this.identity = id; + } + @Override public String toString() { return "BaseEntity{" + @@ -45,4 +71,4 @@ public class BaseEntity extends ResourceSupport implements Serializable { ", lastModified=" + lastModified + '}'; } -} +} \ No newline at end of file diff --git a/account/account-web/src/main/java/demo/event/AccountEvent.java b/account/account-web/src/main/java/demo/event/AccountEvent.java index 97d53dd..b5fde9a 100644 --- a/account/account-web/src/main/java/demo/event/AccountEvent.java +++ b/account/account-web/src/main/java/demo/event/AccountEvent.java @@ -2,33 +2,40 @@ package demo.event; import com.fasterxml.jackson.annotation.JsonIgnore; import demo.account.Account; -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 AccountEvent} tracks the type and state of events as - * applied to the {@link Account} domain object. This event resource can be used - * to event source the aggregate state of {@link Account}. + * The domain event {@link AccountEvent} tracks the type and state of events as applied to the {@link Account} domain + * object. This event resource can be used to event source the aggregate state of {@link Account}. *

- * This event resource also provides a transaction log that can be used to append - * actions to the event. + * This event resource also provides a transaction log that can be used to append actions to the event. * - * @author kbastani + * @author Kenny Bastani */ @Entity -public class AccountEvent extends BaseEntity { +@EntityListeners(AuditingEntityListener.class) +public class AccountEvent extends Event { @Id - @GeneratedValue - private Long id; + @GeneratedValue(strategy = GenerationType.AUTO) + private Long eventId; @Enumerated(EnumType.STRING) private AccountEventType type; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JsonIgnore - private Account account; + private Account entity; + + @CreatedDate + private Long createdAt; + + @LastModifiedDate + private Long lastModified; public AccountEvent() { } @@ -37,37 +44,69 @@ public class AccountEvent extends BaseEntity { this.type = type; } - @JsonIgnore + public AccountEvent(AccountEventType type, Account 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 AccountEventType getType() { return type; } + @Override public void setType(AccountEventType type) { this.type = type; } - public Account getAccount() { - return account; + @Override + public Account getEntity() { + return entity; } - public void setAccount(Account account) { - this.account = account; + @Override + public void setEntity(Account entity) { + this.entity = entity; + } + + @Override + public Long getCreatedAt() { + return createdAt; + } + + @Override + public void setCreatedAt(Long createdAt) { + this.createdAt = createdAt; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + @Override + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; } @Override public String toString() { return "AccountEvent{" + - "id=" + id + + "eventId=" + eventId + ", type=" + type + - ", account=" + account + + ", entity=" + entity + + ", createdAt=" + createdAt + + ", lastModified=" + lastModified + "} " + super.toString(); } } diff --git a/account/account-web/src/main/java/demo/event/AccountEventRepository.java b/account/account-web/src/main/java/demo/event/AccountEventRepository.java new file mode 100644 index 0000000..465d40d --- /dev/null +++ b/account/account-web/src/main/java/demo/event/AccountEventRepository.java @@ -0,0 +1,4 @@ +package demo.event; + +public interface AccountEventRepository extends EventRepository { +} diff --git a/account/account-web/src/main/java/demo/event/AccountEvents.java b/account/account-web/src/main/java/demo/event/AccountEvents.java deleted file mode 100644 index f45e8de..0000000 --- a/account/account-web/src/main/java/demo/event/AccountEvents.java +++ /dev/null @@ -1,74 +0,0 @@ -package demo.event; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import demo.account.Account; -import demo.account.AccountController; -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 AccountEvents} is a hypermedia collection of {@link AccountEvent} resources. - * - * @author kbastani - */ -public class AccountEvents extends Resources implements Serializable { - - private Long accountId; - - /** - * Create a new {@link AccountEvents} hypermedia resources collection for an {@link Account}. - * - * @param accountId is the unique identifier for the {@link Account} - * @param content is the collection of {@link AccountEvents} attached to the {@link Account} - */ - public AccountEvents(Long accountId, List content) { - this(content); - this.accountId = accountId; - - // Add hypermedia links to resources parent - add(linkTo(AccountController.class) - .slash("accounts") - .slash(accountId) - .slash("events") - .withSelfRel(), - linkTo(AccountController.class) - .slash("accounts") - .slash(accountId) - .withRel("account")); - - 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 AccountEvents(Iterable content, Link... links) { - super(content, links); - } - - /** - * Get the {@link Account} identifier that the {@link AccountEvents} apply to. - * - * @return the account identifier - */ - @JsonIgnore - public Long getAccountId() { - return accountId; - } -} diff --git a/account/account-web/src/main/java/demo/event/ConsistencyModel.java b/account/account-web/src/main/java/demo/event/ConsistencyModel.java deleted file mode 100644 index 8bef081..0000000 --- a/account/account-web/src/main/java/demo/event/ConsistencyModel.java +++ /dev/null @@ -1,6 +0,0 @@ -package demo.event; - -public enum ConsistencyModel { - BASE, - ACID -} diff --git a/account/account-web/src/main/java/demo/event/EventController.java b/account/account-web/src/main/java/demo/event/EventController.java deleted file mode 100644 index 125ed8d..0000000 --- a/account/account-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 AccountEvent 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 AccountEvent 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/account/account-web/src/main/java/demo/event/EventRepository.java b/account/account-web/src/main/java/demo/event/EventRepository.java deleted file mode 100644 index 273ba3b..0000000 --- a/account/account-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 findAccountEventsByAccountId(@Param("accountId") Long accountId, Pageable pageable); -} diff --git a/account/account-web/src/main/java/demo/event/EventService.java b/account/account-web/src/main/java/demo/event/EventService.java deleted file mode 100644 index c4c06d9..0000000 --- a/account/account-web/src/main/java/demo/event/EventService.java +++ /dev/null @@ -1,214 +0,0 @@ -package demo.event; - -import demo.account.Account; -import demo.account.AccountController; -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 AccountEvent} - * entities of the Account Service. Account domain events are generated with a {@link AccountEventType}, - * and action logs are appended to the {@link AccountEvent}. - * - * @author kbastani - */ -@Service -@CacheConfig(cacheNames = {"events"}) -public class EventService { - - private final Logger log = Logger.getLogger(EventService.class); - - private final EventRepository eventRepository; - private final Source accountStreamSource; - private final RestTemplate restTemplate; - - public EventService(EventRepository eventRepository, Source accountStreamSource, RestTemplate restTemplate) { - this.eventRepository = eventRepository; - this.accountStreamSource = accountStreamSource; - this.restTemplate = restTemplate; - } - - /** - * Create a new {@link AccountEvent} and append it to the event log of the referenced {@link Account}. - * After the {@link AccountEvent} has been persisted, send the event to the account stream. Events can - * be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}. - * - * @param accountId is the unique identifier for the {@link Account} - * @param event is the {@link AccountEvent} to create - * @param consistencyModel is the desired consistency model for the response - * @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log - */ - public AccountEvent createEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) { - event = createEvent(accountId, event); - return raiseEvent(event, consistencyModel); - } - - /** - * Raise an {@link AccountEvent} that attempts to transition the state of an {@link Account}. - * - * @param event is an {@link AccountEvent} that will be raised - * @param consistencyModel is the consistency model for this request - * @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log - */ - public AccountEvent raiseEvent(AccountEvent event, ConsistencyModel consistencyModel) { - switch (consistencyModel) { - case BASE: - asyncRaiseEvent(event); - break; - case ACID: - event = raiseEvent(event); - break; - } - - return event; - } - - /** - * Raise an asynchronous {@link AccountEvent} by sending an AMQP message to the account stream. Any - * state changes will be applied to the {@link Account} 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 AccountEvent} that will be raised - */ - private void asyncRaiseEvent(AccountEvent event) { - // Append the account event to the stream - accountStreamSource.output() - .send(MessageBuilder - .withPayload(getAccountEventResource(event)) - .build()); - } - - /** - * Raise a synchronous {@link AccountEvent} by sending a HTTP request to the account 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 AccountEvent} that will be raised - * @return an {@link AccountEvent} which contains the consistent state of an {@link Account} - */ - private AccountEvent raiseEvent(AccountEvent event) { - try { - // Create a new request entity - RequestEntity> requestEntity = RequestEntity.post( - URI.create("http://localhost:8081/v1/events")) - .contentType(MediaTypes.HAL_JSON) - .body(getAccountEventResource(event), Resource.class); - - // Update the account entity's status - Account result = restTemplate.exchange(requestEntity, Account.class) - .getBody(); - - log.info(result); - event.setAccount(result); - } catch (Exception ex) { - log.error(ex); - } - - return event; - } - - - /** - * Create a new {@link AccountEvent} and publish it to the account stream. - * - * @param event is the {@link AccountEvent} to publish to the account stream - * @return a hypermedia {@link AccountEvent} resource - */ - @CacheEvict(cacheNames = "events", key = "#id.toString()") - public AccountEvent createEvent(Long id, AccountEvent event) { - // Save new event - event = addEvent(event); - Assert.notNull(event, "The event could not be appended to the account"); - - return event; - } - - /** - * Get an {@link AccountEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link AccountEvent} - * @return an {@link AccountEvent} - */ - public Resource getEvent(Long id) { - return getAccountEventResource(eventRepository.findOne(id)); - } - - /** - * Update an {@link AccountEvent} with the supplied identifier. - * - * @param id is the unique identifier for the {@link AccountEvent} - * @param event is the {@link AccountEvent} to update - * @return the updated {@link AccountEvent} - */ - @CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()") - public AccountEvent updateEvent(Long id, AccountEvent event) { - Assert.notNull(id); - Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId())); - - return eventRepository.save(event); - } - - /** - * Get {@link AccountEvents} for the supplied {@link Account} identifier. - * - * @param id is the unique identifier of the {@link Account} - * @return a list of {@link AccountEvent} wrapped in a hypermedia {@link AccountEvents} resource - */ - @Cacheable(cacheNames = "events", key = "#id.toString()") - public List getAccountEvents(Long id) { - return eventRepository.findAccountEventsByAccountId(id, - new PageRequest(0, Integer.MAX_VALUE)).getContent(); - } - - /** - * Gets a hypermedia resource for a {@link AccountEvent} entity. - * - * @param event is the {@link AccountEvent} to enrich with hypermedia - * @return a hypermedia resource for the supplied {@link AccountEvent} entity - */ - private Resource getAccountEventResource(AccountEvent event) { - return new Resource(event, Arrays.asList( - linkTo(AccountController.class) - .slash("events") - .slash(event.getEventId()) - .withSelfRel(), - linkTo(AccountController.class) - .slash("accounts") - .slash(event.getAccount().getAccountId()) - .withRel("account"))); - } - - /** - * Add a {@link AccountEvent} to an {@link Account} entity. - * - * @param event is the {@link AccountEvent} to append to an {@link Account} entity - * @return the newly appended {@link AccountEvent} entity - */ - @CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()") - private AccountEvent addEvent(AccountEvent event) { - event = eventRepository.saveAndFlush(event); - return event; - } -} diff --git a/account/account-web/src/main/resources/application.yml b/account/account-web/src/main/resources/application.yml index e66280b..3210601 100644 --- a/account/account-web/src/main/resources/application.yml +++ b/account/account-web/src/main/resources/application.yml @@ -10,8 +10,5 @@ spring: output: destination: account contentType: 'application/json' - redis: - host: localhost - port: 6379 server: port: 8080 \ No newline at end of file diff --git a/account/account-web/src/test/java/demo/account/AccountControllerTest.java b/account/account-web/src/test/java/demo/account/AccountControllerTest.java index 0ae456d..ffe5c8f 100644 --- a/account/account-web/src/test/java/demo/account/AccountControllerTest.java +++ b/account/account-web/src/test/java/demo/account/AccountControllerTest.java @@ -1,6 +1,10 @@ package demo.account; +import demo.account.controller.AccountController; +import demo.event.AccountEvent; +import demo.event.AccountEventType; import demo.event.EventService; +import demo.event.Events; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -10,6 +14,8 @@ import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import java.util.Collections; + import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -26,18 +32,22 @@ public class AccountControllerTest { private AccountService accountService; @MockBean - private EventService eventService; + private EventService eventService; @Test public void getUserAccountResourceShouldReturnAccount() throws Exception { String content = "{\"firstName\": \"Jane\", \"lastName\": \"Doe\", \"email\": \"jane.doe@example.com\"}"; Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); - given(this.accountService.getAccount(1L)) - .willReturn(account); + given(this.accountService.get(1L)).willReturn(account); + given(this.eventService.find(1L)).willReturn(new Events<>(1L, Collections + .singletonList(new AccountEvent(AccountEventType + .ACCOUNT_CREATED)))); this.mvc.perform(get("/v1/accounts/1").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(content().json(content)); + .andExpect(status().isOk()) + .andExpect(content().json(content)); } } diff --git a/account/account-web/src/test/java/demo/account/AccountServiceTests.java b/account/account-web/src/test/java/demo/account/AccountServiceTests.java index 0e7e33d..c3511d0 100644 --- a/account/account-web/src/test/java/demo/account/AccountServiceTests.java +++ b/account/account-web/src/test/java/demo/account/AccountServiceTests.java @@ -3,42 +3,36 @@ package demo.account; import demo.event.AccountEvent; import demo.event.AccountEventType; import demo.event.EventService; -import org.junit.Before; 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.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; import static org.mockito.BDDMockito.given; @RunWith(SpringRunner.class) +@SpringBootTest public class AccountServiceTests { @MockBean - private EventService eventService; + private EventService eventService; @MockBean private AccountRepository accountRepository; - @MockBean - private CacheManager cacheManager; - + @Autowired private AccountService accountService; - @Before - public void before() { - accountService = new AccountService(accountRepository, eventService, cacheManager); - } - @Test public void getAccountReturnsAccount() throws Exception { Account expected = new Account("Jane", "Doe", "jane.doe@example.com"); given(this.accountRepository.findOne(1L)).willReturn(expected); - Account actual = accountService.getAccount(1L); + Account actual = accountService.get(1L); assertThat(actual).isNotNull(); assertThat(actual.getEmail()).isEqualTo("jane.doe@example.com"); @@ -49,12 +43,12 @@ public class AccountServiceTests { @Test public void createAccountReturnsAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); - account.setAccountId(1L); + account.setIdentity(1L); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.saveAndFlush(account)).willReturn(account); - Account actual = accountService.createAccount(account); + Account actual = accountService.create(account); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_CREATED); @@ -66,19 +60,20 @@ public class AccountServiceTests { @Test public void applyCommandSuspendsAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_ACTIVE); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.SUSPEND_ACCOUNT); + Account actual = account.suspend(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_SUSPENDED); @@ -87,19 +82,20 @@ public class AccountServiceTests { @Test public void applyCommandUnsuspendsAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_SUSPENDED); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); + Account actual = account.activate(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE); @@ -108,19 +104,20 @@ public class AccountServiceTests { @Test public void applyCommandArchivesAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_ACTIVE); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.ARCHIVE_ACCOUNT); + Account actual = account.archive(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ARCHIVED); @@ -129,19 +126,20 @@ public class AccountServiceTests { @Test public void applyCommandUnarchivesAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_ARCHIVED); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); + Account actual = account.activate(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE); @@ -150,19 +148,20 @@ public class AccountServiceTests { @Test public void applyCommandConfirmsAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_PENDING); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.CONFIRM_ACCOUNT); + Account actual = account.confirm(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_CONFIRMED); @@ -171,20 +170,20 @@ public class AccountServiceTests { @Test public void applyCommandActivatesAccount() throws Exception { Account account = new Account("Jane", "Doe", "jane.doe@example.com"); + account.setIdentity(1L); account.setStatus(AccountStatus.ACCOUNT_CONFIRMED); - AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); - accountEvent.setAccount(account); + accountEvent.setEntity(account); accountEvent.setEventId(1L); + AccountEvent expected = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED, account); given(this.accountRepository.findOne(1L)).willReturn(account); given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); - given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) - .willReturn(accountEvent); + given(this.eventService.send(expected)).willReturn(accountEvent); - Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); + Account actual = account.activate(); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_ACTIVE); diff --git a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java index a504e6e..a51e9f7 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java @@ -11,6 +11,11 @@ import org.springframework.stereotype.Service; import java.util.function.BiConsumer; +/** + * Connects an {@link Order} to an Account. + * + * @author Kenny Bastani + */ @Service public class ConnectAccount extends Action { @@ -25,7 +30,7 @@ public class ConnectAccount extends Action { order = orderService.update(order); // Trigger the account connected event - order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED)); + order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED, order)); }; } } diff --git a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java index ef830d5..37594da 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java @@ -11,6 +11,11 @@ import org.springframework.stereotype.Service; import java.util.function.BiConsumer; +/** + * Connects a {@link demo.payment.Payment} to an {@link Order}. + * + * @author Kenny Bastani + */ @Service public class ConnectPayment extends Action { public BiConsumer getConsumer() { @@ -25,7 +30,7 @@ public class ConnectPayment extends Action { order = orderService.update(order); // Trigger the account connected event - order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED)); + order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED, order)); }; } } diff --git a/order/order-web/src/main/java/demo/order/action/CreatePayment.java b/order/order-web/src/main/java/demo/order/action/CreatePayment.java index ddcd71f..704951b 100644 --- a/order/order-web/src/main/java/demo/order/action/CreatePayment.java +++ b/order/order-web/src/main/java/demo/order/action/CreatePayment.java @@ -20,6 +20,11 @@ import org.springframework.web.client.RestTemplate; import java.net.URI; import java.util.function.Consumer; +/** + * Creates a {@link Payment} for an {@link Order}. + * + * @author Kenny Bastani + */ @Service public class CreatePayment extends Action { diff --git a/order/order-web/src/main/java/demo/order/action/ProcessPayment.java b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java index faa837e..f53dc2c 100644 --- a/order/order-web/src/main/java/demo/order/action/ProcessPayment.java +++ b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java @@ -6,6 +6,11 @@ import org.springframework.stereotype.Service; import java.util.function.Consumer; +/** + * Processes a {@link demo.payment.Payment} for an {@link Order}. + * + * @author Kenny Bastani + */ @Service public class ProcessPayment extends Action { public Consumer getConsumer() { diff --git a/order/order-web/src/main/java/demo/order/action/ReserveInventory.java b/order/order-web/src/main/java/demo/order/action/ReserveInventory.java index 5a7d434..e886ef9 100644 --- a/order/order-web/src/main/java/demo/order/action/ReserveInventory.java +++ b/order/order-web/src/main/java/demo/order/action/ReserveInventory.java @@ -6,6 +6,11 @@ import org.springframework.stereotype.Service; import java.util.function.Consumer; +/** + * Reserves inventory for an {@link Order}. + * + * @author Kenny Bastani + */ @Service public class ReserveInventory extends Action { public Consumer getConsumer() {