Javadoc comments and hypermedia tweaks
This commit is contained in:
@@ -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<Resource<Account>> accountProcessor() {
|
||||||
|
return new ResourceProcessor<Resource<Account>>() {
|
||||||
|
@Override
|
||||||
|
public Resource<Account> process(Resource<Account> resource) {
|
||||||
|
resource.add(
|
||||||
|
linkTo(AccountController.class)
|
||||||
|
.slash("accounts")
|
||||||
|
.slash(resource.getContent().getAccountId())
|
||||||
|
.slash("commands")
|
||||||
|
.withRel("commands"));
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package demo.event;
|
package demo;
|
||||||
|
|
||||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||||
|
import org.springframework.cloud.stream.messaging.Source;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableBinding(ProducerChannels.class)
|
@EnableBinding(Source.class)
|
||||||
public class StreamConfig {
|
public class AccountStreamConfig {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package demo.account;
|
package demo.account;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import demo.domain.BaseEntity;
|
import demo.domain.BaseEntity;
|
||||||
import demo.event.AccountEvent;
|
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
|
* a user's account. The status of an account is event sourced using
|
||||||
* events logged to the {@link AccountEvent} collection attached to
|
* events logged to the {@link AccountEvent} collection attached to
|
||||||
* this resource.
|
* this resource.
|
||||||
|
*
|
||||||
|
* @author kbastani
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
public class Account extends BaseEntity {
|
public class Account extends BaseEntity {
|
||||||
@@ -39,11 +42,12 @@ public class Account extends BaseEntity {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
@JsonIgnore
|
||||||
|
public Long getAccountId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(Long id) {
|
public void setAccountId(Long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package demo.account;
|
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 {
|
public enum AccountCommand {
|
||||||
CONFIRM_ACCOUNT,
|
CONFIRM_ACCOUNT,
|
||||||
ACTIVATE_ACCOUNT,
|
ACTIVATE_ACCOUNT,
|
||||||
|
|||||||
@@ -2,5 +2,11 @@ package demo.account;
|
|||||||
|
|
||||||
import org.springframework.hateoas.ResourceSupport;
|
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 {
|
public class AccountCommandsResource extends ResourceSupport {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ public class AccountController {
|
|||||||
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
|
.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")
|
@PostMapping(path = "/accounts/{id}/events")
|
||||||
public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) {
|
public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) {
|
||||||
return Optional.ofNullable(accountService.appendEventResource(id, event))
|
return Optional.ofNullable(accountService.appendEventResource(id, event))
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ package demo.account;
|
|||||||
* The {@link AccountEventStatus} describes the state of an {@link Account}.
|
* The {@link AccountEventStatus} describes the state of an {@link Account}.
|
||||||
* The aggregate state of a {@link Account} is sourced from attached domain
|
* The aggregate state of a {@link Account} is sourced from attached domain
|
||||||
* events in the form of {@link demo.event.AccountEvent}.
|
* events in the form of {@link demo.event.AccountEvent}.
|
||||||
|
*
|
||||||
|
* @author kbastani
|
||||||
*/
|
*/
|
||||||
public enum AccountEventStatus {
|
public enum AccountEventStatus {
|
||||||
ACCOUNT_CREATED,
|
ACCOUNT_CREATED,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package demo.account;
|
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 {
|
public enum AccountEventType {
|
||||||
ACCOUNT_CREATED,
|
ACCOUNT_CREATED,
|
||||||
ACCOUNT_CONFIRMED,
|
ACCOUNT_CONFIRMED,
|
||||||
|
|||||||
@@ -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<AccountEvent> {
|
||||||
|
|
||||||
|
private Long accountId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty {@link Resources} instance.
|
||||||
|
*/
|
||||||
|
public AccountEvents(Long accountId, Iterable<AccountEvent> 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<AccountEvent> content, Link... links) {
|
||||||
|
super(content, links);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public Long getAccountId() {
|
||||||
|
return accountId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,5 @@ import org.springframework.data.repository.query.Param;
|
|||||||
public interface AccountRepository extends JpaRepository<Account, Long> {
|
public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||||
|
|
||||||
Account findAccountByUserId(@Param("userId") Long userId);
|
Account findAccountByUserId(@Param("userId") Long userId);
|
||||||
|
|
||||||
Account findAccountByAccountNumber(@Param("accountNumber") String accountNumber);
|
Account findAccountByAccountNumber(@Param("accountNumber") String accountNumber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* actions that can be used to support remediation for distributed transactions that encountered
|
||||||
* a partial failure.
|
* a partial failure.
|
||||||
|
*
|
||||||
|
* @author kbastani
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -101,17 +103,15 @@ public class AccountService {
|
|||||||
|
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
eventResource = new Resource<>(event,
|
eventResource = new Resource<>(event,
|
||||||
entityLinks.linkFor(AccountEvent.class, event.getId())
|
linkTo(AccountController.class)
|
||||||
.slash(event.getId())
|
.slash("accounts")
|
||||||
|
.slash(accountId)
|
||||||
|
.slash("events")
|
||||||
.withSelfRel(),
|
.withSelfRel(),
|
||||||
linkTo(AccountController.class)
|
linkTo(AccountController.class)
|
||||||
.slash("accounts")
|
.slash("accounts")
|
||||||
.slash(accountId)
|
.slash(accountId)
|
||||||
.withRel("account"),
|
.withRel("account")
|
||||||
entityLinks.linkFor(AccountEvent.class, event.getId())
|
|
||||||
.slash(event.getId())
|
|
||||||
.slash("logs")
|
|
||||||
.withRel("logs")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,55 +128,61 @@ public class AccountService {
|
|||||||
public Resource<Account> applyCommand(Long id, AccountCommand accountCommand) {
|
public Resource<Account> applyCommand(Long id, AccountCommand accountCommand) {
|
||||||
Resource<Account> account = getAccountResource(id);
|
Resource<Account> 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) {
|
switch (accountCommand) {
|
||||||
case CONFIRM_ACCOUNT:
|
case CONFIRM_ACCOUNT:
|
||||||
Assert.isTrue(status == ACCOUNT_CREATED, "The account has already been confirmed");
|
Assert.isTrue(status == ACCOUNT_CREATED, "The account has already been confirmed");
|
||||||
|
|
||||||
// Confirm the account
|
// Confirm the account
|
||||||
Account updateAccount = account.getContent();
|
Account updateAccount = account.getContent();
|
||||||
updateAccount.setStatus(ACCOUNT_CONFIRMED);
|
updateAccount.setStatus(ACCOUNT_CONFIRMED);
|
||||||
account = updateAccountResource(id, updateAccount);
|
account = updateAccountResource(id, updateAccount);
|
||||||
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED));
|
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED));
|
||||||
break;
|
break;
|
||||||
case ACTIVATE_ACCOUNT:
|
case ACTIVATE_ACCOUNT:
|
||||||
Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active");
|
Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active");
|
||||||
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
|
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
|
||||||
.contains(status), "The account cannot be activated");
|
.contains(status), "The account cannot be activated");
|
||||||
|
|
||||||
// Activate the account
|
// Activate the account
|
||||||
account.getContent().setStatus(ACCOUNT_ACTIVE);
|
account.getContent().setStatus(ACCOUNT_ACTIVE);
|
||||||
account = updateAccountResource(id, account.getContent());
|
account = updateAccountResource(id, account.getContent());
|
||||||
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED));
|
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED));
|
||||||
break;
|
break;
|
||||||
case SUSPEND_ACCOUNT:
|
case SUSPEND_ACCOUNT:
|
||||||
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
|
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
|
||||||
|
|
||||||
// Suspend the account
|
// Suspend the account
|
||||||
account.getContent().setStatus(ACCOUNT_SUSPENDED);
|
account.getContent().setStatus(ACCOUNT_SUSPENDED);
|
||||||
account = updateAccountResource(id, account.getContent());
|
account = updateAccountResource(id, account.getContent());
|
||||||
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED));
|
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED));
|
||||||
break;
|
break;
|
||||||
case ARCHIVE_ACCOUNT:
|
case ARCHIVE_ACCOUNT:
|
||||||
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
|
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
|
||||||
|
|
||||||
// Archive the account
|
// Archive the account
|
||||||
account.getContent().setStatus(ACCOUNT_ARCHIVED);
|
account.getContent().setStatus(ACCOUNT_ARCHIVED);
|
||||||
account = updateAccountResource(id, account.getContent());
|
account = updateAccountResource(id, account.getContent());
|
||||||
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED));
|
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Assert.notNull(accountCommand,
|
Assert.notNull(accountCommand,
|
||||||
"The provided command cannot be applied to this account in its current state");
|
"The provided command cannot be applied to this account in its current state");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
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) {
|
public AccountCommandsResource getCommandsResource(Long id) {
|
||||||
// Get the account resource for the identifier
|
// Get the account resource for the identifier
|
||||||
Resource<Account> accountResource = getAccountResource(id);
|
Resource<Account> accountResource = getAccountResource(id);
|
||||||
@@ -205,6 +211,22 @@ public class AccountService {
|
|||||||
return commandResource;
|
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) {
|
private LinkBuilder getCommandLinkBuilder(Long id) {
|
||||||
return linkTo(AccountController.class)
|
return linkTo(AccountController.class)
|
||||||
.slash("accounts")
|
.slash("accounts")
|
||||||
@@ -212,6 +234,12 @@ public class AccountService {
|
|||||||
.slash("commands");
|
.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<Account> getAccountResource(Account account) {
|
private Resource<Account> getAccountResource(Account account) {
|
||||||
Resource<Account> accountResource;
|
Resource<Account> accountResource;
|
||||||
|
|
||||||
@@ -219,23 +247,36 @@ public class AccountService {
|
|||||||
accountResource = new Resource<>(account,
|
accountResource = new Resource<>(account,
|
||||||
linkTo(AccountController.class)
|
linkTo(AccountController.class)
|
||||||
.slash("accounts")
|
.slash("accounts")
|
||||||
.slash(account.getId())
|
.slash(account.getAccountId())
|
||||||
.withSelfRel(),
|
.withSelfRel(),
|
||||||
entityLinks.linkFor(Account.class, account.getId())
|
linkTo(AccountController.class)
|
||||||
.slash(account.getId())
|
.slash("accounts")
|
||||||
|
.slash(account.getAccountId())
|
||||||
.slash("events")
|
.slash("events")
|
||||||
.withRel("events"),
|
.withRel("events"),
|
||||||
getCommandLinkBuilder(account.getId())
|
getCommandLinkBuilder(account.getAccountId())
|
||||||
.withRel("commands")
|
.withRel("commands")
|
||||||
);
|
);
|
||||||
|
|
||||||
return accountResource;
|
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) {
|
private Account getAccount(Long id) {
|
||||||
return accountRepository.findOne(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) {
|
private Account createAccount(Account account) {
|
||||||
// Assert for uniqueness constraint
|
// Assert for uniqueness constraint
|
||||||
Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()),
|
Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()),
|
||||||
@@ -247,32 +288,40 @@ public class AccountService {
|
|||||||
account = accountRepository.save(account);
|
account = accountRepository.save(account);
|
||||||
|
|
||||||
// Trigger the account creation event
|
// Trigger the account creation event
|
||||||
appendEventResource(account.getId(),
|
appendEventResource(account.getAccountId(),
|
||||||
new AccountEvent(AccountEventType.ACCOUNT_CREATED));
|
new AccountEvent(AccountEventType.ACCOUNT_CREATED));
|
||||||
|
|
||||||
return account;
|
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) {
|
private Account updateAccount(Long id, Account account) {
|
||||||
Assert.notNull(id);
|
Assert.notNull(id);
|
||||||
Assert.notNull(account);
|
Assert.notNull(account);
|
||||||
Assert.isTrue(Objects.equals(id, account.getId()));
|
Assert.isTrue(Objects.equals(id, account.getAccountId()));
|
||||||
return accountRepository.save(account);
|
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) {
|
private AccountEvent appendEvent(Long accountId, AccountEvent event) {
|
||||||
Account account = accountRepository.findOne(accountId);
|
Account account = getAccount(accountId);
|
||||||
if (account != null) {
|
Assert.notNull(account, "The account with the supplied id does not exist");
|
||||||
event.setAccount(account);
|
event.setAccount(account);
|
||||||
event = eventService.createEvent(event).getContent();
|
event = eventService.createEvent(event).getContent();
|
||||||
if (event != null) {
|
account.getEvents().add(event);
|
||||||
account.getEvents().add(event);
|
accountRepository.save(account);
|
||||||
accountRepository.save(account);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package demo.domain;
|
|||||||
import org.springframework.data.annotation.CreatedDate;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.springframework.data.annotation.LastModifiedDate;
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
import org.springframework.hateoas.ResourceSupport;
|
||||||
|
|
||||||
import javax.persistence.EntityListeners;
|
import javax.persistence.EntityListeners;
|
||||||
import javax.persistence.MappedSuperclass;
|
import javax.persistence.MappedSuperclass;
|
||||||
|
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class BaseEntity {
|
public class BaseEntity extends ResourceSupport {
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
private Long createdAt;
|
private Long createdAt;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import java.util.Set;
|
|||||||
* This event resource also provides a transaction log that can be used to append
|
* 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
|
* actions to the event. The collection of {@link Log} items can be used to remediate
|
||||||
* partial failures.
|
* partial failures.
|
||||||
|
*
|
||||||
|
* @author kbastani
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@RestResource(path = "events", rel = "events")
|
@RestResource(path = "events", rel = "events")
|
||||||
@@ -46,11 +48,12 @@ public class AccountEvent extends BaseEntity {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
@JsonIgnore
|
||||||
|
public Long getEventId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(Long id) {
|
public void setEventId(Long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package demo.event;
|
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.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||||
|
|
||||||
@RepositoryRestResource(path = "events", collectionResourceRel = "events", itemResourceRel = "event")
|
@RepositoryRestResource(path = "events", collectionResourceRel = "events", itemResourceRel = "event")
|
||||||
public interface EventRepository extends JpaRepository<AccountEvent, Long> {
|
public interface EventRepository extends JpaRepository<AccountEvent, Long> {
|
||||||
|
Page<AccountEvent> findAccountEventsByAccountId(@Param("accountId") Long accountId, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package demo.event;
|
|||||||
import demo.account.Account;
|
import demo.account.Account;
|
||||||
import demo.account.AccountController;
|
import demo.account.AccountController;
|
||||||
import demo.account.AccountEventType;
|
import demo.account.AccountEventType;
|
||||||
|
import demo.account.AccountEvents;
|
||||||
import demo.log.Log;
|
import demo.log.Log;
|
||||||
import demo.log.LogRepository;
|
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.data.rest.webmvc.support.RepositoryEntityLinks;
|
||||||
import org.springframework.hateoas.Resource;
|
import org.springframework.hateoas.Resource;
|
||||||
import org.springframework.integration.support.MessageBuilder;
|
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},
|
* 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
|
* 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}
|
* log that can be used to source the state of the {@link Account}
|
||||||
|
*
|
||||||
|
* @author kbastani
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -30,57 +36,58 @@ public class EventService {
|
|||||||
private final EventRepository eventRepository;
|
private final EventRepository eventRepository;
|
||||||
private final LogRepository logRepository;
|
private final LogRepository logRepository;
|
||||||
private final RepositoryEntityLinks entityLinks;
|
private final RepositoryEntityLinks entityLinks;
|
||||||
private final ProducerChannels producer;
|
private final Source accountStreamSource;
|
||||||
|
|
||||||
public EventService(EventRepository eventRepository, LogRepository logRepository,
|
public EventService(EventRepository eventRepository, LogRepository logRepository,
|
||||||
RepositoryEntityLinks entityLinks, ProducerChannels producerChannels) {
|
RepositoryEntityLinks entityLinks, Source accountStreamSource) {
|
||||||
this.eventRepository = eventRepository;
|
this.eventRepository = eventRepository;
|
||||||
this.logRepository = logRepository;
|
this.logRepository = logRepository;
|
||||||
this.entityLinks = entityLinks;
|
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<AccountEvent> createEvent(AccountEvent event) {
|
public Resource<AccountEvent> createEvent(AccountEvent event) {
|
||||||
Resource<AccountEvent> eventResource = null;
|
Resource<AccountEvent> eventResource = null;
|
||||||
|
|
||||||
// Save new event
|
// Save new event
|
||||||
event = addEvent(event);
|
event = addEvent(event);
|
||||||
|
Assert.notNull(event, "The event could not be appended to the account");
|
||||||
|
|
||||||
if (event != null) {
|
// Create account event resource
|
||||||
// Create account event resource
|
eventResource = getAccountEventResource(event);
|
||||||
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"))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Produce account event
|
// Append the account event to the stream
|
||||||
producer.output()
|
accountStreamSource.output()
|
||||||
.send(MessageBuilder
|
.send(MessageBuilder
|
||||||
.withPayload(eventResource)
|
.withPayload(eventResource)
|
||||||
.build());
|
.build());
|
||||||
}
|
|
||||||
|
|
||||||
return eventResource;
|
return eventResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountEvent addEvent(AccountEvent event) {
|
/**
|
||||||
event = eventRepository.save(event);
|
* Get an {@link AccountEvent} with the supplied identifier.
|
||||||
return event;
|
*
|
||||||
}
|
* @param id is the unique identifier for the {@link AccountEvent}
|
||||||
|
* @return an {@link AccountEvent}
|
||||||
|
*/
|
||||||
public AccountEvent getEvent(Long id) {
|
public AccountEvent getEvent(Long id) {
|
||||||
return eventRepository.findOne(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) {
|
public AccountEvent updateEvent(Long id, AccountEvent event) {
|
||||||
Assert.notNull(id);
|
Assert.notNull(id);
|
||||||
Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId()));
|
Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId()));
|
||||||
@@ -88,6 +95,13 @@ public class EventService {
|
|||||||
return eventRepository.save(event);
|
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<Log> appendEventLog(Long eventId, Log log) {
|
public Resource<Log> appendEventLog(Long eventId, Log log) {
|
||||||
Assert.notNull(eventId);
|
Assert.notNull(eventId);
|
||||||
Assert.notNull(log);
|
Assert.notNull(log);
|
||||||
@@ -95,20 +109,75 @@ public class EventService {
|
|||||||
Resource<Log> logResource = null;
|
Resource<Log> logResource = null;
|
||||||
AccountEvent event = getEvent(eventId);
|
AccountEvent event = getEvent(eventId);
|
||||||
|
|
||||||
if (event != null) {
|
Assert.notNull(event, "The event with the supplied id could not be found");
|
||||||
log = logRepository.save(log);
|
|
||||||
event.getLogs().add(log);
|
|
||||||
|
|
||||||
logResource = new Resource<>(log, Arrays.asList(
|
log = logRepository.save(log);
|
||||||
entityLinks.linkFor(Log.class)
|
event.getLogs().add(log);
|
||||||
.slash(log.getId())
|
logResource = getLogResource(log, event);
|
||||||
.withSelfRel(),
|
|
||||||
entityLinks.linkFor(AccountEvent.class)
|
|
||||||
.slash(event.getId())
|
|
||||||
.withRel("event")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return logResource;
|
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<Log> 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<AccountEvent> 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<AccountEvent> events = eventRepository.findAccountEventsByAccountId(id, new PageRequest(0, Integer.MAX_VALUE));
|
||||||
|
return new AccountEvents(id, events);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package demo.log;
|
package demo.log;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import demo.account.AccountEventType;
|
import demo.account.AccountEventType;
|
||||||
import demo.domain.BaseEntity;
|
import demo.domain.BaseEntity;
|
||||||
|
|
||||||
@@ -25,11 +26,12 @@ public class Log extends BaseEntity {
|
|||||||
this.action = action;
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
@JsonIgnore
|
||||||
|
public Long getLogId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(Long id) {
|
public void setLogId(Long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,6 @@
|
|||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
|
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-integration</artifactId>
|
<artifactId>spring-boot-starter-integration</artifactId>
|
||||||
@@ -49,10 +45,6 @@
|
|||||||
<version>1.1.1.RELEASE</version>
|
<version>1.1.1.RELEASE</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.h2database</groupId>
|
|
||||||
<artifactId>h2</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.jayway.jsonpath</groupId>
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
<artifactId>json-path</artifactId>
|
<artifactId>json-path</artifactId>
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
package demo.domain;
|
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 org.springframework.hateoas.ResourceSupport;
|
||||||
|
|
||||||
import javax.persistence.EntityListeners;
|
|
||||||
import javax.persistence.MappedSuperclass;
|
|
||||||
|
|
||||||
@MappedSuperclass
|
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
|
||||||
public class BaseEntity extends ResourceSupport {
|
public class BaseEntity extends ResourceSupport {
|
||||||
|
|
||||||
@CreatedDate
|
|
||||||
private Long createdAt;
|
private Long createdAt;
|
||||||
|
|
||||||
@LastModifiedDate
|
|
||||||
private Long lastModified;
|
private Long lastModified;
|
||||||
|
|
||||||
public BaseEntity() {
|
public BaseEntity() {
|
||||||
@@ -42,6 +31,6 @@ public class BaseEntity extends ResourceSupport {
|
|||||||
return "BaseEntity{" +
|
return "BaseEntity{" +
|
||||||
"createdAt=" + createdAt +
|
"createdAt=" + createdAt +
|
||||||
", lastModified=" + lastModified +
|
", lastModified=" + lastModified +
|
||||||
'}';
|
"} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.apache.log4j.Logger;
|
|||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||||
import org.springframework.cloud.stream.annotation.StreamListener;
|
import org.springframework.cloud.stream.annotation.StreamListener;
|
||||||
|
import org.springframework.cloud.stream.messaging.Sink;
|
||||||
import org.springframework.hateoas.MediaTypes;
|
import org.springframework.hateoas.MediaTypes;
|
||||||
import org.springframework.hateoas.client.Traverson;
|
import org.springframework.hateoas.client.Traverson;
|
||||||
import org.springframework.messaging.MessageHeaders;
|
import org.springframework.messaging.MessageHeaders;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user