Javadoc comments and hypermedia tweaks

This commit is contained in:
Kenny Bastani
2016-12-07 02:46:43 -08:00
parent 8d6597c712
commit d0e8b2eba8
21 changed files with 369 additions and 157 deletions

View File

@@ -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;
}
};
}
}

View File

@@ -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 {
} }

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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 {
} }

View File

@@ -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))

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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);
} }

View File

@@ -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;
} }
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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);
}
} }

View File

@@ -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();
}

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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();
} }
} }

View File

@@ -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;

View File

@@ -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();
}