diff --git a/account-parent/account-web/src/main/java/demo/AccountResourceConfig.java b/account-parent/account-web/src/main/java/demo/AccountResourceConfig.java new file mode 100644 index 0000000..302159d --- /dev/null +++ b/account-parent/account-web/src/main/java/demo/AccountResourceConfig.java @@ -0,0 +1,36 @@ +package demo; + +import demo.account.Account; +import demo.account.AccountController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceProcessor; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +@Configuration +public class AccountResourceConfig { + + /** + * Enriches the {@link Account} resource with hypermedia links. + * + * @return a hypermedia processor for the {@link Account} resource + */ + @Bean + public ResourceProcessor> accountProcessor() { + return new ResourceProcessor>() { + @Override + public Resource process(Resource resource) { + resource.add( + linkTo(AccountController.class) + .slash("accounts") + .slash(resource.getContent().getAccountId()) + .slash("commands") + .withRel("commands")); + return resource; + } + }; + } + +} diff --git a/account-parent/account-web/src/main/java/demo/event/StreamConfig.java b/account-parent/account-web/src/main/java/demo/AccountStreamConfig.java similarity index 51% rename from account-parent/account-web/src/main/java/demo/event/StreamConfig.java rename to account-parent/account-web/src/main/java/demo/AccountStreamConfig.java index 5a49c07..d469042 100644 --- a/account-parent/account-web/src/main/java/demo/event/StreamConfig.java +++ b/account-parent/account-web/src/main/java/demo/AccountStreamConfig.java @@ -1,10 +1,10 @@ -package demo.event; +package demo; import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.messaging.Source; import org.springframework.context.annotation.Configuration; @Configuration -@EnableBinding(ProducerChannels.class) -public class StreamConfig { - +@EnableBinding(Source.class) +public class AccountStreamConfig { } diff --git a/account-parent/account-web/src/main/java/demo/account/Account.java b/account-parent/account-web/src/main/java/demo/account/Account.java index 396c0a9..1cc633c 100644 --- a/account-parent/account-web/src/main/java/demo/account/Account.java +++ b/account-parent/account-web/src/main/java/demo/account/Account.java @@ -1,5 +1,6 @@ package demo.account; +import com.fasterxml.jackson.annotation.JsonIgnore; import demo.domain.BaseEntity; import demo.event.AccountEvent; @@ -12,6 +13,8 @@ import java.util.Set; * 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 */ @Entity public class Account extends BaseEntity { @@ -39,11 +42,12 @@ public class Account extends BaseEntity { this.status = status; } - public Long getId() { + @JsonIgnore + public Long getAccountId() { return id; } - public void setId(Long id) { + public void setAccountId(Long id) { this.id = id; } diff --git a/account-parent/account-web/src/main/java/demo/account/AccountCommand.java b/account-parent/account-web/src/main/java/demo/account/AccountCommand.java index b13b88d..479982f 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountCommand.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountCommand.java @@ -1,5 +1,12 @@ 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 AccountEventStatus} values. + * + * @author kbastani + */ public enum AccountCommand { CONFIRM_ACCOUNT, ACTIVATE_ACCOUNT, diff --git a/account-parent/account-web/src/main/java/demo/account/AccountCommandsResource.java b/account-parent/account-web/src/main/java/demo/account/AccountCommandsResource.java index d5ede86..14390d0 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountCommandsResource.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountCommandsResource.java @@ -2,5 +2,11 @@ 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-parent/account-web/src/main/java/demo/account/AccountController.java b/account-parent/account-web/src/main/java/demo/account/AccountController.java index d3a8be6..42d778b 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountController.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountController.java @@ -38,6 +38,13 @@ public class AccountController { .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } + @GetMapping(path = "/accounts/{id}/events") + public ResponseEntity getAccountEvents(@PathVariable Long id) { + return Optional.ofNullable(accountService.getAccountEventResources(id)) + .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + .orElseThrow(() -> new IllegalArgumentException("Could not get account events")); + } + @PostMapping(path = "/accounts/{id}/events") public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) { return Optional.ofNullable(accountService.appendEventResource(id, event)) diff --git a/account-parent/account-web/src/main/java/demo/account/AccountEventStatus.java b/account-parent/account-web/src/main/java/demo/account/AccountEventStatus.java index cf70cb0..1559991 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountEventStatus.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountEventStatus.java @@ -4,6 +4,8 @@ package demo.account; * The {@link AccountEventStatus} describes the state of an {@link Account}. * The aggregate state of a {@link Account} is sourced from attached domain * events in the form of {@link demo.event.AccountEvent}. + * + * @author kbastani */ public enum AccountEventStatus { ACCOUNT_CREATED, diff --git a/account-parent/account-web/src/main/java/demo/account/AccountEventType.java b/account-parent/account-web/src/main/java/demo/account/AccountEventType.java index 5401068..1484c72 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountEventType.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountEventType.java @@ -1,5 +1,11 @@ package demo.account; +/** + * The {@link AccountEventType} represents a collection of possible events that describe + * state transitions of {@link AccountEventStatus} on the {@link Account} aggregate. + * + * @author kbastani + */ public enum AccountEventType { ACCOUNT_CREATED, ACCOUNT_CONFIRMED, diff --git a/account-parent/account-web/src/main/java/demo/account/AccountEvents.java b/account-parent/account-web/src/main/java/demo/account/AccountEvents.java new file mode 100644 index 0000000..2ab8bb9 --- /dev/null +++ b/account-parent/account-web/src/main/java/demo/account/AccountEvents.java @@ -0,0 +1,56 @@ +package demo.account; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import demo.event.AccountEvent; +import demo.event.EventController; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resources; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +public class AccountEvents extends Resources { + + private Long accountId; + + /** + * Creates an empty {@link Resources} instance. + */ + public AccountEvents(Long accountId, Iterable 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")); + + // Add hypermedia links to each item of the collection + content.forEach(event -> event.add( + linkTo(EventController.class) + .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}. + */ + public AccountEvents(Iterable content, Link... links) { + super(content, links); + } + + @JsonIgnore + public Long getAccountId() { + return accountId; + } +} diff --git a/account-parent/account-web/src/main/java/demo/account/AccountRepository.java b/account-parent/account-web/src/main/java/demo/account/AccountRepository.java index 0e0a73f..d19b565 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountRepository.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountRepository.java @@ -6,6 +6,5 @@ import org.springframework.data.repository.query.Param; public interface AccountRepository extends JpaRepository { Account findAccountByUserId(@Param("userId") Long userId); - Account findAccountByAccountNumber(@Param("accountNumber") String accountNumber); } diff --git a/account-parent/account-web/src/main/java/demo/account/AccountService.java b/account-parent/account-web/src/main/java/demo/account/AccountService.java index e492f11..0ebf5ed 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountService.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountService.java @@ -21,6 +21,8 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * 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 @Transactional @@ -101,17 +103,15 @@ public class AccountService { if (event != null) { eventResource = new Resource<>(event, - entityLinks.linkFor(AccountEvent.class, event.getId()) - .slash(event.getId()) + linkTo(AccountController.class) + .slash("accounts") + .slash(accountId) + .slash("events") .withSelfRel(), linkTo(AccountController.class) .slash("accounts") .slash(accountId) - .withRel("account"), - entityLinks.linkFor(AccountEvent.class, event.getId()) - .slash(event.getId()) - .slash("logs") - .withRel("logs") + .withRel("account") ); } @@ -128,55 +128,61 @@ public class AccountService { public Resource applyCommand(Long id, AccountCommand accountCommand) { Resource account = getAccountResource(id); - if (account != null) { + Assert.notNull(account, "The account for the supplied id could not be found"); - AccountEventStatus status = account.getContent().getStatus(); + AccountEventStatus status = account.getContent().getStatus(); - switch (accountCommand) { - case CONFIRM_ACCOUNT: - Assert.isTrue(status == ACCOUNT_CREATED, "The account has already been confirmed"); + switch (accountCommand) { + case CONFIRM_ACCOUNT: + Assert.isTrue(status == ACCOUNT_CREATED, "The account has already been confirmed"); - // Confirm the account - Account updateAccount = account.getContent(); - updateAccount.setStatus(ACCOUNT_CONFIRMED); - account = updateAccountResource(id, updateAccount); - appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)); - 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"); + // Confirm the account + Account updateAccount = account.getContent(); + updateAccount.setStatus(ACCOUNT_CONFIRMED); + account = updateAccountResource(id, updateAccount); + appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)); + 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.getContent().setStatus(ACCOUNT_ACTIVE); - account = updateAccountResource(id, account.getContent()); - appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)); - break; - case SUSPEND_ACCOUNT: - Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); + // Activate the account + account.getContent().setStatus(ACCOUNT_ACTIVE); + account = updateAccountResource(id, account.getContent()); + appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)); + break; + case SUSPEND_ACCOUNT: + Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); - // Suspend the account - account.getContent().setStatus(ACCOUNT_SUSPENDED); - account = updateAccountResource(id, account.getContent()); - appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)); - break; - case ARCHIVE_ACCOUNT: - Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); + // Suspend the account + account.getContent().setStatus(ACCOUNT_SUSPENDED); + account = updateAccountResource(id, account.getContent()); + 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.getContent().setStatus(ACCOUNT_ARCHIVED); - account = updateAccountResource(id, account.getContent()); - 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"); - } + // Archive the account + account.getContent().setStatus(ACCOUNT_ARCHIVED); + account = updateAccountResource(id, account.getContent()); + 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; } + /** + * 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 + */ public AccountCommandsResource getCommandsResource(Long id) { // Get the account resource for the identifier Resource accountResource = getAccountResource(id); @@ -205,6 +211,22 @@ public class AccountService { 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 + */ + public AccountEvents getAccountEventResources(Long id) { + return eventService.getEvents(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") @@ -212,6 +234,12 @@ public class AccountService { .slash("commands"); } + /** + * Get a hypermedia enriched {@link Account} entity. + * + * @param account is the {@link Account} to enrich with hypermedia links + * @return is a hypermedia enriched resource for the supplied {@link Account} entity + */ private Resource getAccountResource(Account account) { Resource accountResource; @@ -219,23 +247,36 @@ public class AccountService { accountResource = new Resource<>(account, linkTo(AccountController.class) .slash("accounts") - .slash(account.getId()) + .slash(account.getAccountId()) .withSelfRel(), - entityLinks.linkFor(Account.class, account.getId()) - .slash(account.getId()) + linkTo(AccountController.class) + .slash("accounts") + .slash(account.getAccountId()) .slash("events") .withRel("events"), - getCommandLinkBuilder(account.getId()) + getCommandLinkBuilder(account.getAccountId()) .withRel("commands") ); return accountResource; } + /** + * Get an {@link Account} entity for the supplied identifier. + * + * @param id is the unique identifier of a {@link Account} entity + * @return an {@link Account} entity + */ private Account getAccount(Long id) { return accountRepository.findOne(id); } + /** + * Create a new {@link Account} entity. + * + * @param account is the {@link Account} to create + * @return the newly created {@link Account} + */ private Account createAccount(Account account) { // Assert for uniqueness constraint Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()), @@ -247,32 +288,40 @@ public class AccountService { account = accountRepository.save(account); // Trigger the account creation event - appendEventResource(account.getId(), + appendEventResource(account.getAccountId(), new AccountEvent(AccountEventType.ACCOUNT_CREATED)); return account; } + /** + * 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 + */ private Account updateAccount(Long id, Account account) { Assert.notNull(id); Assert.notNull(account); - Assert.isTrue(Objects.equals(id, account.getId())); + Assert.isTrue(Objects.equals(id, account.getAccountId())); return accountRepository.save(account); } + /** + * 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} + */ private AccountEvent appendEvent(Long accountId, AccountEvent event) { - Account account = accountRepository.findOne(accountId); - if (account != null) { - event.setAccount(account); - event = eventService.createEvent(event).getContent(); - if (event != null) { - account.getEvents().add(event); - accountRepository.save(account); - } - } else { - event = null; - } - + Account account = getAccount(accountId); + Assert.notNull(account, "The account with the supplied id does not exist"); + event.setAccount(account); + event = eventService.createEvent(event).getContent(); + account.getEvents().add(event); + accountRepository.save(account); return event; } } diff --git a/account-parent/account-web/src/main/java/demo/domain/BaseEntity.java b/account-parent/account-web/src/main/java/demo/domain/BaseEntity.java index fcd62be..db285dc 100644 --- a/account-parent/account-web/src/main/java/demo/domain/BaseEntity.java +++ b/account-parent/account-web/src/main/java/demo/domain/BaseEntity.java @@ -3,13 +3,14 @@ package demo.domain; 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; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class BaseEntity { +public class BaseEntity extends ResourceSupport { @CreatedDate private Long createdAt; diff --git a/account-parent/account-web/src/main/java/demo/event/AccountEvent.java b/account-parent/account-web/src/main/java/demo/event/AccountEvent.java index 8f311e1..9b816dc 100644 --- a/account-parent/account-web/src/main/java/demo/event/AccountEvent.java +++ b/account-parent/account-web/src/main/java/demo/event/AccountEvent.java @@ -19,6 +19,8 @@ import java.util.Set; * This event resource also provides a transaction log that can be used to append * actions to the event. The collection of {@link Log} items can be used to remediate * partial failures. + * + * @author kbastani */ @Entity @RestResource(path = "events", rel = "events") @@ -46,11 +48,12 @@ public class AccountEvent extends BaseEntity { this.type = type; } - public Long getId() { + @JsonIgnore + public Long getEventId() { return id; } - public void setId(Long id) { + public void setEventId(Long id) { this.id = id; } diff --git a/account-parent/account-web/src/main/java/demo/event/EventRepository.java b/account-parent/account-web/src/main/java/demo/event/EventRepository.java index af97e8d..d54e522 100644 --- a/account-parent/account-web/src/main/java/demo/event/EventRepository.java +++ b/account-parent/account-web/src/main/java/demo/event/EventRepository.java @@ -1,8 +1,12 @@ 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; import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(path = "events", collectionResourceRel = "events", itemResourceRel = "event") public interface EventRepository extends JpaRepository { + Page findAccountEventsByAccountId(@Param("accountId") Long accountId, Pageable pageable); } diff --git a/account-parent/account-web/src/main/java/demo/event/EventService.java b/account-parent/account-web/src/main/java/demo/event/EventService.java index cb7c1b3..bdafddc 100644 --- a/account-parent/account-web/src/main/java/demo/event/EventService.java +++ b/account-parent/account-web/src/main/java/demo/event/EventService.java @@ -3,8 +3,12 @@ package demo.event; import demo.account.Account; import demo.account.AccountController; import demo.account.AccountEventType; +import demo.account.AccountEvents; import demo.log.Log; import demo.log.LogRepository; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.hateoas.Resource; import org.springframework.integration.support.MessageBuilder; @@ -22,6 +26,8 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * entities of the Account Service. Account domain events are generated with a {@link AccountEventType}, * and action logs are appended to the {@link AccountEvent}. The logs resource provides an append-only transaction * log that can be used to source the state of the {@link Account} + * + * @author kbastani */ @Service @Transactional @@ -30,57 +36,58 @@ public class EventService { private final EventRepository eventRepository; private final LogRepository logRepository; private final RepositoryEntityLinks entityLinks; - private final ProducerChannels producer; + private final Source accountStreamSource; public EventService(EventRepository eventRepository, LogRepository logRepository, - RepositoryEntityLinks entityLinks, ProducerChannels producerChannels) { + RepositoryEntityLinks entityLinks, Source accountStreamSource) { this.eventRepository = eventRepository; this.logRepository = logRepository; this.entityLinks = entityLinks; - this.producer = producerChannels; + this.accountStreamSource = accountStreamSource; } + /** + * 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 + */ public Resource createEvent(AccountEvent event) { Resource eventResource = null; // Save new event event = addEvent(event); + Assert.notNull(event, "The event could not be appended to the account"); - if (event != null) { - // Create account event resource - eventResource = new Resource<>(event, Arrays.asList( - entityLinks.linkFor(AccountEvent.class, event.getId()) - .slash(event.getId()) - .withRel("self"), - entityLinks.linkFor(AccountEvent.class) - .slash(event.getId()) - .slash("logs") - .withRel("logs"), - linkTo(AccountController.class) - .slash("accounts") - .slash(event.getAccount().getId()) - .withRel("account")) - ); + // Create account event resource + eventResource = getAccountEventResource(event); - // Produce account event - producer.output() - .send(MessageBuilder - .withPayload(eventResource) - .build()); - } + // Append the account event to the stream + accountStreamSource.output() + .send(MessageBuilder + .withPayload(eventResource) + .build()); return eventResource; } - private AccountEvent addEvent(AccountEvent event) { - event = eventRepository.save(event); - 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 AccountEvent getEvent(Long id) { return 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} + */ public AccountEvent updateEvent(Long id, AccountEvent event) { Assert.notNull(id); Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId())); @@ -88,6 +95,13 @@ public class EventService { return eventRepository.save(event); } + /** + * Append a {@link Log} to an {@link AccountEvent} entity. + * + * @param eventId is the unique identifier for an {@link AccountEvent} + * @param log is the {@link Log} descirbing an action performed on an {@link AccountEvent} + * @return a hypermedia resource for the appended {@link Log} + */ public Resource appendEventLog(Long eventId, Log log) { Assert.notNull(eventId); Assert.notNull(log); @@ -95,20 +109,75 @@ public class EventService { Resource logResource = null; AccountEvent event = getEvent(eventId); - if (event != null) { - log = logRepository.save(log); - event.getLogs().add(log); + Assert.notNull(event, "The event with the supplied id could not be found"); - logResource = new Resource<>(log, Arrays.asList( - entityLinks.linkFor(Log.class) - .slash(log.getId()) - .withSelfRel(), - entityLinks.linkFor(AccountEvent.class) - .slash(event.getId()) - .withRel("event") - )); - } + log = logRepository.save(log); + event.getLogs().add(log); + logResource = getLogResource(log, event); return logResource; } + + /** + * Gets a hypermedia resource for a {@link Log} entity. + * + * @param log is the {@link Log} descirbing an action performed on an {@link AccountEvent} + * @param event is the {@link AccountEvent} to associate with the {@link Log} + * @return a hypermedia resource for the appended {@link Log} + */ + private Resource getLogResource(Log log, AccountEvent event) { + return new Resource<>(log, Arrays.asList( + entityLinks.linkFor(Log.class) + .slash(log.getLogId()) + .withSelfRel(), + entityLinks.linkFor(AccountEvent.class) + .slash(event.getId()) + .withRel("event") + )); + } + + /** + * 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(), + entityLinks.linkFor(AccountEvent.class) + .slash(event.getId()) + .slash("logs") + .withRel("logs"), + 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 + */ + private AccountEvent addEvent(AccountEvent event) { + event = eventRepository.save(event); + return 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 + */ + public AccountEvents getEvents(Long id) { + Page events = eventRepository.findAccountEventsByAccountId(id, new PageRequest(0, Integer.MAX_VALUE)); + return new AccountEvents(id, events); + } } diff --git a/account-parent/account-web/src/main/java/demo/event/ProducerChannels.java b/account-parent/account-web/src/main/java/demo/event/ProducerChannels.java deleted file mode 100644 index e47053c..0000000 --- a/account-parent/account-web/src/main/java/demo/event/ProducerChannels.java +++ /dev/null @@ -1,10 +0,0 @@ -package demo.event; - -import org.springframework.cloud.stream.annotation.Output; -import org.springframework.messaging.MessageChannel; - -public interface ProducerChannels { - - @Output - MessageChannel output(); -} \ No newline at end of file diff --git a/account-parent/account-web/src/main/java/demo/log/Log.java b/account-parent/account-web/src/main/java/demo/log/Log.java index adc94f7..07273a5 100644 --- a/account-parent/account-web/src/main/java/demo/log/Log.java +++ b/account-parent/account-web/src/main/java/demo/log/Log.java @@ -1,5 +1,6 @@ package demo.log; +import com.fasterxml.jackson.annotation.JsonIgnore; import demo.account.AccountEventType; import demo.domain.BaseEntity; @@ -25,11 +26,12 @@ public class Log extends BaseEntity { this.action = action; } - public Long getId() { + @JsonIgnore + public Long getLogId() { return id; } - public void setId(Long id) { + public void setLogId(Long id) { this.id = id; } diff --git a/account-parent/account-worker/pom.xml b/account-parent/account-worker/pom.xml index 0a1b01a..8c287b6 100644 --- a/account-parent/account-worker/pom.xml +++ b/account-parent/account-worker/pom.xml @@ -35,10 +35,6 @@ org.springframework.cloud spring-cloud-starter-stream-rabbit - - org.springframework.boot - spring-boot-starter-data-jpa - org.springframework.boot spring-boot-starter-integration @@ -49,10 +45,6 @@ 1.1.1.RELEASE - - com.h2database - h2 - com.jayway.jsonpath json-path diff --git a/account-parent/account-worker/src/main/java/demo/domain/BaseEntity.java b/account-parent/account-worker/src/main/java/demo/domain/BaseEntity.java index db285dc..586dbb4 100644 --- a/account-parent/account-worker/src/main/java/demo/domain/BaseEntity.java +++ b/account-parent/account-worker/src/main/java/demo/domain/BaseEntity.java @@ -1,21 +1,10 @@ package demo.domain; -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; - -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) public class BaseEntity extends ResourceSupport { - @CreatedDate private Long createdAt; - - @LastModifiedDate private Long lastModified; public BaseEntity() { @@ -42,6 +31,6 @@ public class BaseEntity extends ResourceSupport { return "BaseEntity{" + "createdAt=" + createdAt + ", lastModified=" + lastModified + - '}'; + "} " + super.toString(); } } diff --git a/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java b/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java index 36a8045..1bc1f11 100644 --- a/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java +++ b/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java @@ -8,6 +8,7 @@ import org.apache.log4j.Logger; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.cloud.stream.messaging.Sink; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.client.Traverson; import org.springframework.messaging.MessageHeaders; diff --git a/account-parent/account-worker/src/main/java/demo/event/Sink.java b/account-parent/account-worker/src/main/java/demo/event/Sink.java deleted file mode 100644 index 7fac286..0000000 --- a/account-parent/account-worker/src/main/java/demo/event/Sink.java +++ /dev/null @@ -1,11 +0,0 @@ -package demo.event; - -import org.springframework.cloud.stream.annotation.Input; -import org.springframework.messaging.SubscribableChannel; - -public interface Sink { - String INPUT = "input"; - - @Input(INPUT) - SubscribableChannel input(); -}