From 9f4535b986c38f29ccc1911f6cb29b55b251d000 Mon Sep 17 00:00:00 2001 From: Kenny Bastani Date: Tue, 20 Dec 2016 11:47:44 -0800 Subject: [PATCH] AWS Lambda Starter & Consistency Models --- account-parent/account-web/manifest.yml | 2 +- .../java/demo/AccountServiceApplication.java | 2 + .../src/main/java/demo/account/Account.java | 54 ++++---- .../java/demo/account/AccountController.java | 8 +- .../java/demo/account/AccountRepository.java | 3 +- .../java/demo/account/AccountService.java | 74 +++++++---- ...cheConfiguration.java => CacheConfig.java} | 2 +- .../{JpaConfiguration.java => JpaConfig.java} | 2 +- .../demo/config/ResourceConfiguration.java | 36 ------ ...amConfiguration.java => StreamConfig.java} | 2 +- .../main/java/demo/config/WebMvcConfig.java | 36 ++++++ .../main/java/demo/event/AccountEvent.java | 2 +- .../java/demo/event/ConsistencyModel.java | 6 + .../main/java/demo/event/EventController.java | 6 +- .../main/java/demo/event/EventService.java | 117 +++++++++++++++--- .../src/main/resources/application.yml | 2 +- .../demo/account/AccountControllerTest.java | 4 +- .../demo/account/AccountServiceTests.java | 61 +++++---- .../src/test/resources/data-h2.sql | 2 +- account-parent/account-worker/manifest.yml | 9 +- account-parent/account-worker/pom.xml | 10 ++ .../demo/AccountStreamModuleApplication.java | 3 + .../src/main/java/demo/account/Account.java | 39 +++--- .../java/demo/config/AwsLambdaConfig.java | 23 ++++ .../java/demo/config/StateMachineConfig.java | 87 ++++++++++--- .../java/demo/event/AccountEventStream.java | 60 +-------- .../main/java/demo/event/EventController.java | 28 +++++ .../main/java/demo/event/EventService.java | 82 ++++++++++++ .../java/demo/function/AccountFunction.java | 15 ++- .../java/demo/function/AccountService.java | 4 - ...ountFunction.java => ActivateAccount.java} | 13 +- ...countFunction.java => ArchiveAccount.java} | 13 +- ...countFunction.java => ConfirmAccount.java} | 13 +- ...ccountFunction.java => CreateAccount.java} | 23 ++-- .../java/demo/function/LambdaFunctions.java | 31 +++++ ...countFunction.java => SuspendAccount.java} | 13 +- ...untFunction.java => UnarchiveAccount.java} | 13 +- ...untFunction.java => UnsuspendAccount.java} | 13 +- .../src/main/java/demo/util/LambdaUtil.java | 29 +++++ .../src/main/resources/application.yml | 6 +- pom.xml | 1 + spring-boot-starter-aws-lambda/pom.xml | 52 ++++++++ .../aws/AWSLambdaConfigurerAdapter.java | 88 +++++++++++++ .../amazon/aws/AmazonAutoConfiguration.java | 28 +++++ .../java/amazon/aws/AmazonProperties.java | 89 +++++++++++++ .../spring-configuration-metadata.json | 9 ++ .../main/resources/META-INF/spring.factories | 1 + .../src/main/resources/application.properties | 2 + .../amazon/aws/AmazonConfigurationTest.java | 44 +++++++ 49 files changed, 949 insertions(+), 313 deletions(-) rename account-parent/account-web/src/main/java/demo/config/{CacheConfiguration.java => CacheConfig.java} (97%) rename account-parent/account-web/src/main/java/demo/config/{JpaConfiguration.java => JpaConfig.java} (89%) delete mode 100644 account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java rename account-parent/account-web/src/main/java/demo/config/{StreamConfiguration.java => StreamConfig.java} (87%) create mode 100644 account-parent/account-web/src/main/java/demo/config/WebMvcConfig.java create mode 100644 account-parent/account-web/src/main/java/demo/event/ConsistencyModel.java create mode 100644 account-parent/account-worker/src/main/java/demo/config/AwsLambdaConfig.java create mode 100644 account-parent/account-worker/src/main/java/demo/event/EventController.java create mode 100644 account-parent/account-worker/src/main/java/demo/event/EventService.java delete mode 100644 account-parent/account-worker/src/main/java/demo/function/AccountService.java rename account-parent/account-worker/src/main/java/demo/function/{ActivateAccountFunction.java => ActivateAccount.java} (69%) rename account-parent/account-worker/src/main/java/demo/function/{ArchiveAccountFunction.java => ArchiveAccount.java} (69%) rename account-parent/account-worker/src/main/java/demo/function/{ConfirmAccountFunction.java => ConfirmAccount.java} (69%) rename account-parent/account-worker/src/main/java/demo/function/{CreateAccountFunction.java => CreateAccount.java} (83%) create mode 100644 account-parent/account-worker/src/main/java/demo/function/LambdaFunctions.java rename account-parent/account-worker/src/main/java/demo/function/{SuspendAccountFunction.java => SuspendAccount.java} (69%) rename account-parent/account-worker/src/main/java/demo/function/{UnarchiveAccountFunction.java => UnarchiveAccount.java} (74%) rename account-parent/account-worker/src/main/java/demo/function/{UnsuspendAccountFunction.java => UnsuspendAccount.java} (74%) create mode 100644 account-parent/account-worker/src/main/java/demo/util/LambdaUtil.java create mode 100755 spring-boot-starter-aws-lambda/pom.xml create mode 100755 spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AWSLambdaConfigurerAdapter.java create mode 100755 spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonAutoConfiguration.java create mode 100755 spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonProperties.java create mode 100755 spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100755 spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring.factories create mode 100755 spring-boot-starter-aws-lambda/src/main/resources/application.properties create mode 100755 spring-boot-starter-aws-lambda/src/test/java/amazon/aws/AmazonConfigurationTest.java diff --git a/account-parent/account-web/manifest.yml b/account-parent/account-web/manifest.yml index 1552562..798e047 100644 --- a/account-parent/account-web/manifest.yml +++ b/account-parent/account-web/manifest.yml @@ -1,5 +1,5 @@ name: account-web -memory: 512M +memory: 1024M instances: 1 path: ./target/account-web-0.0.1-SNAPSHOT.jar buildpack: java_buildpack diff --git a/account-parent/account-web/src/main/java/demo/AccountServiceApplication.java b/account-parent/account-web/src/main/java/demo/AccountServiceApplication.java index 6b3a5a4..9e17f64 100644 --- a/account-parent/account-web/src/main/java/demo/AccountServiceApplication.java +++ b/account-parent/account-web/src/main/java/demo/AccountServiceApplication.java @@ -2,8 +2,10 @@ package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.hateoas.config.EnableHypermediaSupport; @SpringBootApplication +@EnableHypermediaSupport(type = {EnableHypermediaSupport.HypermediaType.HAL}) public class AccountServiceApplication { public static void main(String[] args) { diff --git a/account-parent/account-web/src/main/java/demo/account/Account.java b/account-parent/account-web/src/main/java/demo/account/Account.java index 8ebdd6d..4987f6b 100644 --- a/account-parent/account-web/src/main/java/demo/account/Account.java +++ b/account-parent/account-web/src/main/java/demo/account/Account.java @@ -22,11 +22,12 @@ public class Account extends BaseEntity { @Id @GeneratedValue private Long id; - private Long userId; - private String accountNumber; - private Boolean defaultAccount; - @OneToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY) + private String firstName; + private String lastName; + private String email; + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Set events = new HashSet<>(); @Enumerated(value = EnumType.STRING) @@ -36,17 +37,11 @@ public class Account extends BaseEntity { status = AccountStatus.ACCOUNT_CREATED; } - public Account(Long userId, String accountNumber, Boolean defaultAccount) { + public Account(String firstName, String lastName, String email) { this(); - this.accountNumber = accountNumber; - this.defaultAccount = defaultAccount; - this.userId = userId; - } - - public Account(String accountNumber, Boolean defaultAccount, AccountStatus status) { - this.accountNumber = accountNumber; - this.defaultAccount = defaultAccount; - this.status = status; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; } @JsonIgnore @@ -58,28 +53,28 @@ public class Account extends BaseEntity { this.id = id; } - public Long getUserId() { - return userId; + public String getFirstName() { + return firstName; } - public void setUserId(Long userId) { - this.userId = userId; + public void setFirstName(String firstName) { + this.firstName = firstName; } - public String getAccountNumber() { - return accountNumber; + public String getLastName() { + return lastName; } - public void setAccountNumber(String accountNumber) { - this.accountNumber = accountNumber; + public void setLastName(String lastName) { + this.lastName = lastName; } - public Boolean getDefaultAccount() { - return defaultAccount; + public String getEmail() { + return email; } - public void setDefaultAccount(Boolean defaultAccount) { - this.defaultAccount = defaultAccount; + public void setEmail(String email) { + this.email = email; } @JsonIgnore @@ -103,9 +98,10 @@ public class Account extends BaseEntity { public String toString() { return "Account{" + "id=" + id + - ", userId=" + userId + - ", accountNumber='" + accountNumber + '\'' + - ", defaultAccount=" + defaultAccount + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", events=" + events + ", status=" + status + "} " + super.toString(); } 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 3e8bcb2..d48bcb0 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 @@ -136,12 +136,12 @@ public class AccountController { */ private Resource createAccountResource(Account account) { Assert.notNull(account, "Account body must not be null"); - Assert.notNull(account.getUserId(), "UserId is required"); - Assert.notNull(account.getAccountNumber(), "AccountNumber is required"); - Assert.notNull(account.getDefaultAccount(), "DefaultAccount is required"); + Assert.notNull(account.getEmail(), "Email is required"); + Assert.notNull(account.getFirstName(), "First name is required"); + Assert.notNull(account.getLastName(), "Last name is required"); // Create the new account - account = accountService.createAccount(account); + account = accountService.registerAccount(account); return getAccountResource(account); } diff --git a/account-parent/account-web/src/main/java/demo/account/AccountRepository.java b/account-parent/account-web/src/main/java/demo/account/AccountRepository.java index d19b565..2a04aa1 100644 --- a/account-parent/account-web/src/main/java/demo/account/AccountRepository.java +++ b/account-parent/account-web/src/main/java/demo/account/AccountRepository.java @@ -5,6 +5,5 @@ import org.springframework.data.repository.query.Param; public interface AccountRepository extends JpaRepository { - Account findAccountByUserId(@Param("userId") Long userId); - Account findAccountByAccountNumber(@Param("accountNumber") String accountNumber); + Account findAccountByEmail(@Param("email") String email); } 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 30a0064..8a2f049 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,20 +3,19 @@ package demo.account; import demo.event.AccountEvent; import demo.event.AccountEventType; import demo.event.EventService; +import demo.event.ConsistencyModel; +import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; 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} @@ -28,16 +27,36 @@ import static demo.account.AccountStatus.ACCOUNT_ARCHIVED; * @author kbastani */ @Service -@Transactional @CacheConfig(cacheNames = {"accounts"}) public class AccountService { private final AccountRepository accountRepository; private final EventService eventService; + private final CacheManager cacheManager; - public AccountService(AccountRepository accountRepository, EventService eventService) { + public AccountService(AccountRepository accountRepository, EventService eventService, CacheManager cacheManager) { this.accountRepository = accountRepository; this.eventService = eventService; + this.cacheManager = cacheManager; + } + + @CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()") + public Account registerAccount(Account account) { + + account = createAccount(account); + + cacheManager.getCache("accounts") + .evict(account.getAccountId()); + + // Trigger the account creation event + AccountEvent event = appendEvent(account.getAccountId(), + new AccountEvent(AccountEventType.ACCOUNT_CREATED)); + + // Attach account identifier + event.getAccount().setAccountId(account.getAccountId()); + + // Return the result + return event.getAccount(); } /** @@ -48,18 +67,13 @@ public class AccountService { */ @CacheEvict(cacheNames = "accounts", key = "#account.getAccountId().toString()") public Account createAccount(Account account) { + // Assert for uniqueness constraint - Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()), - "An account with the supplied userId already exists"); - Assert.isNull(accountRepository.findAccountByAccountNumber(account.getAccountNumber()), - "An account with the supplied account number already exists"); + Assert.isNull(accountRepository.findAccountByEmail(account.getEmail()), + "An account with the supplied email already exists"); // Save the account to the repository - account = accountRepository.save(account); - - // Trigger the account creation event - appendEvent(account.getAccountId(), - new AccountEvent(AccountEventType.ACCOUNT_CREATED)); + account = accountRepository.saveAndFlush(account); return account; } @@ -98,9 +112,9 @@ public class AccountService { "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.setEmail(account.getEmail()); + currentAccount.setFirstName(account.getFirstName()); + currentAccount.setLastName(account.getLastName()); currentAccount.setStatus(account.getStatus()); return accountRepository.save(currentAccount); @@ -127,12 +141,24 @@ public class AccountService { * @return the newly appended {@link AccountEvent} */ public AccountEvent appendEvent(Long accountId, AccountEvent event) { + return appendEvent(accountId, event, ConsistencyModel.ACID); + } + + /** + * Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier. + * + * @param accountId is the unique identifier for the {@link Account} + * @param event is the {@link AccountEvent} to append to the {@link Account} entity + * @return the newly appended {@link AccountEvent} + */ + public AccountEvent appendEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) { Account account = getAccount(accountId); Assert.notNull(account, "The account with the supplied id does not exist"); event.setAccount(account); - event = eventService.createEvent(event); + event = eventService.createEvent(accountId, event); account.getEvents().add(event); - accountRepository.save(account); + accountRepository.saveAndFlush(account); + eventService.raiseEvent(event, consistencyModel); return event; } @@ -158,8 +184,9 @@ public class AccountService { // Confirm the account Account updateAccount = account; updateAccount.setStatus(ACCOUNT_CONFIRMED); - account = this.updateAccount(id, updateAccount); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)); + this.updateAccount(id, updateAccount); + this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)) + .getAccount(); break; case ACTIVATE_ACCOUNT: Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active"); @@ -168,8 +195,9 @@ public class AccountService { // Activate the account account.setStatus(ACCOUNT_ACTIVE); - account = this.updateAccount(id, account); - this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)); + this.updateAccount(id, account); + this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)) + .getAccount(); break; case SUSPEND_ACCOUNT: Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended"); diff --git a/account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java b/account-parent/account-web/src/main/java/demo/config/CacheConfig.java similarity index 97% rename from account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java rename to account-parent/account-web/src/main/java/demo/config/CacheConfig.java index afac329..07b27c7 100644 --- a/account-parent/account-web/src/main/java/demo/config/CacheConfiguration.java +++ b/account-parent/account-web/src/main/java/demo/config/CacheConfig.java @@ -14,7 +14,7 @@ import java.util.Arrays; @Configuration @EnableCaching -public class CacheConfiguration { +public class CacheConfig { @Bean public JedisConnectionFactory redisConnectionFactory( diff --git a/account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java b/account-parent/account-web/src/main/java/demo/config/JpaConfig.java similarity index 89% rename from account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java rename to account-parent/account-web/src/main/java/demo/config/JpaConfig.java index 7c300cb..c6c09e0 100644 --- a/account-parent/account-web/src/main/java/demo/config/JpaConfiguration.java +++ b/account-parent/account-web/src/main/java/demo/config/JpaConfig.java @@ -9,5 +9,5 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; */ @Configuration @EnableJpaAuditing -public class JpaConfiguration { +public class JpaConfig { } diff --git a/account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java b/account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java deleted file mode 100644 index 41b3f25..0000000 --- a/account-parent/account-web/src/main/java/demo/config/ResourceConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -package demo.config; - -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 ResourceConfiguration { - - /** - * Enriches the {@link Account} resource with hypermedia links. - * - * @return a hypermedia processor for the {@link Account} resource - */ - @Bean - public ResourceProcessor> accountProcessor() { - return new ResourceProcessor>() { - @Override - public Resource process(Resource resource) { - resource.add( - linkTo(AccountController.class) - .slash("accounts") - .slash(resource.getContent().getAccountId()) - .slash("commands") - .withRel("commands")); - return resource; - } - }; - } - -} diff --git a/account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java b/account-parent/account-web/src/main/java/demo/config/StreamConfig.java similarity index 87% rename from account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java rename to account-parent/account-web/src/main/java/demo/config/StreamConfig.java index cec5ec1..2eaf902 100644 --- a/account-parent/account-web/src/main/java/demo/config/StreamConfiguration.java +++ b/account-parent/account-web/src/main/java/demo/config/StreamConfig.java @@ -6,5 +6,5 @@ import org.springframework.context.annotation.Configuration; @Configuration @EnableBinding(Source.class) -public class StreamConfiguration { +public class StreamConfig { } diff --git a/account-parent/account-web/src/main/java/demo/config/WebMvcConfig.java b/account-parent/account-web/src/main/java/demo/config/WebMvcConfig.java new file mode 100644 index 0000000..9282e23 --- /dev/null +++ b/account-parent/account-web/src/main/java/demo/config/WebMvcConfig.java @@ -0,0 +1,36 @@ +package demo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import java.util.Collections; +import java.util.List; + +@Configuration +public class WebMvcConfig extends WebMvcConfigurerAdapter { + + private ObjectMapper objectMapper; + + public WebMvcConfig(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void configureMessageConverters(List> converters) { + final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + converters.add(converter); + } + + @Bean + protected RestTemplate restTemplate(ObjectMapper objectMapper) { + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return new RestTemplate(Collections.singletonList(converter)); + } +} \ No newline at end of file diff --git a/account-parent/account-web/src/main/java/demo/event/AccountEvent.java b/account-parent/account-web/src/main/java/demo/event/AccountEvent.java index bdcb79a..97d53dd 100644 --- a/account-parent/account-web/src/main/java/demo/event/AccountEvent.java +++ b/account-parent/account-web/src/main/java/demo/event/AccountEvent.java @@ -26,7 +26,7 @@ public class AccountEvent extends BaseEntity { @Enumerated(EnumType.STRING) private AccountEventType type; - @OneToOne(cascade = CascadeType.MERGE, fetch = FetchType.LAZY) + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JsonIgnore private Account account; diff --git a/account-parent/account-web/src/main/java/demo/event/ConsistencyModel.java b/account-parent/account-web/src/main/java/demo/event/ConsistencyModel.java new file mode 100644 index 0000000..8bef081 --- /dev/null +++ b/account-parent/account-web/src/main/java/demo/event/ConsistencyModel.java @@ -0,0 +1,6 @@ +package demo.event; + +public enum ConsistencyModel { + BASE, + ACID +} diff --git a/account-parent/account-web/src/main/java/demo/event/EventController.java b/account-parent/account-web/src/main/java/demo/event/EventController.java index 926e304..125ed8d 100644 --- a/account-parent/account-web/src/main/java/demo/event/EventController.java +++ b/account-parent/account-web/src/main/java/demo/event/EventController.java @@ -16,9 +16,9 @@ public class EventController { this.eventService = eventService; } - @PostMapping(path = "/events") - public ResponseEntity createEvent(@RequestBody AccountEvent event) { - return Optional.ofNullable(eventService.createEvent(event)) + @PostMapping(path = "/events/{id}") + public ResponseEntity createEvent(@RequestBody AccountEvent event, @PathVariable Long id) { + return Optional.ofNullable(eventService.createEvent(id, event, ConsistencyModel.ACID)) .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) .orElseThrow(() -> new IllegalArgumentException("Event creation failed")); } diff --git a/account-parent/account-web/src/main/java/demo/event/EventService.java b/account-parent/account-web/src/main/java/demo/event/EventService.java index a9225bb..c4c06d9 100644 --- a/account-parent/account-web/src/main/java/demo/event/EventService.java +++ b/account-parent/account-web/src/main/java/demo/event/EventService.java @@ -2,17 +2,21 @@ package demo.event; import demo.account.Account; import demo.account.AccountController; +import org.apache.log4j.Logger; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.stream.messaging.Source; import org.springframework.data.domain.PageRequest; +import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.Resource; +import org.springframework.http.RequestEntity; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import org.springframework.web.client.RestTemplate; +import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -22,42 +26,122 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** * The {@link EventService} provides transactional service methods for {@link AccountEvent} * entities of the Account Service. Account domain events are generated with a {@link AccountEventType}, - * and action logs are appended to the {@link AccountEvent}. The logs resource provides an append-only transaction - * log that can be used to source the state of the {@link Account} + * and action logs are appended to the {@link AccountEvent}. * * @author kbastani */ @Service -@Transactional @CacheConfig(cacheNames = {"events"}) public class EventService { + private final Logger log = Logger.getLogger(EventService.class); + private final EventRepository eventRepository; private final Source accountStreamSource; + private final RestTemplate restTemplate; - public EventService(EventRepository eventRepository, Source accountStreamSource) { + public EventService(EventRepository eventRepository, Source accountStreamSource, RestTemplate restTemplate) { this.eventRepository = eventRepository; this.accountStreamSource = accountStreamSource; + this.restTemplate = restTemplate; } + /** + * Create a new {@link AccountEvent} and append it to the event log of the referenced {@link Account}. + * After the {@link AccountEvent} has been persisted, send the event to the account stream. Events can + * be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}. + * + * @param accountId is the unique identifier for the {@link Account} + * @param event is the {@link AccountEvent} to create + * @param consistencyModel is the desired consistency model for the response + * @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log + */ + public AccountEvent createEvent(Long accountId, AccountEvent event, ConsistencyModel consistencyModel) { + event = createEvent(accountId, event); + return raiseEvent(event, consistencyModel); + } + + /** + * Raise an {@link AccountEvent} that attempts to transition the state of an {@link Account}. + * + * @param event is an {@link AccountEvent} that will be raised + * @param consistencyModel is the consistency model for this request + * @return an {@link AccountEvent} that has been appended to the {@link Account}'s event log + */ + public AccountEvent raiseEvent(AccountEvent event, ConsistencyModel consistencyModel) { + switch (consistencyModel) { + case BASE: + asyncRaiseEvent(event); + break; + case ACID: + event = raiseEvent(event); + break; + } + + return event; + } + + /** + * Raise an asynchronous {@link AccountEvent} by sending an AMQP message to the account stream. Any + * state changes will be applied to the {@link Account} outside of the current HTTP request context. + *

+ * Use this operation when a workflow can be processed asynchronously outside of the current HTTP + * request context. + * + * @param event is an {@link AccountEvent} that will be raised + */ + private void asyncRaiseEvent(AccountEvent event) { + // Append the account event to the stream + accountStreamSource.output() + .send(MessageBuilder + .withPayload(getAccountEventResource(event)) + .build()); + } + + /** + * Raise a synchronous {@link AccountEvent} by sending a HTTP request to the account stream. The response + * is a blocking operation, which ensures that the result of a multi-step workflow will not return until + * the transaction reaches a consistent state. + *

+ * Use this operation when the result of a workflow must be returned within the current HTTP request context. + * + * @param event is an {@link AccountEvent} that will be raised + * @return an {@link AccountEvent} which contains the consistent state of an {@link Account} + */ + private AccountEvent raiseEvent(AccountEvent event) { + try { + // Create a new request entity + RequestEntity> requestEntity = RequestEntity.post( + URI.create("http://localhost:8081/v1/events")) + .contentType(MediaTypes.HAL_JSON) + .body(getAccountEventResource(event), Resource.class); + + // Update the account entity's status + Account result = restTemplate.exchange(requestEntity, Account.class) + .getBody(); + + log.info(result); + event.setAccount(result); + } catch (Exception ex) { + log.error(ex); + } + + return event; + } + + /** * Create a new {@link AccountEvent} and publish it to the account stream. * * @param event is the {@link AccountEvent} to publish to the account stream * @return a hypermedia {@link AccountEvent} resource */ - @CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()") - public AccountEvent createEvent(AccountEvent event) { + @CacheEvict(cacheNames = "events", key = "#id.toString()") + public AccountEvent createEvent(Long id, AccountEvent event) { // Save new event event = addEvent(event); Assert.notNull(event, "The event could not be appended to the account"); - // Append the account event to the stream - accountStreamSource.output() - .send(MessageBuilder - .withPayload(getAccountEventResource(event)) - .build()); - return event; } @@ -105,7 +189,7 @@ public class EventService { * @return a hypermedia resource for the supplied {@link AccountEvent} entity */ private Resource getAccountEventResource(AccountEvent event) { - return new Resource<>(event, Arrays.asList( + return new Resource(event, Arrays.asList( linkTo(AccountController.class) .slash("events") .slash(event.getEventId()) @@ -113,8 +197,7 @@ public class EventService { linkTo(AccountController.class) .slash("accounts") .slash(event.getAccount().getAccountId()) - .withRel("account")) - ); + .withRel("account"))); } /** @@ -125,7 +208,7 @@ public class EventService { */ @CacheEvict(cacheNames = "events", key = "#event.getAccount().getAccountId().toString()") private AccountEvent addEvent(AccountEvent event) { - event = eventRepository.save(event); + event = eventRepository.saveAndFlush(event); return event; } } diff --git a/account-parent/account-web/src/main/resources/application.yml b/account-parent/account-web/src/main/resources/application.yml index e940116..e66280b 100644 --- a/account-parent/account-web/src/main/resources/application.yml +++ b/account-parent/account-web/src/main/resources/application.yml @@ -14,4 +14,4 @@ spring: host: localhost port: 6379 server: - port: 0 \ No newline at end of file + port: 8080 \ No newline at end of file diff --git a/account-parent/account-web/src/test/java/demo/account/AccountControllerTest.java b/account-parent/account-web/src/test/java/demo/account/AccountControllerTest.java index 3812115..0ae456d 100644 --- a/account-parent/account-web/src/test/java/demo/account/AccountControllerTest.java +++ b/account-parent/account-web/src/test/java/demo/account/AccountControllerTest.java @@ -30,9 +30,9 @@ public class AccountControllerTest { @Test public void getUserAccountResourceShouldReturnAccount() throws Exception { - String content = "{\"userId\": 1, \"accountNumber\": \"123456789\", \"defaultAccount\": true}"; + String content = "{\"firstName\": \"Jane\", \"lastName\": \"Doe\", \"email\": \"jane.doe@example.com\"}"; - Account account = new Account(1L, "123456789", true); + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); given(this.accountService.getAccount(1L)) .willReturn(account); 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 dbdd816..0e7e33d 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 @@ -7,6 +7,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cache.CacheManager; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -21,49 +22,51 @@ public class AccountServiceTests { @MockBean private AccountRepository accountRepository; + @MockBean + private CacheManager cacheManager; + private AccountService accountService; @Before public void before() { - accountService = new AccountService(accountRepository, eventService); + accountService = new AccountService(accountRepository, eventService, cacheManager); } @Test public void getAccountReturnsAccount() throws Exception { - Account expected = new Account(1L, "123456789", true); - expected.setUserId(1L); + Account expected = new Account("Jane", "Doe", "jane.doe@example.com"); given(this.accountRepository.findOne(1L)).willReturn(expected); Account actual = accountService.getAccount(1L); assertThat(actual).isNotNull(); - assertThat(actual.getUserId()).isEqualTo(1L); - assertThat(actual.getAccountNumber()).isEqualTo("123456789"); + assertThat(actual.getEmail()).isEqualTo("jane.doe@example.com"); + assertThat(actual.getFirstName()).isEqualTo("Jane"); + assertThat(actual.getLastName()).isEqualTo("Doe"); } @Test public void createAccountReturnsAccount() throws Exception { - Account account = new Account(1L, "123456789", true); - account.setUserId(1L); + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setAccountId(1L); given(this.accountRepository.findOne(1L)).willReturn(account); - given(this.accountRepository.save(account)).willReturn(account); + given(this.accountRepository.saveAndFlush(account)).willReturn(account); Account actual = accountService.createAccount(account); assertThat(actual).isNotNull(); assertThat(actual.getStatus()).isEqualTo(AccountStatus.ACCOUNT_CREATED); - assertThat(actual.getUserId()).isEqualTo(1L); - assertThat(actual.getAccountNumber()).isEqualTo("123456789"); + assertThat(actual.getEmail()).isEqualTo("jane.doe@example.com"); + assertThat(actual.getFirstName()).isEqualTo("Jane"); + assertThat(actual.getLastName()).isEqualTo("Doe"); } @Test public void applyCommandSuspendsAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_ACTIVE); - account.setUserId(1L); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED); accountEvent.setAccount(account); @@ -72,7 +75,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.SUSPEND_ACCOUNT); @@ -83,9 +86,8 @@ public class AccountServiceTests { @Test public void applyCommandUnsuspendsAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_SUSPENDED); - account.setUserId(1L); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); accountEvent.setAccount(account); @@ -94,7 +96,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); @@ -105,9 +107,8 @@ public class AccountServiceTests { @Test public void applyCommandArchivesAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_ACTIVE); - account.setUserId(1L); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED); accountEvent.setAccount(account); @@ -116,7 +117,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.ARCHIVE_ACCOUNT); @@ -126,10 +127,9 @@ public class AccountServiceTests { } @Test - public void applyCommmandUnarchivesAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + public void applyCommandUnarchivesAccount() throws Exception { + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_ARCHIVED); - account.setUserId(1L); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); accountEvent.setAccount(account); @@ -138,7 +138,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); @@ -148,10 +148,9 @@ public class AccountServiceTests { } @Test - public void applyCommmandConfirmsAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + public void applyCommandConfirmsAccount() throws Exception { + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_PENDING); - account.setUserId(1L); AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED); accountEvent.setAccount(account); @@ -160,7 +159,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.CONFIRM_ACCOUNT); @@ -170,10 +169,10 @@ public class AccountServiceTests { } @Test - public void applyCommmandActivatesAccount() throws Exception { - Account account = new Account(1L, "123456789", true); + public void applyCommandActivatesAccount() throws Exception { + Account account = new Account("Jane", "Doe", "jane.doe@example.com"); account.setStatus(AccountStatus.ACCOUNT_CONFIRMED); - account.setUserId(1L); + AccountEvent accountEvent = new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED); accountEvent.setAccount(account); @@ -182,7 +181,7 @@ public class AccountServiceTests { 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))) + given(this.eventService.createEvent(1L, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED))) .willReturn(accountEvent); Account actual = accountService.applyCommand(1L, AccountCommand.ACTIVATE_ACCOUNT); diff --git a/account-parent/account-web/src/test/resources/data-h2.sql b/account-parent/account-web/src/test/resources/data-h2.sql index ffca7ae..df18478 100644 --- a/account-parent/account-web/src/test/resources/data-h2.sql +++ b/account-parent/account-web/src/test/resources/data-h2.sql @@ -1 +1 @@ -INSERT INTO ACCOUNT(ID, USER_ID, ACCOUNT_NUMBER, DEFAULT_ACCOUNT) values (1, 1, '123456789', FALSE); +INSERT INTO ACCOUNT(ID, FIRST_NAME, LAST_NAME, EMAIL) values (1, 'John', 'Doe', 'john.doe@example.com'); diff --git a/account-parent/account-worker/manifest.yml b/account-parent/account-worker/manifest.yml index 1552562..f1e91cc 100644 --- a/account-parent/account-worker/manifest.yml +++ b/account-parent/account-worker/manifest.yml @@ -1,11 +1,10 @@ -name: account-web -memory: 512M +name: account-worker +memory: 1024M instances: 1 -path: ./target/account-web-0.0.1-SNAPSHOT.jar +path: ./target/account-worker-0.0.1-SNAPSHOT.jar buildpack: java_buildpack services: - rabbit-events -- redis-cache disk_quota: 1024M -host: account-event-web +host: account-event-worker domain: cfapps.io diff --git a/account-parent/account-worker/pom.xml b/account-parent/account-worker/pom.xml index 27533e5..681f166 100644 --- a/account-parent/account-worker/pom.xml +++ b/account-parent/account-worker/pom.xml @@ -48,6 +48,16 @@ spring-statemachine-core 1.1.1.RELEASE + + org.kbastani + spring-boot-starter-aws-lambda + 1.0-SNAPSHOT + + + com.amazonaws + aws-java-sdk-sts + 1.11.67 + com.amazonaws aws-java-sdk-lambda diff --git a/account-parent/account-worker/src/main/java/demo/AccountStreamModuleApplication.java b/account-parent/account-worker/src/main/java/demo/AccountStreamModuleApplication.java index 0ace61e..53ac1ba 100644 --- a/account-parent/account-worker/src/main/java/demo/AccountStreamModuleApplication.java +++ b/account-parent/account-worker/src/main/java/demo/AccountStreamModuleApplication.java @@ -2,8 +2,11 @@ package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; @SpringBootApplication +@EnableHypermediaSupport(type = {HypermediaType.HAL}) public class AccountStreamModuleApplication { public static void main(String[] args) { SpringApplication.run(AccountStreamModuleApplication.class, args); diff --git a/account-parent/account-worker/src/main/java/demo/account/Account.java b/account-parent/account-worker/src/main/java/demo/account/Account.java index a941a3b..b004781 100644 --- a/account-parent/account-worker/src/main/java/demo/account/Account.java +++ b/account-parent/account-worker/src/main/java/demo/account/Account.java @@ -12,36 +12,36 @@ import demo.event.AccountEvent; * @author kbastani */ public class Account extends BaseEntity { - private Long userId; - private String accountNumber; - private Boolean defaultAccount; + private String firstName; + private String lastName; + private String email; private AccountStatus status; public Account() { } - public Long getUserId() { - return userId; + public String getFirstName() { + return firstName; } - public void setUserId(Long userId) { - this.userId = userId; + public void setFirstName(String firstName) { + this.firstName = firstName; } - public String getAccountNumber() { - return accountNumber; + public String getLastName() { + return lastName; } - public void setAccountNumber(String accountNumber) { - this.accountNumber = accountNumber; + public void setLastName(String lastName) { + this.lastName = lastName; } - public Boolean getDefaultAccount() { - return defaultAccount; + public String getEmail() { + return email; } - public void setDefaultAccount(Boolean defaultAccount) { - this.defaultAccount = defaultAccount; + public void setEmail(String email) { + this.email = email; } public AccountStatus getStatus() { @@ -51,13 +51,4 @@ public class Account extends BaseEntity { public void setStatus(AccountStatus status) { this.status = status; } - - @Override - public String toString() { - return "Account{" + - "userId=" + userId + - ", accountNumber='" + accountNumber + '\'' + - ", defaultAccount=" + defaultAccount + - "} " + super.toString(); - } } diff --git a/account-parent/account-worker/src/main/java/demo/config/AwsLambdaConfig.java b/account-parent/account-worker/src/main/java/demo/config/AwsLambdaConfig.java new file mode 100644 index 0000000..4fed012 --- /dev/null +++ b/account-parent/account-worker/src/main/java/demo/config/AwsLambdaConfig.java @@ -0,0 +1,23 @@ +package demo.config; + +import amazon.aws.AWSLambdaConfigurerAdapter; +import com.fasterxml.jackson.databind.ObjectMapper; +import demo.function.LambdaFunctions; +import demo.util.LambdaUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsLambdaConfig { + + @Bean + public LambdaFunctions lambdaInvoker(AWSLambdaConfigurerAdapter configurerAdapter) { + return configurerAdapter + .getFunctionInstance(LambdaFunctions.class); + } + + @Bean + public LambdaUtil lambdaUtil(ObjectMapper objectMapper) { + return new LambdaUtil(objectMapper); + } +} diff --git a/account-parent/account-worker/src/main/java/demo/config/StateMachineConfig.java b/account-parent/account-worker/src/main/java/demo/config/StateMachineConfig.java index ef8f266..e267e13 100644 --- a/account-parent/account-worker/src/main/java/demo/config/StateMachineConfig.java +++ b/account-parent/account-worker/src/main/java/demo/config/StateMachineConfig.java @@ -150,13 +150,13 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter * The body of this method shows an example of how to map an {@link AccountFunction} to a function - * defined in a class method of {@link CreateAccountFunction}. + * defined in a class method of {@link CreateAccount}. * * @return an implementation of {@link Action} that includes a function to execute */ @Bean public Action createAccount() { - return context -> applyEvent(context, new CreateAccountFunction(context)); + return context -> applyEvent(context, new CreateAccount(context)); } /** @@ -164,7 +164,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter * The body of this method shows an example of how to use a {@link java.util.function.Consumer} Java 8 - * lambda function instead of the class method definition that was shown in {@link CreateAccountFunction}. + * lambda function instead of the class method definition that was shown in {@link CreateAccount}. * * @return an implementation of {@link Action} that includes a function to execute */ @@ -172,9 +172,9 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter confirmAccount() { return context -> { // Map the account action to a Java 8 lambda function - ConfirmAccountFunction accountFunction; + ConfirmAccount accountFunction; - accountFunction = new ConfirmAccountFunction(context, event -> { + accountFunction = new ConfirmAccount(context, event -> { // Get the account resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("account").getHref()), @@ -188,6 +188,8 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter activateAccount() { return context -> applyEvent(context, - new ActivateAccountFunction(context, - event -> log.info(event.getType() + ": " + - event.getLink("account").getHref()))); + new ActivateAccount(context, event -> { + log.info(event.getType() + ": " + event.getLink("account").getHref()); + // Get the account resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Account.class) + .getBody(); + })); } /** @@ -217,9 +228,18 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter archiveAccount() { return context -> applyEvent(context, - new ArchiveAccountFunction(context, - event -> log.info(event.getType() + ": " + - event.getLink("account").getHref()))); + new ArchiveAccount(context, event -> { + log.info(event.getType() + ": " + event.getLink("account").getHref()); + // Get the account resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Account.class) + .getBody(); + })); } /** @@ -231,9 +251,18 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter suspendAccount() { return context -> applyEvent(context, - new SuspendAccountFunction(context, - event -> log.info(event.getType() + ": " + - event.getLink("account").getHref()))); + new SuspendAccount(context, event -> { + log.info(event.getType() + ": " + event.getLink("account").getHref()); + // Get the account resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Account.class) + .getBody(); + })); } /** @@ -245,9 +274,18 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter unarchiveAccount() { return context -> applyEvent(context, - new UnarchiveAccountFunction(context, - event -> log.info(event.getType() + ": " + - event.getLink("account").getHref()))); + new UnarchiveAccount(context, event -> { + log.info(event.getType() + ": " + event.getLink("account").getHref()); + // Get the account resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Account.class) + .getBody(); + })); } /** @@ -259,9 +297,18 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter unsuspendAccount() { return context -> applyEvent(context, - new UnsuspendAccountFunction(context, - event -> log.info(event.getType() + ": " + - event.getLink("account").getHref()))); + new UnsuspendAccount(context, event -> { + log.info(event.getType() + ": " + event.getLink("account").getHref()); + // Get the account resource for the event + Traverson traverson = new Traverson( + URI.create(event.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + return traverson.follow("self") + .toEntity(Account.class) + .getBody(); + })); } } diff --git a/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java b/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java index 56f25e9..933c240 100644 --- a/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java +++ b/account-parent/account-worker/src/main/java/demo/event/AccountEventStream.java @@ -1,23 +1,12 @@ package demo.event; import demo.account.Account; -import demo.account.AccountStatus; -import demo.state.StateMachineService; -import org.apache.log4j.Logger; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.cloud.stream.messaging.Sink; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.client.Traverson; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.StateMachine; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; - /** * The {@link AccountEventStream} monitors for a variety of {@link AccountEvent} domain * events for an {@link Account}. @@ -28,11 +17,10 @@ import java.util.Map; @EnableBinding(Sink.class) public class AccountEventStream { - final private Logger log = Logger.getLogger(AccountEventStream.class); - final private StateMachineService stateMachineService; + private EventService eventService; - public AccountEventStream(StateMachineService stateMachineService) { - this.stateMachineService = stateMachineService; + public AccountEventStream(EventService eventService) { + this.eventService = eventService; } /** @@ -41,48 +29,10 @@ public class AccountEventStream { * reproduces the current state of the {@link Account} resource that is the * subject of the {@link AccountEvent}. * - * @param accountEvent + * @param accountEvent is the {@link Account} domain event to process */ @StreamListener(Sink.INPUT) public void streamListerner(AccountEvent accountEvent) { - log.info("Account event received: " + accountEvent.getLink("self").getHref()); - - // Generate a state machine for computing the state of the account resource - StateMachine stateMachine = - stateMachineService.getStateMachine(); - - // Follow the hypermedia link to fetch the attached account - Traverson traverson = new Traverson( - URI.create(accountEvent.getLink("account").getHref()), - MediaTypes.HAL_JSON - ); - - // Get the event log for the attached account resource - AccountEvents events = traverson.follow("events") - .toEntity(AccountEvents.class) - .getBody(); - - // Prepare account event message headers - Map headerMap = new HashMap<>(); - headerMap.put("event", accountEvent); - - // Replicate the current state of the account resource - events.getContent() - .stream() - .sorted((a1, a2) -> a1.getCreatedAt().compareTo(a2.getCreatedAt())) - .forEach(e -> { - MessageHeaders headers = new MessageHeaders(null); - - // Check to see if this is the current event - if (e.getLink("self").equals(accountEvent.getLink("self"))) { - headers = new MessageHeaders(headerMap); - } - - // Send the event to the state machine - stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), headers)); - }); - - // Destroy the state machine - stateMachine.stop(); + eventService.apply(accountEvent); } } diff --git a/account-parent/account-worker/src/main/java/demo/event/EventController.java b/account-parent/account-worker/src/main/java/demo/event/EventController.java new file mode 100644 index 0000000..57e8e40 --- /dev/null +++ b/account-parent/account-worker/src/main/java/demo/event/EventController.java @@ -0,0 +1,28 @@ +package demo.event; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@RestController +@RequestMapping("/v1") +public class EventController { + + private EventService eventService; + + public EventController(EventService eventService) { + this.eventService = eventService; + } + + @PostMapping(path = "/events") + public ResponseEntity handleEvent(@RequestBody AccountEvent event) { + return Optional.ofNullable(eventService.apply(event)) + .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED)) + .orElseThrow(() -> new RuntimeException("Apply event failed")); + } +} diff --git a/account-parent/account-worker/src/main/java/demo/event/EventService.java b/account-parent/account-worker/src/main/java/demo/event/EventService.java new file mode 100644 index 0000000..ab492bd --- /dev/null +++ b/account-parent/account-worker/src/main/java/demo/event/EventService.java @@ -0,0 +1,82 @@ +package demo.event; + +import demo.account.Account; +import demo.account.AccountStatus; +import demo.state.StateMachineService; +import org.apache.log4j.Logger; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.client.Traverson; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +@Service +public class EventService { + + final private Logger log = Logger.getLogger(EventService.class); + final private StateMachineService stateMachineService; + + public EventService(StateMachineService stateMachineService) { + this.stateMachineService = stateMachineService; + } + + public Account apply(AccountEvent accountEvent) { + + Account result; + + log.info("Account event received: " + accountEvent.getLink("self").getHref()); + + // Generate a state machine for computing the state of the account resource + StateMachine stateMachine = + stateMachineService.getStateMachine(); + + // Follow the hypermedia link to fetch the attached account + Traverson traverson = new Traverson( + URI.create(accountEvent.getLink("account").getHref()), + MediaTypes.HAL_JSON + ); + + // Get the event log for the attached account resource + AccountEvents events = traverson.follow("events") + .toEntity(AccountEvents.class) + .getBody(); + + // Prepare account event message headers + Map headerMap = new HashMap<>(); + headerMap.put("event", accountEvent); + + // Replicate the current state of the account resource + events.getContent() + .stream() + .sorted((a1, a2) -> a1.getCreatedAt().compareTo(a2.getCreatedAt())) + .forEach(e -> { + MessageHeaders headers = new MessageHeaders(null); + + // Check to see if this is the current event + if (e.getLink("self").equals(accountEvent.getLink("self"))) { + headers = new MessageHeaders(headerMap); + } + + // Send the event to the state machine + stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), headers)); + }); + + + // Get result + Map context = stateMachine.getExtendedState() + .getVariables(); + + // Get the account result + result = (Account) context.getOrDefault("account", null); + + // Destroy the state machine + stateMachine.stop(); + + return result; + } +} diff --git a/account-parent/account-worker/src/main/java/demo/function/AccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/AccountFunction.java index fad5788..e4a5d35 100644 --- a/account-parent/account-worker/src/main/java/demo/function/AccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/AccountFunction.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -18,8 +19,8 @@ import java.util.function.Consumer; public abstract class AccountFunction { final private Logger log = Logger.getLogger(AccountFunction.class); - final private StateContext context; - final private Consumer lambda; + final protected StateContext context; + final protected Function lambda; /** * Create a new instance of a class that extends {@link AccountFunction}, supplying @@ -30,7 +31,7 @@ public abstract class AccountFunction { * @param lambda is the lambda function describing an action that consumes an {@link AccountEvent} */ public AccountFunction(StateContext context, - Consumer lambda) { + Function lambda) { this.context = context; this.lambda = lambda; } @@ -41,8 +42,10 @@ public abstract class AccountFunction { * * @param event is the {@link AccountEvent} to apply to the lambda function */ - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { // Execute the lambda function - lambda.accept(event); + Account result = lambda.apply(event); + context.getExtendedState().getVariables().put("account", result); + return result; } } diff --git a/account-parent/account-worker/src/main/java/demo/function/AccountService.java b/account-parent/account-worker/src/main/java/demo/function/AccountService.java deleted file mode 100644 index 6d3f733..0000000 --- a/account-parent/account-worker/src/main/java/demo/function/AccountService.java +++ /dev/null @@ -1,4 +0,0 @@ -package demo.function; - -public class AccountService { -} diff --git a/account-parent/account-worker/src/main/java/demo/function/ActivateAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/ActivateAccount.java similarity index 69% rename from account-parent/account-worker/src/main/java/demo/function/ActivateAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/ActivateAccount.java index da42ec5..bf41531 100644 --- a/account-parent/account-worker/src/main/java/demo/function/ActivateAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/ActivateAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class ActivateAccountFunction extends AccountFunction { +public class ActivateAccount extends AccountFunction { - final private Logger log = Logger.getLogger(ActivateAccountFunction.class); + final private Logger log = Logger.getLogger(ActivateAccount.class); - public ActivateAccountFunction(StateContext context, Consumer lambda) { + public ActivateAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class ActivateAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for an activated account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/function/ArchiveAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/ArchiveAccount.java similarity index 69% rename from account-parent/account-worker/src/main/java/demo/function/ArchiveAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/ArchiveAccount.java index 24a8511..f55e009 100644 --- a/account-parent/account-worker/src/main/java/demo/function/ArchiveAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/ArchiveAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class ArchiveAccountFunction extends AccountFunction { +public class ArchiveAccount extends AccountFunction { - final private Logger log = Logger.getLogger(ArchiveAccountFunction.class); + final private Logger log = Logger.getLogger(ArchiveAccount.class); - public ArchiveAccountFunction(StateContext context, Consumer lambda) { + public ArchiveAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class ArchiveAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for an archived account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/function/ConfirmAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/ConfirmAccount.java similarity index 69% rename from account-parent/account-worker/src/main/java/demo/function/ConfirmAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/ConfirmAccount.java index b60fc4a..8a8f4b4 100644 --- a/account-parent/account-worker/src/main/java/demo/function/ConfirmAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/ConfirmAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class ConfirmAccountFunction extends AccountFunction { +public class ConfirmAccount extends AccountFunction { - final private Logger log = Logger.getLogger(ConfirmAccountFunction.class); + final private Logger log = Logger.getLogger(ConfirmAccount.class); - public ConfirmAccountFunction(StateContext context, Consumer lambda) { + public ConfirmAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class ConfirmAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for a confirmed account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/function/CreateAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/CreateAccount.java similarity index 83% rename from account-parent/account-worker/src/main/java/demo/function/CreateAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/CreateAccount.java index 8152a6c..9a0ba87 100644 --- a/account-parent/account-worker/src/main/java/demo/function/CreateAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/CreateAccount.java @@ -12,7 +12,7 @@ import org.springframework.statemachine.StateContext; import org.springframework.web.client.RestTemplate; import java.net.URI; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -21,16 +21,16 @@ import java.util.function.Consumer; * * @author kbastani */ -public class CreateAccountFunction extends AccountFunction { +public class CreateAccount extends AccountFunction { - final private Logger log = Logger.getLogger(CreateAccountFunction.class); + final private Logger log = Logger.getLogger(CreateAccount.class); - public CreateAccountFunction(StateContext context) { + public CreateAccount(StateContext context) { this(context, null); } - public CreateAccountFunction(StateContext context, - Consumer function) { + public CreateAccount(StateContext context, + Function function) { super(context, function); } @@ -40,7 +40,10 @@ public class CreateAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} for this context */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { + + Account account; + log.info("Executing workflow for a created account..."); // Create a traverson for the root account @@ -50,7 +53,7 @@ public class CreateAccountFunction extends AccountFunction { ); // Get the account resource attached to the event - Account account = traverson.follow("self") + account = traverson.follow("self") .toEntity(Account.class) .getBody(); @@ -68,6 +71,10 @@ public class CreateAccountFunction extends AccountFunction { log.info(event.getType() + ": " + event.getLink("account").getHref()); } + + context.getExtendedState().getVariables().put("account", account); + + return account; } /** diff --git a/account-parent/account-worker/src/main/java/demo/function/LambdaFunctions.java b/account-parent/account-worker/src/main/java/demo/function/LambdaFunctions.java new file mode 100644 index 0000000..ffb860c --- /dev/null +++ b/account-parent/account-worker/src/main/java/demo/function/LambdaFunctions.java @@ -0,0 +1,31 @@ +package demo.function; + +import com.amazonaws.services.lambda.invoke.LambdaFunction; +import com.amazonaws.services.lambda.model.LogType; +import demo.account.Account; + +import java.util.Map; + +public interface LambdaFunctions { + + @LambdaFunction(functionName="account-created-accountCreated-13P0EDGLDE399", logType = LogType.Tail) + Account accountCreated(Map event); + + @LambdaFunction(functionName="accountConfirmed", logType = LogType.Tail) + Account accountConfirmed(Map event); + + @LambdaFunction(functionName="account-activated-accountActivated-1P0I6FTFCMHKH", logType = LogType.Tail) + Account accountActivated(Map event); + + @LambdaFunction(functionName="accountSuspended", logType = LogType.Tail) + Account accountSuspended(Map event); + + @LambdaFunction(functionName="accountArchived", logType = LogType.Tail) + Account accountArchived(Map event); + + @LambdaFunction(functionName="accountUnsuspended", logType = LogType.Tail) + Account accountUnsuspended(Map event); + + @LambdaFunction(functionName="accountUnarchived", logType = LogType.Tail) + Account accountUnarchived(Map event); +} diff --git a/account-parent/account-worker/src/main/java/demo/function/SuspendAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/SuspendAccount.java similarity index 69% rename from account-parent/account-worker/src/main/java/demo/function/SuspendAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/SuspendAccount.java index eaf7696..ab4fae0 100644 --- a/account-parent/account-worker/src/main/java/demo/function/SuspendAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/SuspendAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class SuspendAccountFunction extends AccountFunction { +public class SuspendAccount extends AccountFunction { - final private Logger log = Logger.getLogger(SuspendAccountFunction.class); + final private Logger log = Logger.getLogger(SuspendAccount.class); - public SuspendAccountFunction(StateContext context, Consumer lambda) { + public SuspendAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class SuspendAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for a suspended account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/function/UnarchiveAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/UnarchiveAccount.java similarity index 74% rename from account-parent/account-worker/src/main/java/demo/function/UnarchiveAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/UnarchiveAccount.java index ac8e42f..a82cd40 100644 --- a/account-parent/account-worker/src/main/java/demo/function/UnarchiveAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/UnarchiveAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class UnarchiveAccountFunction extends AccountFunction { +public class UnarchiveAccount extends AccountFunction { - final private Logger log = Logger.getLogger(UnarchiveAccountFunction.class); + final private Logger log = Logger.getLogger(UnarchiveAccount.class); - public UnarchiveAccountFunction(StateContext context, Consumer lambda) { + public UnarchiveAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class UnarchiveAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for an unarchived account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/function/UnsuspendAccountFunction.java b/account-parent/account-worker/src/main/java/demo/function/UnsuspendAccount.java similarity index 74% rename from account-parent/account-worker/src/main/java/demo/function/UnsuspendAccountFunction.java rename to account-parent/account-worker/src/main/java/demo/function/UnsuspendAccount.java index d1e33b6..7c2434b 100644 --- a/account-parent/account-worker/src/main/java/demo/function/UnsuspendAccountFunction.java +++ b/account-parent/account-worker/src/main/java/demo/function/UnsuspendAccount.java @@ -1,12 +1,13 @@ package demo.function; +import demo.account.Account; import demo.account.AccountStatus; import demo.event.AccountEvent; import demo.event.AccountEventType; import org.apache.log4j.Logger; import org.springframework.statemachine.StateContext; -import java.util.function.Consumer; +import java.util.function.Function; /** * The {@link AccountFunction} is an abstraction used to map actions that are triggered by @@ -15,11 +16,11 @@ import java.util.function.Consumer; * * @author kbastani */ -public class UnsuspendAccountFunction extends AccountFunction { +public class UnsuspendAccount extends AccountFunction { - final private Logger log = Logger.getLogger(UnsuspendAccountFunction.class); + final private Logger log = Logger.getLogger(UnsuspendAccount.class); - public UnsuspendAccountFunction(StateContext context, Consumer lambda) { + public UnsuspendAccount(StateContext context, Function lambda) { super(context, lambda); } @@ -30,8 +31,8 @@ public class UnsuspendAccountFunction extends AccountFunction { * @param event is the {@link AccountEvent} to apply to the lambda function */ @Override - public void apply(AccountEvent event) { + public Account apply(AccountEvent event) { log.info("Executing workflow for a unsuspended account..."); - super.apply(event); + return super.apply(event); } } diff --git a/account-parent/account-worker/src/main/java/demo/util/LambdaUtil.java b/account-parent/account-worker/src/main/java/demo/util/LambdaUtil.java new file mode 100644 index 0000000..bd282f4 --- /dev/null +++ b/account-parent/account-worker/src/main/java/demo/util/LambdaUtil.java @@ -0,0 +1,29 @@ +package demo.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; + +@Component +public class LambdaUtil { + + private ObjectMapper objectMapper; + + public LambdaUtil(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public HashMap objectToMap(Object object) { + HashMap result = null; + + try { + result = objectMapper.readValue(objectMapper.writeValueAsString(object), HashMap.class); + } catch (IOException e) { + e.printStackTrace(); + } + + return result; + } +} diff --git a/account-parent/account-worker/src/main/resources/application.yml b/account-parent/account-worker/src/main/resources/application.yml index 716efce..f29e29d 100644 --- a/account-parent/account-worker/src/main/resources/application.yml +++ b/account-parent/account-worker/src/main/resources/application.yml @@ -14,4 +14,8 @@ spring: consumer: durableSubscription: true server: - port: 0 \ No newline at end of file + port: 8081 +amazon: + aws: + access-key-id: replace + access-key-secret: replace \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2ba004b..a9e2d9d 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ order-parent user-parent warehouse-parent + spring-boot-starter-aws-lambda diff --git a/spring-boot-starter-aws-lambda/pom.xml b/spring-boot-starter-aws-lambda/pom.xml new file mode 100755 index 0000000..18674fd --- /dev/null +++ b/spring-boot-starter-aws-lambda/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + spring-boot-starter-aws-lambda + jar + + + org.kbastani + event-stream-processing-parent + 1.0-SNAPSHOT + ../ + + + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-test + + + com.amazonaws + aws-java-sdk-lambda + + + com.amazonaws + aws-java-sdk-sts + 1.11.67 + + + junit + junit + test + + + + + + + com.amazonaws + aws-java-sdk-bom + 1.11.67 + pom + import + + + + + diff --git a/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AWSLambdaConfigurerAdapter.java b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AWSLambdaConfigurerAdapter.java new file mode 100755 index 0000000..c8ccc51 --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AWSLambdaConfigurerAdapter.java @@ -0,0 +1,88 @@ +package amazon.aws; + +import com.amazonaws.auth.*; +import com.amazonaws.services.lambda.AWSLambdaClientBuilder; +import com.amazonaws.services.lambda.invoke.LambdaInvokerFactory; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; +import com.amazonaws.services.securitytoken.model.Credentials; +import com.amazonaws.services.securitytoken.model.GetSessionTokenRequest; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * This class is a client for interacting with Amazon S3 bucket resources. + * + * @author kbastani + */ +@Component +public class AWSLambdaConfigurerAdapter { + + private String accessKeyId; + private String accessKeySecret; + private Credentials sessionCredentials; + + /** + * Create a new instance of the {@link AWSLambdaConfigurerAdapter} with the bucket name and access credentials + * + * @param accessKeyId is the access key id credential for the specified bucket name + * @param accessKeySecret is the access key secret for the specified bucket name + */ + public AWSLambdaConfigurerAdapter(String accessKeyId, + String accessKeySecret) { + this.accessKeyId = accessKeyId; + this.accessKeySecret = accessKeySecret; + } + + /** + * Gets an instance of a function interface + * @param type + * @param + * @return + */ + public T getFunctionInstance(Class type) { + return LambdaInvokerFactory.builder() + .lambdaClient(AWSLambdaClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider( + getBasicSessionCredentials())) + .build()) + .build(type); + } + + /** + * Get the basic session credentials for the template's configured IAM authentication keys + * + * @return a {@link BasicSessionCredentials} instance with a valid authenticated session token + */ + private BasicSessionCredentials getBasicSessionCredentials() { + + // Create a new session token if the session is expired or not initialized + if (sessionCredentials == null || sessionCredentials.getExpiration().before(new Date())) + sessionCredentials = getSessionCredentials(); + + // Create basic session credentials using the generated session token + return new BasicSessionCredentials(sessionCredentials.getAccessKeyId(), + sessionCredentials.getSecretAccessKey(), + sessionCredentials.getSessionToken()); + } + + /** + * Creates a new session credential that is valid for 12 hours + * + * @return an authenticated {@link Credentials} for the new session token + */ + private Credentials getSessionCredentials() { + // Create a new session with the user credentials for the service instance + AWSSecurityTokenServiceClient stsClient = + new AWSSecurityTokenServiceClient(new BasicAWSCredentials(accessKeyId, accessKeySecret)); + + // Start a new session for managing a service instance's bucket + GetSessionTokenRequest getSessionTokenRequest = + new GetSessionTokenRequest().withDurationSeconds(43200); + + // Get the session token for the service instance's bucket + sessionCredentials = stsClient.getSessionToken(getSessionTokenRequest).getCredentials(); + + return sessionCredentials; + } +} diff --git a/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonAutoConfiguration.java b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonAutoConfiguration.java new file mode 100755 index 0000000..d6ff64b --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonAutoConfiguration.java @@ -0,0 +1,28 @@ +package amazon.aws; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * This class auto-configures a {@link AWSLambdaConfigurerAdapter} bean. + * + * @author kbastani + */ +@Configuration +@ConditionalOnMissingBean(AWSLambdaConfigurerAdapter.class) +@EnableConfigurationProperties(AmazonProperties.class) +public class AmazonAutoConfiguration { + + @Autowired + private AmazonProperties amazonProperties; + + @Bean + protected AWSLambdaConfigurerAdapter lambdaAdapter() { + return new AWSLambdaConfigurerAdapter( + amazonProperties.getAws().getAccessKeyId(), + amazonProperties.getAws().getAccessKeySecret()); + } +} diff --git a/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonProperties.java b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonProperties.java new file mode 100755 index 0000000..07bb250 --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/java/amazon/aws/AmazonProperties.java @@ -0,0 +1,89 @@ +package amazon.aws; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration property group for Amazon S3 and AWS + * + * @author kbastani + */ +@Configuration +@ConfigurationProperties(prefix = "amazon") +public class AmazonProperties { + + @NestedConfigurationProperty + private Aws aws; + + /** + * A property group for Amazon Web Service (AWS) configurations + * + * @return a property group for AWS configurations + */ + public Aws getAws() { + return aws; + } + + /** + * A property group for Amazon Web Service (AWS) configurations + * + * @param aws is a property group for AWS configurations + */ + public void setAws(Aws aws) { + this.aws = aws; + } + + /** + * A property group for Amazon Web Service (AWS) configurations + */ + public static class Aws { + + private String accessKeyId; + private String accessKeySecret; + + /** + * A valid AWS account's access key id. + * + * @return an AWS access key id + */ + public String getAccessKeyId() { + return accessKeyId; + } + + /** + * A valid AWS account's access key id. + * + * @param accessKeyId is a valid AWS account's access key id. + */ + public void setAccessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + } + + /** + * A valid AWS account's secret access token. + * + * @return an AWS account's secret access key + */ + public String getAccessKeySecret() { + return accessKeySecret; + } + + /** + * A valid AWS account's secret access token. + * + * @param accessKeySecret is a valid AWS account's secret access token. + */ + public void setAccessKeySecret(String accessKeySecret) { + this.accessKeySecret = accessKeySecret; + } + + @Override + public String toString() { + return "Aws{" + + "accessKeyId='" + accessKeyId + '\'' + + ", accessKeySecret='" + accessKeySecret + '\'' + + '}'; + } + } +} diff --git a/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100755 index 0000000..b17d82e --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + { + "name": "amazon", + "type": "amazon.aws.AmazonProperties", + "sourceType": "amazon.aws.AmazonProperties" + } + ] +} \ No newline at end of file diff --git a/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring.factories b/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring.factories new file mode 100755 index 0000000..e31075a --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=amazon.aws.AmazonAutoConfiguration \ No newline at end of file diff --git a/spring-boot-starter-aws-lambda/src/main/resources/application.properties b/spring-boot-starter-aws-lambda/src/main/resources/application.properties new file mode 100755 index 0000000..9ac8812 --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/main/resources/application.properties @@ -0,0 +1,2 @@ +amazon.aws.access-key-id=replace +amazon.aws.access-key-secret=replace diff --git a/spring-boot-starter-aws-lambda/src/test/java/amazon/aws/AmazonConfigurationTest.java b/spring-boot-starter-aws-lambda/src/test/java/amazon/aws/AmazonConfigurationTest.java new file mode 100755 index 0000000..b1a111f --- /dev/null +++ b/spring-boot-starter-aws-lambda/src/test/java/amazon/aws/AmazonConfigurationTest.java @@ -0,0 +1,44 @@ +package amazon.aws; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static junit.framework.TestCase.assertNotNull; + +public class AmazonConfigurationTest { + + private AnnotationConfigApplicationContext context; + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void defaultAdapter() { + load(EmptyConfiguration.class, + "amazon.aws.access-key-id=AJGLDLSXKDFLS", + "amazon.aws.access-key-secret=XSDFSDFLKKHASDFJALASDF"); + + AWSLambdaConfigurerAdapter amazonS3Template = this.context.getBean(AWSLambdaConfigurerAdapter.class); + assertNotNull(amazonS3Template); + } + + @Configuration + static class EmptyConfiguration { + } + + private void load(Class config, String... environment) { + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(applicationContext, environment); + applicationContext.register(config); + applicationContext.register(AmazonAutoConfiguration.class); + applicationContext.refresh(); + this.context = applicationContext; + } +}