diff --git a/account-parent/account-web/pom.xml b/account-parent/account-web/pom.xml index a985dec..e794708 100644 --- a/account-parent/account-web/pom.xml +++ b/account-parent/account-web/pom.xml @@ -31,6 +31,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-redis + org.springframework.boot spring-boot-starter-actuator 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 360ac1c..e0a2ec3 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 @@ -31,14 +31,14 @@ public class AccountController { public ResponseEntity createAccount(@RequestBody Account account) { return Optional.ofNullable(createAccountResource(account)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) - .orElseThrow(() -> new IllegalArgumentException("Account creation failed")); + .orElseThrow(() -> new RuntimeException("Account creation failed")); } @PutMapping(path = "/accounts/{id}") public ResponseEntity updateAccount(@RequestBody Account account, @PathVariable Long id) { return Optional.ofNullable(updateAccountResource(id, account)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("Account update failed")); + .orElseThrow(() -> new RuntimeException("Account update failed")); } @GetMapping(path = "/accounts/{id}") @@ -48,25 +48,32 @@ public class AccountController { .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } + @DeleteMapping(path = "/accounts/{id}") + public ResponseEntity deleteAccount(@PathVariable Long id) { + return Optional.ofNullable(accountService.deleteAccount(id)) + .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) + .orElseThrow(() -> new RuntimeException("Account deletion failed")); + } + @GetMapping(path = "/accounts/{id}/events") public ResponseEntity getAccountEvents(@PathVariable Long id) { return Optional.ofNullable(getAccountEventResources(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("Could not get account events")); + .orElseThrow(() -> new RuntimeException("Could not get account events")); } @PostMapping(path = "/accounts/{id}/events") public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) { return Optional.ofNullable(appendEventResource(id, event)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) - .orElseThrow(() -> new IllegalArgumentException("Append account event failed")); + .orElseThrow(() -> new RuntimeException("Append account event failed")); } @GetMapping(path = "/accounts/{id}/commands") public ResponseEntity getAccountCommands(@PathVariable Long id) { return Optional.ofNullable(getCommandsResource(id)) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("The account could not be found")); + .orElseThrow(() -> new RuntimeException("The account could not be found")); } @GetMapping(path = "/accounts/{id}/commands/confirm") @@ -74,7 +81,7 @@ public class AccountController { return Optional.ofNullable(getAccountResource( accountService.applyCommand(id, AccountCommand.CONFIRM_ACCOUNT))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("The command could not be applied")); + .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @GetMapping(path = "/accounts/{id}/commands/activate") @@ -82,7 +89,7 @@ public class AccountController { return Optional.ofNullable(getAccountResource( accountService.applyCommand(id, AccountCommand.ACTIVATE_ACCOUNT))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("The command could not be applied")); + .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @GetMapping(path = "/accounts/{id}/commands/suspend") @@ -90,7 +97,7 @@ public class AccountController { return Optional.ofNullable(getAccountResource( accountService.applyCommand(id, AccountCommand.SUSPEND_ACCOUNT))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("The command could not be applied")); + .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @GetMapping(path = "/accounts/{id}/commands/archive") @@ -98,7 +105,7 @@ public class AccountController { return Optional.ofNullable(getAccountResource( accountService.applyCommand(id, AccountCommand.ARCHIVE_ACCOUNT))) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) - .orElseThrow(() -> new IllegalArgumentException("The command could not be applied")); + .orElseThrow(() -> new RuntimeException("The command could not be applied")); } /** 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 e6d83f0..a604fec 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 @@ -3,6 +3,9 @@ package demo.account; import demo.event.AccountEvent; import demo.event.AccountEventType; import demo.event.EventService; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -11,6 +14,8 @@ import java.util.Arrays; import java.util.Objects; import static demo.account.AccountStatus.*; +import static demo.account.AccountStatus.ACCOUNT_ACTIVE; +import static demo.account.AccountStatus.ACCOUNT_ARCHIVED; /** * The {@link AccountService} provides transactional support for managing {@link Account} @@ -33,81 +38,13 @@ public class AccountService { this.eventService = eventService; } - /** - * Apply an {@link AccountCommand} to the {@link Account} with a specified identifier. - * - * @param id is the unique identifier of the {@link Account} - * @param accountCommand is the command to apply to the {@link Account} - * @return a hypermedia resource containing the updated {@link Account} - */ - public Account applyCommand(Long id, AccountCommand accountCommand) { - Account account = getAccount(id); - - Assert.notNull(account, "The account for the supplied id could not be found"); - - AccountStatus status = account.getStatus(); - - switch (accountCommand) { - case CONFIRM_ACCOUNT: - Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed"); - - // Confirm the account - Account updateAccount = account; - updateAccount.setStatus(ACCOUNT_CONFIRMED); - account = updateAccount(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.setStatus(ACCOUNT_ACTIVE); - account = updateAccount(id, account); - 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.setStatus(ACCOUNT_SUSPENDED); - account = updateAccount(id, account); - appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)); - break; - case ARCHIVE_ACCOUNT: - Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); - - // Archive the account - account.setStatus(ACCOUNT_ARCHIVED); - account = updateAccount(id, account); - 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 an {@link Account} entity for the supplied identifier. - * - * @param id is the unique identifier of a {@link Account} entity - * @return an {@link Account} entity - */ - public 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} */ + @CacheEvict(value = "account", key = "#account.getAccountId().toString()") public Account createAccount(Account account) { // Assert for uniqueness constraint Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()), @@ -125,6 +62,17 @@ public class AccountService { return account; } + /** + * 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 + */ + @Cacheable(value = "account", key = "#id.toString()") + public Account getAccount(Long id) { + return accountRepository.findOne(id); + } + /** * Update an {@link Account} entity with the supplied identifier. * @@ -132,6 +80,7 @@ public class AccountService { * @param account is the {@link Account} containing updated fields * @return the updated {@link Account} entity */ + @CachePut(value = "account", key = "#id.toString()") public Account updateAccount(Long id, Account account) { Assert.notNull(id, "Account id must be present in the resource URL"); Assert.notNull(account, "Account request body cannot be null"); @@ -143,15 +92,31 @@ public class AccountService { account.setAccountId(id); } - Account currentAccount = getAccount(id); - currentAccount.setStatus(account.getStatus()); - currentAccount.setDefaultAccount(account.getDefaultAccount()); - currentAccount.setAccountNumber(account.getAccountNumber()); + Assert.state(accountRepository.exists(id), + "The account with the supplied id does not exist"); + + Account currentAccount = accountRepository.findOne(id); currentAccount.setUserId(account.getUserId()); + currentAccount.setAccountNumber(account.getAccountNumber()); + currentAccount.setDefaultAccount(account.getDefaultAccount()); + currentAccount.setStatus(account.getStatus()); return accountRepository.save(currentAccount); } + /** + * Delete the {@link Account} with the supplied identifier. + * + * @param id is the unique identifier for the {@link Account} + */ + @CacheEvict(value = "account", key = "#id.toString()") + public Boolean deleteAccount(Long id) { + Assert.state(accountRepository.exists(id), + "The account with the supplied id does not exist"); + this.accountRepository.delete(id); + return true; + } + /** * Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier. * @@ -168,4 +133,63 @@ public class AccountService { accountRepository.save(account); return event; } + + /** + * Apply an {@link AccountCommand} to the {@link Account} with a specified identifier. + * + * @param id is the unique identifier of the {@link Account} + * @param accountCommand is the command to apply to the {@link Account} + * @return a hypermedia resource containing the updated {@link Account} + */ + @CachePut(value = "account", key = "#id.toString()") + public Account applyCommand(Long id, AccountCommand accountCommand) { + Account account = getAccount(id); + + Assert.notNull(account, "The account for the supplied id could not be found"); + + AccountStatus status = account.getStatus(); + + switch (accountCommand) { + case CONFIRM_ACCOUNT: + Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed"); + + // Confirm the account + Account updateAccount = account; + updateAccount.setStatus(ACCOUNT_CONFIRMED); + account = this.updateAccount(id, updateAccount); + this.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.setStatus(ACCOUNT_ACTIVE); + account = this.updateAccount(id, account); + this.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.setStatus(ACCOUNT_SUSPENDED); + account = this.updateAccount(id, account); + this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)); + break; + case ARCHIVE_ACCOUNT: + Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived"); + + // Archive the account + account.setStatus(ACCOUNT_ARCHIVED); + account = this.updateAccount(id, account); + this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED)); + break; + default: + Assert.notNull(accountCommand, + "The provided command cannot be applied to this account in its current state"); + } + + return account; + } } diff --git a/account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java b/account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java new file mode 100644 index 0000000..1ec7986 --- /dev/null +++ b/account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java @@ -0,0 +1,42 @@ +package demo.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +@EnableCaching +public class CacheConfiguration { + + @Bean + public JedisConnectionFactory redisConnectionFactory( + @Value("${spring.redis.port}") Integer redisPort, + @Value("${spring.redis.host}") String redisHost) { + JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(); + + redisConnectionFactory.setHostName(redisHost); + redisConnectionFactory.setPort(redisPort); + + return redisConnectionFactory; + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory cf) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(cf); + return redisTemplate; + } + + @Bean + public CacheManager cacheManager(RedisTemplate redisTemplate) { + RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); + cacheManager.setDefaultExpiration(3000); + return cacheManager; + } +} \ No newline at end of file diff --git a/account-parent/account-web/src/main/java/demo/config/JpaConfig.java b/account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java similarity index 89% rename from account-parent/account-web/src/main/java/demo/config/JpaConfig.java rename to account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java index c6c09e0..7c300cb 100644 --- a/account-parent/account-web/src/main/java/demo/config/JpaConfig.java +++ b/account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java @@ -9,5 +9,5 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; */ @Configuration @EnableJpaAuditing -public class JpaConfig { +public class JpaConfiguration { } diff --git a/account-parent/account-web/src/main/java/demo/config/AccountResourceConfig.java b/account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java similarity index 96% rename from account-parent/account-web/src/main/java/demo/config/AccountResourceConfig.java rename to account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java index ef9587b..41b3f25 100644 --- a/account-parent/account-web/src/main/java/demo/config/AccountResourceConfig.java +++ b/account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java @@ -10,7 +10,7 @@ import org.springframework.hateoas.ResourceProcessor; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @Configuration -public class AccountResourceConfig { +public class ResourceConfiguration { /** * Enriches the {@link Account} resource with hypermedia links. diff --git a/account-parent/account-web/src/main/java/demo/config/AccountStreamConfig.java b/account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java similarity index 87% rename from account-parent/account-web/src/main/java/demo/config/AccountStreamConfig.java rename to account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java index 45ceb93..cec5ec1 100644 --- a/account-parent/account-web/src/main/java/demo/config/AccountStreamConfig.java +++ b/account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java @@ -6,5 +6,5 @@ import org.springframework.context.annotation.Configuration; @Configuration @EnableBinding(Source.class) -public class AccountStreamConfig { +public class StreamConfiguration { } 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 db285dc..fb472ef 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 @@ -7,10 +7,11 @@ import org.springframework.hateoas.ResourceSupport; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; +import java.io.Serializable; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class BaseEntity extends ResourceSupport { +public class BaseEntity extends ResourceSupport implements Serializable { @CreatedDate private Long createdAt; diff --git a/account-parent/account-web/src/main/resources/application.yml b/account-parent/account-web/src/main/resources/application.yml index 722c9b5..e940116 100644 --- a/account-parent/account-web/src/main/resources/application.yml +++ b/account-parent/account-web/src/main/resources/application.yml @@ -10,5 +10,8 @@ spring: output: destination: account contentType: 'application/json' + redis: + host: localhost + port: 6379 server: port: 0 \ No newline at end of file diff --git a/account-parent/account-web/src/test/java/demo/account/AccountServiceTests.java b/account-parent/account-web/src/test/java/demo/account/AccountServiceTests.java index 42a3d96..dbdd816 100644 --- a/account-parent/account-web/src/test/java/demo/account/AccountServiceTests.java +++ b/account-parent/account-web/src/test/java/demo/account/AccountServiceTests.java @@ -70,6 +70,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED))) .willReturn(accountEvent); @@ -91,6 +92,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent); @@ -112,6 +114,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED))) .willReturn(accountEvent); @@ -133,6 +136,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent); @@ -154,6 +158,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED))) .willReturn(accountEvent); @@ -175,6 +180,7 @@ public class AccountServiceTests { accountEvent.setEventId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); + given(this.accountRepository.exists(1L)).willReturn(true); given(this.accountRepository.save(account)).willReturn(account); given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent);