1 Commits

Author SHA1 Message Date
Chris Richardson
d78f88337a Migrated to new Eventuate Java Client 2016-08-30 19:15:50 -07:00
322 changed files with 3375 additions and 68033 deletions

128
README.md
View File

@@ -1,4 +1,4 @@
# Event-Sourcing+CQRS example application
#Event-Sourcing+CQRS example application
This example application is the money transfer application described in my talk [Building and deploying microservices with event sourcing, CQRS and Docker](http://plainoldobjects.com/presentations/building-and-deploying-microservices-with-event-sourcing-cqrs-and-docker/).
This talk describes a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
@@ -35,89 +35,111 @@ The following diagram shows the architecture:
![Money transfer architecture](https://github.com/cer/event-sourcing-examples/wiki/i/applicationarchitecture.png)
There are the following services:
There are four logical services:
* Customers Service - REST API for creating customers
* Accounts Service - REST API for creating accounts
* Transactions Service - REST API for transferring money
* Customers View Service - subscribes to events and updates a MongoDB View, and provides an API for retrieving customers
* Accounts View Service - subscribes to events and updates a MongoDB View, and provides an API for retrieving accounts
* Accounts (command-side) - REST API for creating accounts
* Money transfers (command-side) - REST API for transferring money
* Account view updater (query-side) - subscribes to events and updates a MongoDB View
* Account view reader (query-side) - REST API for retrieving accounts
There is also an [API gateway](http://microservices.io/patterns/apigateway.html) service that acts as a Facade in front of the services.
One of the neat things about the modular architecture is that there are two ways to deploy these four services:
* monolithic-service - all services are packaged as a single Spring Boot executable JAR
* Microservices - three separate Spring Boot executable JARs
* accounts-command-side-service - command-side accounts
* transactions-command-side-service - command-side money transfers
* accounts-query-side-service - Account View Updater and Account View Reader
# About the examples
There are currently the following versions of the example application:
* java-spring - a Java and Spring Boot example
* scala-spring - a Scala and Spring Boot example (NOTE: this version is lagging the Java Spring and hasn't been updated in a longtime.)
* scala-spring - a Scala and Spring Boot example
Other examples will be added shortly including a Scala/Play example.
For more information, please see the [wiki](../../wiki)
# About the Eventuate Platform
# About the Event Store
The application is built using [Eventuate](http://eventuate.io/), which is an application platform for writing transactional microservices.
It provides a simple yet powerful event-driven programming model that is based on event sourcing and Command Query Responsibility Segregation (CQRS).
Eventuate solves the distributed data management problems inherent in a microservice architecture.
It consists of a scalable, distributed event store and client libraries for various languages and frameworks including Java, Scala, and the Spring framework.
The application uses one of two event stores:
There are two versions of Eventuate:
* Embedded SQL-based event store, which is great for integration tests.
It is also used when running the monolithic version of the application.
* Event Store server - this is a full featured event store.
See this [wiki page](../../wiki/AboutTheEventStoreServer) for more details.
* [Eventuate SaaS server](http://eventuate.io/usingeventuate.html) - this is a full featured event store that is hosted on AWS
* [Eventuate Local](http://eventuate.io/usingeventuate.html) - an open-source event store that is built using MySQL and Kafka
# Building the application (and running the tests)
There is also an embedded test event store, which is great for integration tests.
# Building and running the microservices
This is a Gradle project.
However, you do not need to install Gradle since it will be downloaded automatically.
You just need to have Java 8 installed.
The details of how to build and run the services depend slightly on whether you are using Eventuate SaaS or Eventuate Local.
## Building and running using Eventuate SaaS
First, must [sign up to get your credentials](https://signup.eventuate.io/) in order to get free access to the SaaS version.
Next, build the application
Both versions of the application use Gradle.
To build an application, execute this command in the application's top-level directory:
```
cd java-spring
./gradlew assemble
```
Next, you can launch the services using [Docker Compose](https://docs.docker.com/compose/):
Note: you do not need to install Gradle.
It will be automatically downloaded by `./gradlew`.
This will build a Spring Boot jar in each of the `*-service` directories.
You can also run the tests using `gradle build`.
However, you must set some environment variables.
First, you need to tell the query side code how to connect to MongoDB:
```
docker-compose up -d
export SPRING_DATA_MONGODB_URI=mongodb://192.168.59.103/yourdb
```
Finally, you can open the home page, which is served up by the API Gateway: `http://$DOCKER_HOST_IP:8080`
Note: `DOCKER_HOST_IP` is the IP address of the machine where Docker is running, e.g. the IP address of the VirtualBox VM.
## Building and running using Eventuate Local
First, build the application
[Docker Compose](https://docs.docker.com/compose/) is a great way to run MongoDB.
You can run the `docker-compose up -d mongodb` to run MongoDB and then set `SPRING_DATA_MONGODB_URI` as follows:
```
cd java-spring
./gradlew assemble -P eventuateDriver=local
export SPRING_DATA_MONGODB_URI=mongodb://$(docker-machine ip default)/yourdb
```
Next, launch the services using [Docker Compose](https://docs.docker.com/compose/):
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test require you to set some environment variables that tell them how to connect to the Event Store server.
But don't worry.
The build is configured to ignore failures for those projects.
# Running the application
To run the application, you must to set the SPRING_DATA_MONGODB_URI environment variable, which tells the query services how to connect to MongoDB.
There are a couple of different ways of running the application.
## Running the monolithic application
One option is to run the self-contained monolithic application.
It uses the embedded event store.
Simply use this command:
```
export DOCKER_HOST_IP=...
docker-compose -f docker-compose-eventuate-local.yml up -d
java -jar monolithic-service/build/libs/monolithic-service.jar
```
Note: You need to set `DOCKER_HOST_IP` before running Docker Compose.
This must be an IP address or resolvable hostname.
It cannot be `localhost`.
See this [guide to setting `DOCKER_HOST_IP`](http://eventuate.io/docs/usingdocker.html) for more information.
This will start the service running on port 8080 (you can change using the --server.port=9999 option).
Finally, you can open the home page, which is served up by the API Gateway: `http://$DOCKER_HOST_IP:8080`
Once the service has started you can open the Swagger UI: http://localhost:8080/swagger-ui.html.
You can then:
1. Create two accounts (save the account ids)
2. Create a money transfer
3. View the updated account balances
## Running the microservices
The other option is to run the services separately.
However, in order to do this you need to [get credentials for the Event Store](../../wiki/AboutTheEventStoreServer).
One way to run the services is to use the scripts `run-all-services.sh`, which runs the services, and `kill-all-services.sh`, which kills the processes.
A much better way, however, is to use Docker Compose.
Simply run the command `docker-compose up` to launch the services.
This will create containers for MongoDB and each of the services.
You can now, for example, use the curl commands in `handy-curl-commands.sh` to interact with the server.
You can also use the Swagger UI exposed by each service `http://host:port/swagger-ui.html`.

103
Vagrantfile vendored Normal file
View File

@@ -0,0 +1,103 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure(2) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://atlas.hashicorp.com/search.
config.vm.box = "ubuntu/trusty64"
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
end
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
# such as FTP and Heroku are also available. See the documentation at
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
# config.push.define "atlas" do |push|
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
# end
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL
#!/bin/sh
# https://github.com/pussinboots/vagrant-devel/blob/master/provision/packages/java8.sh
if which java >/dev/null; then
echo "skip java 8 installation"
else
echo "java 8 installation"
apt-get install --yes python-software-properties
add-apt-repository ppa:webupd8team/java
apt-get update -qq
echo debconf shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
echo debconf shared/accepted-oracle-license-v1-1 seen true | /usr/bin/debconf-set-selections
apt-get install --yes oracle-java8-installer
yes "" | apt-get -f install
fi
SHELL
config.vm.provision "docker" do |d|
end
config.vm.provision "shell", inline: <<-SHELL
if which docker-compose >/dev/null; then
echo "skip docker-compose installation"
else
sudo bash -c "curl -L https://github.com/docker/compose/releases/download/1.6.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose ; chmod +x /usr/local/bin/docker-compose"
fi
SHELL
end

View File

@@ -2,27 +2,10 @@
set -e
if [ -z "$DOCKER_HOST_IP" ] ; then
if [ -z "$DOCKER_HOST" ] ; then
export DOCKER_HOST_IP=`hostname`
else
echo using ${DOCKER_HOST?}
XX=${DOCKER_HOST%\:*}
export DOCKER_HOST_IP=${XX#tcp\:\/\/}
fi
echo set DOCKER_HOST_IP $DOCKER_HOST_IP
fi
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DOCKER_COMPOSE="docker-compose -p event-sourcing-examples"
if [ "$1" = "-f" ] ; then
shift;
DOCKER_COMPOSE="$DOCKER_COMPOSE -f ${1?}"
shift
fi
if [ "$1" = "--use-existing" ] ; then
shift;
else
@@ -30,16 +13,17 @@ else
${DOCKER_COMPOSE?} rm -v --force
fi
NO_RM=false
${DOCKER_COMPOSE?} up -d mongodb
if [ "$1" = "--no-rm" ] ; then
NO_RM=true
shift
if [ -z "$DOCKER_HOST_IP" ] ; then
if which docker-machine >/dev/null; then
export DOCKER_HOST_IP=$(docker-machine ip default)
else
export DOCKER_HOST_IP=localhost
fi
echo set DOCKER_HOST_IP $DOCKER_HOST_IP
fi
${DOCKER_COMPOSE?} up -d mongodb $EXTRA_INFRASTRUCTURE_SERVICES
if [ -z "$SPRING_DATA_MONGODB_URI" ] ; then
export SPRING_DATA_MONGODB_URI=mongodb://${DOCKER_HOST_IP?}/mydb
echo Set SPRING_DATA_MONGODB_URI $SPRING_DATA_MONGODB_URI
@@ -47,24 +31,21 @@ fi
export SERVICE_HOST=$DOCKER_HOST_IP
./gradlew $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS $* build -x :e2e-test:test
./gradlew $* build -x :e2e-test:test
if [ -z "$EVENTUATE_LOCAL" ] && [ -z "$EVENTUATE_API_KEY_ID" -o -z "$EVENTUATE_API_KEY_SECRET" ] ; then
if [ -z "$EVENTUATE_API_KEY_ID" -o -z "$EVENTUATE_API_KEY_SECRET" ] ; then
echo You must set EVENTUATE_API_KEY_ID and EVENTUATE_API_KEY_SECRET
exit -1
fi
${DOCKER_COMPOSE?} build
${DOCKER_COMPOSE?} up -d
$DIR/wait-for-services.sh $DOCKER_HOST_IP 8080 8081 8082 8083 8084
$DIR/wait-for-services.sh $DOCKER_HOST_IP 8080 8081 8082
set -e
./gradlew $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
./gradlew -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
if [ $NO_RM = false ] ; then
${DOCKER_COMPOSE?} stop
${DOCKER_COMPOSE?} rm -v --force
fi
${DOCKER_COMPOSE?} stop
${DOCKER_COMPOSE?} rm -v --force

6
gradle-all.sh Executable file
View File

@@ -0,0 +1,6 @@
#! /bin/bash -e
for dir in java-spring scala-spring; do
(cd $dir ; ./gradlew -b build.gradle $*)
done

View File

@@ -2,10 +2,34 @@ This is the Java/Spring version of the Event Sourcing/CQRS money transfer exampl
# About the application
This application consists of the following microservices:
This application consists of three microservices:
* Account Service - the command side business logic for Accounts
* Account View Service - query side implementation of a MongoDB-based, denormalized view of Accounts
* Customer Service - the command side business logic for Customers
* Customer View Service - query side implementation of a MongoDB-based, denormalized view of Customers
* Transaction Service - the command side business logic for Money Transfers
* Money Transfer Service - the command side business logic for Money Transfers
* Query service - query side implementation of a MongoDB-based, denormalized view of Accounts and MoneyTransfers
The Account Service consists of the following modules:
* accounts-command-side-backend - the Account aggregate
* accounts-command-side-web - a REST API for creating and retrieving Accounts
* accounts-command-side-service - a standalone microservice
The Money Transfer Service consists of the following modules:
* transactions-command-side-backend - the MoneyTransfer aggregate
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
* transactions-command-side-service - a standalone microservice
The Query Service consists the following modules:
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
* accounts-query-side-web - a REST API for querying the denormalized view
* accounts-query-side-service - a standalone microservice
# Deploying the application
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simplified integration testing.
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
* monolithic-service - all-in-one, monolithic packaging of the application

View File

@@ -0,0 +1,14 @@
apply plugin: 'java'
dependencies {
compile project(":common-backend")
compile "io.eventuate.client.java:eventuate-client-java-spring:$eventuateClientVersion"
testCompile project(":testutil")
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}

View File

@@ -1,31 +1,25 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.Event;
import io.eventuate.EventUtil;
import io.eventuate.ReflectiveMutableCommandProcessingAggregate;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitFailedDueToInsufficientFundsEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class Account extends ReflectiveMutableCommandProcessingAggregate<Account, AccountCommand> {
private BigDecimal balance;
private boolean deleted;
public List<Event> process(OpenAccountCommand cmd) {
return EventUtil.events(new AccountOpenedEvent(cmd.getCustomerId(), cmd.getTitle(), cmd.getInitialBalance(), cmd.getDescription()));
}
public List<Event> process(DeleteAccountCommand cmd) {
return EventUtil.events(new AccountDeletedEvent());
}
public List<Event> process(DebitAccountCommand cmd) {
if(deleted)
return new ArrayList<>();
if (balance.compareTo(cmd.getAmount()) < 0)
return EventUtil.events(new AccountDebitFailedDueToInsufficientFundsEvent(cmd.getTransactionId()));
else
@@ -33,9 +27,6 @@ public class Account extends ReflectiveMutableCommandProcessingAggregate<Account
}
public List<Event> process(CreditAccountCommand cmd) {
if(deleted)
return new ArrayList<>();
return EventUtil.events(new AccountCreditedEvent(cmd.getAmount(), cmd.getTransactionId()));
}
@@ -43,10 +34,6 @@ public class Account extends ReflectiveMutableCommandProcessingAggregate<Account
balance = event.getInitialBalance();
}
public void apply(AccountDeletedEvent event) {
deleted = true;
}
public void apply(AccountDebitedEvent event) {
balance = balance.subtract(event.getAmount());
}

View File

@@ -0,0 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.Command;
interface AccountCommand extends Command {
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.AggregateRepository;
import io.eventuate.EventuateAggregateStore;
@@ -8,7 +8,7 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@EnableEventHandlers
public class AccountsBackendConfiguration {
public class AccountConfiguration {
@Bean
public AccountWorkflow accountWorkflow() {

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.AggregateRepository;
@@ -19,7 +19,4 @@ public class AccountService {
return accountRepository.save(new OpenAccountCommand(customerId, title, initialBalance, description));
}
public CompletableFuture<EntityWithIdAndVersion<Account>> deleteAccount(String accountId) {
return accountRepository.update(accountId, new DeleteAccountCommand());
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.EntityWithIdAndVersion;
@@ -22,7 +22,13 @@ public class AccountWorkflow {
String fromAccountId = event.getDetails().getFromAccountId();
return ctx.update(Account.class, fromAccountId, new DebitAccountCommand(amount, transactionId));
return ctx.update(Account.class, fromAccountId, new DebitAccountCommand(amount, transactionId)).handle((x, e) -> {
if (e != null) {
e.printStackTrace();
}
return x;
}
);
}
@EventHandlerMethod
@@ -32,6 +38,13 @@ public class AccountWorkflow {
String fromAccountId = event.getDetails().getToAccountId();
String transactionId = ctx.getEntityId();
return ctx.update(Account.class, fromAccountId, new CreditAccountCommand(amount, transactionId));
return ctx.update(Account.class, fromAccountId, new CreditAccountCommand(amount, transactionId)).handle((x, e) -> {
if (e != null) {
e.printStackTrace();
}
return x;
}
);
}
}

View File

@@ -1,4 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.Aggregate;
import java.math.BigDecimal;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import java.math.BigDecimal;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import java.math.BigDecimal;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AbstractEntityEventTest;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import io.eventuate.Event;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;

View File

@@ -0,0 +1,22 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: 'spring-boot'
dependencies {
compile project(":accounts-command-side-web")
compile project(":common-swagger")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}
test {
ignoreFailures System.getenv("EVENTUATE_API_KEY_ID") == null
}

View File

@@ -1,6 +1,9 @@
package net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.web;
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.backend.CustomerViewBackendConfiguration;
import io.eventuate.javaclient.spring.httpstomp.EventuateHttpStompClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CommandSideWebAccountsConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@@ -8,16 +11,19 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@Import({CustomerViewBackendConfiguration.class})
@Import({CommandSideWebAccountsConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class CustomersViewWebConfiguration extends WebMvcConfigurerAdapter {
public class AccountsCommandSideServiceConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
}

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.main;
import net.chrisrichardson.eventstore.javaexamples.banking.web.AccountsCommandSideServiceConfiguration;
import org.springframework.boot.SpringApplication;
public class AccountsCommandSideServiceMain {
public static void main(String[] args) {
SpringApplication.run(AccountsCommandSideServiceConfiguration.class, args);
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice;
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
@@ -25,7 +25,7 @@ public class AccountsCommandSideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/api" + path;
return "http://localhost:" + port + "/" + path;
}
@Autowired
@@ -33,9 +33,10 @@ public class AccountsCommandSideServiceIntegrationTest {
@Test
public void shouldCreateAccounts() {
public void shouldCreateAccountsAndTransferMoney() {
BigDecimal initialFromAccountBalance = new BigDecimal(500);
BigDecimal initialToAccountBalance = new BigDecimal(100);
BigDecimal amountToTransfer = new BigDecimal(150);
String customerId = "00000000-00000000";
String title = "My Account";
@@ -47,6 +48,8 @@ public class AccountsCommandSideServiceIntegrationTest {
Assert.assertNotNull(fromAccountId);
Assert.assertNotNull(toAccountId);
}

View File

@@ -1,10 +1,5 @@
package net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice;
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.web.CustomersWebConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.web.CustomersViewWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -17,9 +12,8 @@ import java.util.Arrays;
import java.util.List;
@Configuration
@Import({CustomersWebConfiguration.class, CustomersViewWebConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class, AuthConfiguration.class})
@EnableAutoConfiguration
public class CustomersQuerySideServiceTestConfiguration {
@Import(AccountsCommandSideServiceConfiguration.class)
public class AccountsCommandSideServiceTestConfiguration {
@Bean
public RestTemplate restTemplate(HttpMessageConverters converters) {

View File

@@ -0,0 +1,12 @@
dependencies {
compile project(":accounts-command-side-backend")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}

View File

@@ -1,17 +1,19 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.DeleteAccountResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/accounts")
@RequestMapping("/accounts")
public class AccountController {
private AccountService accountService;
@@ -24,12 +26,6 @@ public class AccountController {
@RequestMapping(method = RequestMethod.POST)
public CompletableFuture<CreateAccountResponse> createAccount(@Validated @RequestBody CreateAccountRequest request) {
return accountService.openAccount(request.getCustomerId(), request.getTitle(), request.getInitialBalance(), request.getDescription())
.thenApply(entityAndEventInfo -> new CreateAccountResponse(entityAndEventInfo.getEntityId()));
}
@RequestMapping(value = "{accountId}", method = RequestMethod.DELETE)
public CompletableFuture<DeleteAccountResponse> deleteAccount(@PathVariable String accountId) {
return accountService.deleteAccount(accountId)
.thenApply(entityAndEventInfo -> new DeleteAccountResponse(accountId));
.thenApply(entityAndEventInfo -> new CreateAccountResponse(entityAndEventInfo.getEntityId()));
}
}

View File

@@ -0,0 +1,16 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountConfiguration.class})
@ComponentScan
@EnableAutoConfiguration
public class CommandSideWebAccountsConfiguration {
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import org.junit.Before;
import org.junit.Test;
@@ -35,7 +35,7 @@ public class AccountControllerIntegrationTest {
@Test
public void shouldCreateAccount() throws Exception {
mockMvc.perform(post("/api/accounts")
mockMvc.perform(post("/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"customerId\" : \"00000000-00000000\", \"initialBalance\" : 500}")
.accept(MediaType.APPLICATION_JSON))
@@ -44,7 +44,7 @@ public class AccountControllerIntegrationTest {
@Test
public void shouldRejectBadRequest() throws Exception {
mockMvc.perform(post("/api/accounts")
mockMvc.perform(post("/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"initialBalanceXXX\" : 500}")
.accept(MediaType.APPLICATION_JSON))

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({CommandSideWebAccountsConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
public class AccountControllerIntegrationTestConfiguration {
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- [%thread] -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</Pattern>
</layout>
</appender>
<root level="error">
<appender-ref ref="STDOUT" />
</root>
<logger name="org.springframework.web" level='debug'>
</logger>
</configuration>

View File

@@ -1,15 +1,8 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: EventuateDependencyPlugin
apply plugin: 'spring-boot'
apply plugin: 'java'
dependencies {
compile project(":common-swagger")
compile project(":common-backend")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
compile "io.eventuate.client.java:eventuate-client-java-spring:$eventuateClientVersion"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
@@ -17,8 +10,6 @@ dependencies {
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}
test {
ignoreFailures System.getenv("EVENTUATE_API_KEY_ID") == null
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountChangeInfo {
private String changeId;
private String transactionId;
private String transactionType;
private long amount;
private long balanceDelta;
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
this.changeId = changeId;
this.transactionId = transactionId;
this.transactionType = transactionType;
this.amount = amount;
this.balanceDelta = balanceDelta;
}
}

View File

@@ -1,10 +1,8 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import com.fasterxml.jackson.annotation.JsonProperty;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import java.util.*;
import java.util.List;
/**
* Created by cer on 11/21/14.
@@ -17,19 +15,13 @@ public class AccountInfo {
private String description;
private long balance;
private List<AccountChangeInfo> changes;
private Map<String, AccountTransactionInfo> transactions;
private List<AccountTransactionInfo> transactions;
private String version;
@JsonProperty("date")
private Date creationDate;
private AccountInfo() {
}
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version) {
this(id, customerId, title, description, balance, changes, transactions, version, new Date());
}
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version, Date creationDate) {
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, List<AccountTransactionInfo> transactions, String version) {
this.id = id;
this.customerId = customerId;
@@ -39,7 +31,6 @@ public class AccountInfo {
this.changes = changes;
this.transactions = transactions;
this.version = version;
this.creationDate = creationDate;
}
public String getId() {
@@ -63,18 +54,14 @@ public class AccountInfo {
}
public List<AccountChangeInfo> getChanges() {
return changes == null ? Collections.EMPTY_LIST : changes;
return changes;
}
public List<AccountTransactionInfo> getTransactions() {
return transactions == null ? Collections.EMPTY_LIST : new ArrayList<>(transactions.values());
return transactions;
}
public String getVersion() {
return version;
}
public Date getCreationDate() {
return creationDate;
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import org.springframework.data.mongodb.repository.MongoRepository;

View File

@@ -0,0 +1,68 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import com.mongodb.WriteResult;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.math.BigDecimal;
import java.util.Collections;
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
import static org.springframework.data.mongodb.core.query.Criteria.where;
public class AccountInfoUpdateService {
private Logger logger = LoggerFactory.getLogger(getClass());
private AccountInfoRepository accountInfoRepository;
private MongoTemplate mongoTemplate;
public AccountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
this.accountInfoRepository = accountInfoRepository;
this.mongoTemplate = mongoTemplate;
}
public void create(String accountId, String customerId, String title, BigDecimal initialBalance, String description, String version) {
try {
accountInfoRepository.save(new AccountInfo(
accountId,
customerId,
title,
description,
toIntegerRepr(initialBalance),
Collections.<AccountChangeInfo>emptyList(),
Collections.<AccountTransactionInfo>emptyList(),
version));
logger.info("Saved in mongo");
} catch (Throwable t) {
logger.error("Error during saving: ");
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
}
}
public void addTransaction(String eventId, String fromAccountId, AccountTransactionInfo ti) {
mongoTemplate.updateMulti(new Query(where("id").is(fromAccountId)), /* wrong .and("version").lt(eventId) */
new Update().
push("transactions", ti).
set("version", eventId),
AccountInfo.class);
}
public void updateBalance(String accountId, String changeId, long balanceDelta, AccountChangeInfo ci) {
WriteResult x = mongoTemplate.updateMulti(new Query(where("id").is(accountId).and("version").lt(changeId)),
new Update().
inc("balance", balanceDelta).
push("changes", ci).
set("version", changeId),
AccountInfo.class);
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountNotFoundException extends RuntimeException {

View File

@@ -0,0 +1,27 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.CompletableFutureUtil;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class AccountQueryService {
private AccountInfoRepository accountInfoRepository;
public AccountQueryService(AccountInfoRepository accountInfoRepository) {
this.accountInfoRepository = accountInfoRepository;
}
public CompletableFuture<AccountInfo> findByAccountId(String accountId) {
AccountInfo account = accountInfoRepository.findOne(accountId);
if (account == null)
return CompletableFutureUtil.failedFuture(new AccountNotFoundException(accountId));
else
return CompletableFuture.completedFuture(account);
}
public CompletableFuture<List<AccountInfo>> findByCustomerId(String customerId) {
return CompletableFuture.completedFuture(accountInfoRepository.findByCustomerId(customerId));
}
}

View File

@@ -1,23 +1,21 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.DispatchedEvent;
import io.eventuate.EventHandlerMethod;
import io.eventuate.EventSubscriber;
import io.eventuate.Int128;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.CreditRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.FailedDebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountChangedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import static net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.MoneyUtil.toIntegerRepr;
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
@EventSubscriber(id="querySideEventHandlers")
public class AccountQueryWorkflow {
@@ -33,31 +31,24 @@ public class AccountQueryWorkflow {
public void create(DispatchedEvent<AccountOpenedEvent> de) {
AccountOpenedEvent event = de.getEvent();
String id = de.getEntityId();
Int128 eventId = de.getEventId();
logger.info("**************** account version={}, {}", id, eventId);
String eventId = de.getEventId().asString();
logger.info("**************** account version=" + id + ", " + eventId);
BigDecimal initialBalance = event.getInitialBalance();
String customerId = event.getCustomerId();
String title = event.getTitle();
String description = event.getDescription();
accountInfoUpdateService.create(id, customerId, title, initialBalance, description, eventId);
}
@EventHandlerMethod
public void delete(DispatchedEvent<AccountDeletedEvent> de) {
String id = de.getEntityId();
accountInfoUpdateService.delete(id);
}
@EventHandlerMethod
public void recordTransfer(DispatchedEvent<MoneyTransferCreatedEvent> de) {
String eventId = de.getEventId().asString();
String moneyTransferId = de.getEntityId();
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
String toAccountId = de.getEvent().getDetails().getToAccountId();
logger.info("**************** account version={}, {}", fromAccountId, eventId);
logger.info("**************** account version={}, {}", toAccountId, eventId);
logger.info("**************** account version=" + fromAccountId + ", " + de.getEventId().asString());
logger.info("**************** account version=" + toAccountId + ", " + de.getEventId().asString());
AccountTransactionInfo ti = new AccountTransactionInfo(moneyTransferId,
fromAccountId,
@@ -66,8 +57,8 @@ public class AccountQueryWorkflow {
de.getEvent().getDetails().getDate(),
de.getEvent().getDetails().getDescription());
accountInfoUpdateService.addTransaction(fromAccountId, ti);
accountInfoUpdateService.addTransaction(toAccountId, ti);
accountInfoUpdateService.addTransaction(eventId, fromAccountId, ti);
accountInfoUpdateService.addTransaction(eventId, toAccountId, ti);
}
@EventHandlerMethod
@@ -80,36 +71,6 @@ public class AccountQueryWorkflow {
saveChange(de, +1);
}
@EventHandlerMethod
public void updateDebitTransactionState(DispatchedEvent<DebitRecordedEvent> de) {
String transactionId = de.getEntityId();
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
String toAccountId = de.getEvent().getDetails().getToAccountId();
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.DEBITED);
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.DEBITED);
}
@EventHandlerMethod
public void updateCreditTransactionState(DispatchedEvent<CreditRecordedEvent> de) {
String transactionId = de.getEntityId();
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
String toAccountId = de.getEvent().getDetails().getToAccountId();
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.COMPLETED);
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.COMPLETED);
}
@EventHandlerMethod
public void recordFailed(DispatchedEvent<FailedDebitRecordedEvent> de) {
String transactionId = de.getEntityId();
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
String toAccountId = de.getEvent().getDetails().getToAccountId();
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
}
public <T extends AccountChangedEvent> void saveChange(DispatchedEvent<T> de, int delta) {
String changeId = de.getEventId().asString();
String transactionId = de.getEvent().getTransactionId();
@@ -118,8 +79,9 @@ public class AccountQueryWorkflow {
long balanceDelta = amount * delta;
AccountChangeInfo ci = new AccountChangeInfo(changeId, transactionId, de.getEvent().getClass().getSimpleName(), amount, balanceDelta);
String accountId = de.getEntityId();
logger.info("**************** account version={}, {}", accountId, de.getEventId().asString());
logger.info("**************** account version=" + accountId + ", " + de.getEventId().asString());
accountInfoUpdateService.updateBalance(accountId, changeId, balanceDelta, ci);
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import java.math.BigDecimal;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.javaclient.spring.EnableEventHandlers;
@@ -10,7 +10,7 @@ import org.springframework.data.mongodb.repository.config.EnableMongoRepositorie
@Configuration
@EnableMongoRepositories
@EnableEventHandlers
public class AccountViewBackendConfiguration {
public class QuerySideAccountConfiguration {
@Bean
public AccountQueryWorkflow accountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
@@ -27,6 +27,8 @@ public class AccountViewBackendConfiguration {
return new AccountQueryService(accountInfoRepository);
}
@Bean
public QuerySideDependencyChecker querysideDependencyChecker(MongoTemplate mongoTemplate) {
return new QuerySideDependencyChecker(mongoTemplate);

View File

@@ -1,10 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
public class QuerySideDependencyChecker {
private Logger logger = LoggerFactory.getLogger(getClass());

View File

@@ -1,19 +1,20 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: EventuateDependencyPlugin
apply plugin: 'spring-boot'
dependencies {
compile project(":common-backend")
compile project(":accounts-query-side-web")
compile project(":common-swagger")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile project(":testutil")
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}
test {

View File

@@ -1,6 +1,9 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web;
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
import io.eventuate.javaclient.spring.httpstomp.EventuateHttpStompClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.QuerySideWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@@ -10,9 +13,11 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
@Import({AccountViewBackendConfiguration.class})
@Import({QuerySideWebConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class AccountViewWebConfiguration {
public class AccountsQuerySideServiceConfiguration {
@Bean
public HttpMessageConverters customConverters() {

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.main;
import net.chrisrichardson.eventstore.javaexamples.banking.web.AccountsQuerySideServiceConfiguration;
import org.springframework.boot.SpringApplication;
public class AccountsQuerySideServiceMain {
public static void main(String[] args) {
SpringApplication.run(AccountsQuerySideServiceConfiguration.class, args);
}
}

View File

@@ -30,7 +30,7 @@ public class AccountsQuerySideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/api" + path;
return "http://localhost:" + port + "/" + path;
}
@Autowired

View File

@@ -1,9 +1,5 @@
package net.chrisrichardson.eventstore.javaexamples.banking.customersservice;
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.web.CustomersWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -16,17 +12,8 @@ import java.util.Arrays;
import java.util.List;
@Configuration
@Import({CustomersWebConfiguration.class,
EmbeddedTestAggregateStoreConfiguration.class,
AuthConfiguration.class})
@EnableAutoConfiguration
public class CustomersCommandSideServiceTestConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
@Import(AccountsQuerySideServiceConfiguration.class)
public class AccountsQuerySideServiceTestConfiguration {
@Bean
public RestTemplate restTemplate(HttpMessageConverters converters) {

View File

@@ -0,0 +1,10 @@
dependencies {
compile project(":accounts-query-side-backend")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
}

View File

@@ -0,0 +1,14 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.QuerySideAccountConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({QuerySideAccountConfiguration.class})
@ComponentScan
public class QuerySideWebConfiguration {
}

View File

@@ -0,0 +1,50 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountNotFoundException;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@RestController
public class AccountQueryController {
private AccountQueryService accountInfoQueryService;
@Autowired
public AccountQueryController(AccountQueryService accountInfoQueryService) {
this.accountInfoQueryService = accountInfoQueryService;
}
@RequestMapping(value = "/accounts/{accountId}", method = RequestMethod.GET)
public CompletableFuture<GetAccountResponse> get(@PathVariable String accountId) {
return accountInfoQueryService.findByAccountId(accountId)
.thenApply(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
}
@RequestMapping(value = "/accounts", method = RequestMethod.GET)
public CompletableFuture<List<GetAccountResponse>> getAccountsForCustomer(@RequestParam("customerId") String customerId) {
return accountInfoQueryService.findByCustomerId(customerId)
.thenApply(accountInfoList -> accountInfoList.stream().map(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription())).collect(Collectors.toList()));
}
@RequestMapping(value = "/accounts/{accountId}/history", method = RequestMethod.GET)
public CompletableFuture<List<AccountTransactionInfo>> getTransactionsHistory(@PathVariable String accountId) {
return accountInfoQueryService.findByAccountId(accountId)
.thenApply(AccountInfo::getTransactions);
}
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="account not found")
@ExceptionHandler(AccountNotFoundException.class)
public void accountNotFound() {
}
}

View File

@@ -1,4 +0,0 @@
FROM java:openjdk-8u91-jdk
CMD java ${JAVA_OPTS} -jar accounts-service.jar
EXPOSE 8080
COPY build/libs/accounts-service.jar .

View File

@@ -1,21 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice;
import io.eventuate.javaclient.driver.EventuateDriverConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web.AccountsWebConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountsWebConfiguration.class, EventuateDriverConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class AccountsServiceMain {
public static void main(String[] args) {
SpringApplication.run(AccountsServiceMain.class, args);
}
}

View File

@@ -1,7 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
import io.eventuate.Command;
interface AccountCommand extends Command {
}

View File

@@ -1,4 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
public class DeleteAccountCommand implements AccountCommand {
}

View File

@@ -1,15 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountsBackendConfiguration.class})
@ComponentScan
public class AccountsWebConfiguration {
}

View File

@@ -1,13 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountsWebConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class})
@EnableAutoConfiguration
public class AccountControllerIntegrationTestConfiguration {
}

View File

@@ -1,4 +0,0 @@
FROM java:openjdk-8u91-jdk
CMD java ${JAVA_OPTS} -jar accounts-view-service.jar
EXPOSE 8080
COPY build/libs/accounts-view-service.jar .

View File

@@ -1,21 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice;
import io.eventuate.javaclient.driver.EventuateDriverConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web.AccountViewWebConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountViewWebConfiguration.class, EventuateDriverConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class AccountsViewServiceMain {
public static void main(String[] args) {
SpringApplication.run(AccountsViewServiceMain.class, args);
}
}

View File

@@ -1,85 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
import com.mongodb.WriteResult;
import io.eventuate.Int128;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.math.BigDecimal;
import java.util.Date;
import static net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.MoneyUtil.toIntegerRepr;
import static org.springframework.data.mongodb.core.query.Criteria.where;
public class AccountInfoUpdateService {
private Logger logger = LoggerFactory.getLogger(getClass());
private AccountInfoRepository accountInfoRepository;
private MongoTemplate mongoTemplate;
public AccountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
this.accountInfoRepository = accountInfoRepository;
this.mongoTemplate = mongoTemplate;
}
public void create(String accountId, String customerId, String title, BigDecimal initialBalance, String description, Int128 version) {
try {
AccountChangeInfo ci = new AccountChangeInfo();
ci.setAmount(toIntegerRepr(initialBalance));
WriteResult x = mongoTemplate.upsert(new Query(where("id").is(accountId).and("version").exists(false)),
new Update()
.set("customerId", customerId)
.set("title", title)
.set("description", description)
.set("balance", toIntegerRepr(initialBalance))
.push("changes", ci)
.set("creationDate", new Date(version.getHi()))
.set("version", version.asString()),
AccountInfo.class);
logger.info("Saved in mongo");
} catch (DuplicateKeyException t) {
logger.warn("When saving ", t);
} catch (Throwable t) {
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
}
}
public void delete(String accountId) {
accountInfoRepository.delete(accountId);
}
public void addTransaction(String accountId, AccountTransactionInfo ti) {
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
set("transactions." + ti.getTransactionId(), ti),
AccountInfo.class);
}
public void updateBalance(String accountId, String changeId, long balanceDelta, AccountChangeInfo ci) {
WriteResult x = mongoTemplate.updateMulti(new Query(where("id").is(accountId).and("version").lt(changeId)),
new Update().
inc("balance", balanceDelta).
push("changes", ci).
set("version", changeId),
AccountInfo.class);
}
public void updateTransactionStatus(String accountId, String transactionId, TransferState status) {
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
set("transactions." + transactionId + ".status", status),
AccountInfo.class);
}
}

View File

@@ -1,24 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
import java.util.List;
public class AccountQueryService {
private AccountInfoRepository accountInfoRepository;
public AccountQueryService(AccountInfoRepository accountInfoRepository) {
this.accountInfoRepository = accountInfoRepository;
}
public AccountInfo findByAccountId(String accountId) {
AccountInfo account = accountInfoRepository.findOne(accountId);
if (account == null)
throw new AccountNotFoundException(accountId);
else
return account;
}
public List<AccountInfo> findByCustomerId(String customerId) {
return accountInfoRepository.findByCustomerId(customerId);
}
}

View File

@@ -1,64 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountNotFoundException;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
public class AccountQueryController {
private AccountQueryService accountInfoQueryService;
@Autowired
public AccountQueryController(AccountQueryService accountInfoQueryService) {
this.accountInfoQueryService = accountInfoQueryService;
}
@RequestMapping(value = "/accounts/{accountId}", method = RequestMethod.GET)
public ResponseEntity<GetAccountResponse> get(@PathVariable String accountId) {
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
return ResponseEntity.ok().body(new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
}
@RequestMapping(value = "/customers/{customerId}/accounts", method = RequestMethod.GET)
public ResponseEntity<GetAccountsResponse> getAccountsForCustomer(@PathVariable("customerId") String customerId) {
return ResponseEntity.ok().body(
new GetAccountsResponse(
accountInfoQueryService.findByCustomerId(customerId)
.stream()
.map(accountInfo -> new GetAccountResponse(
accountInfo.getId(),
new BigDecimal(accountInfo.getBalance()),
accountInfo.getTitle(),
accountInfo.getDescription()))
.collect(Collectors.toList())
)
);
}
@RequestMapping(value = "/accounts/{accountId}/history", method = RequestMethod.GET)
public ResponseEntity<AccountHistoryResponse> getTransactionsHistory(@PathVariable String accountId) {
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
List<AccountHistoryEntry> historyEntries = new ArrayList<>();
historyEntries.add(new AccountOpenInfo(accountInfo.getCreationDate(), AccountHistoryEntry.EntryType.account, accountInfo.getChanges().get(0).getAmount()));
accountInfo.getTransactions().forEach(historyEntries::add);
return ResponseEntity.ok().body(new AccountHistoryResponse(historyEntries));
}
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "account not found")
@ExceptionHandler(AccountNotFoundException.class)
public void accountNotFound() {
}
}

View File

@@ -1,154 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.Int128;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import io.eventuate.javaclient.spring.jdbc.IdGenerator;
import io.eventuate.javaclient.spring.jdbc.IdGeneratorImpl;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfoUpdateService;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AccountInfoUpdateServiceTest.AccountInfoUpdateServiceTestConfiguration.class)
@IntegrationTest
public class AccountInfoUpdateServiceTest {
@Configuration
@EnableAutoConfiguration
@Import({AccountViewBackendConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
public static class AccountInfoUpdateServiceTestConfiguration {
}
@Autowired
private AccountInfoUpdateService accountInfoUpdateService;
@Autowired
private AccountQueryService accountQueryService;
@Test
public void shouldSaveAccountInfo() throws ExecutionException, InterruptedException {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
Int128 version = x.genId();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertEquals(customerId, accountInfo.getCustomerId());
assertEquals(title, accountInfo.getTitle());
assertEquals(description, accountInfo.getDescription());
assertEquals(initialBalance.longValue() * 100, accountInfo.getBalance());
assertEquals(1, accountInfo.getChanges().size());
assertTrue(accountInfo.getTransactions().isEmpty());
assertEquals(version.asString(), accountInfo.getVersion());
String changeId = x.genId().asString();
String transactionId = x.genId().asString();
AccountChangeInfo change = new AccountChangeInfo(changeId, transactionId, AccountCreditedEvent.class.getSimpleName(),
500, +1);
accountInfoUpdateService.updateBalance(accountId, changeId, 500,
change);
accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(initialBalance.add(new BigDecimal(5)).longValue() * 100, accountInfo.getBalance());
assertFalse(accountInfo.getChanges().isEmpty());
assertEquals(change, accountInfo.getChanges().get(1));
String eventId = x.genId().asString();
AccountTransactionInfo ti = new AccountTransactionInfo(transactionId, accountId, accountId, 5, new Date(), "A transfer");
accountInfoUpdateService.addTransaction(accountId, ti);
accountInfo = accountQueryService.findByAccountId(accountId);
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(ti, accountInfo.getTransactions().get(0));
}
@Test
public void shouldHandleDuplicateSaveAccountInfo() throws ExecutionException, InterruptedException {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
Int128 version = x.genId();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
}
@Test
public void shouldUpdateTransactionStatus() {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
Int128 version = x.genId();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
String transactionId = x.genId().asString();
AccountTransactionInfo transactionInfo = new AccountTransactionInfo();
transactionInfo.setTransactionId(transactionId);
transactionInfo.setStatus(TransferState.INITIAL);
accountInfoUpdateService.addTransaction(accountId, transactionInfo);
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(1, accountInfo.getTransactions().size());
assertEquals(TransferState.INITIAL, accountInfo.getTransactions().get(0).getStatus());
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(1, accountInfo.getTransactions().size());
assertEquals(TransferState.COMPLETED, accountInfo.getTransactions().get(0).getStatus());
}
}

View File

@@ -1,4 +0,0 @@
FROM java:openjdk-8u91-jdk
CMD java ${JAVA_OPTS} -jar api-gateway-service.jar
EXPOSE 8080
COPY build/libs/api-gateway-service.jar .

View File

@@ -14,7 +14,7 @@ dependencies {
}
task copyWebStatic(type: Copy) {
from "../../js-frontend/build"
from "../../prebuilt-web-client"
into "build/resources/main/static"
}

View File

@@ -5,6 +5,9 @@ import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
/**
* Created by popikyardo on 15.01.16.
*/
@ConfigurationProperties(prefix = "api.gateway")
public class ApiGatewayProperties {

View File

@@ -20,6 +20,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
import java.util.Collections;
/**
* Created by popikyardo on 15.01.16.
*/
@Configuration
@ComponentScan
@EnableAutoConfiguration

View File

@@ -2,6 +2,9 @@ package net.chrisrichardson.eventstore.javaexamples.banking.apigateway;
import org.springframework.http.HttpStatus;
/**
* Created by popikyardo on 07.12.15.
*/
public class RestUtil {
public static boolean isError(HttpStatus status) {

View File

@@ -4,7 +4,6 @@ import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGateway
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.ContentRequestTransformer;
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.HeadersRequestTransformer;
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.URLRequestTransformer;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
@@ -13,11 +12,9 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
@@ -30,8 +27,12 @@ import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* Created by popikyardo on 15.01.16.
*/
@RestController
public class GatewayController {
@@ -51,21 +52,13 @@ public class GatewayController {
.build();
}
@RequestMapping(value = "/api/**", method = {GET, POST, DELETE})
@ResponseBody
@RequestMapping(value = "/**", method = {GET, POST})
public ResponseEntity<String> proxyRequest(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, IOException, URISyntaxException {
HttpUriRequest proxiedRequest = createHttpUriRequest(request);
logger.info("request: {}", proxiedRequest);
HttpResponse proxiedResponse = httpClient.execute(proxiedRequest);
logger.info("Response {}", proxiedResponse.getStatusLine().getStatusCode());
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), makeResponseHeaders(proxiedResponse), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
}
private HttpHeaders makeResponseHeaders(HttpResponse response) {
HttpHeaders result = new HttpHeaders();
Header h = response.getFirstHeader("Content-Type");
result.set(h.getName(), h.getValue());
return result;
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
}
private HttpUriRequest createHttpUriRequest(HttpServletRequest request) throws URISyntaxException, NoSuchRequestHandlingMethodException, IOException {

View File

@@ -10,6 +10,9 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
/**
* Created by popikyardo on 21.01.16.
*/
public class ContentRequestTransformer extends ProxyRequestTransformer {
@Override

View File

@@ -8,6 +8,9 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Enumeration;
/**
* Created by popikyardo on 21.01.16.
*/
public class HeadersRequestTransformer extends ProxyRequestTransformer {
@Override

View File

@@ -7,6 +7,9 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URISyntaxException;
/**
* Created by popikyardo on 21.01.16.
*/
public abstract class ProxyRequestTransformer {
protected ProxyRequestTransformer predecessor;

View File

@@ -9,6 +9,9 @@ import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
/**
* Created by popikyardo on 21.01.16.
*/
public class URLRequestTransformer extends ProxyRequestTransformer {
private ApiGatewayProperties apiGatewayProperties;

View File

@@ -5,27 +5,18 @@ customers.queryside.service.host=localhost
transfers.commandside.service.host=localhost
api.gateway.endpoints[0].path=[/]*api/accounts.*
api.gateway.endpoints[0].path=[/]*accounts.*
api.gateway.endpoints[0].method=GET
api.gateway.endpoints[0].location=http://${accounts.queryside.service.host}:8080
api.gateway.endpoints[1].path=[/]*api/customers.*/accounts
api.gateway.endpoints[1].method=GET
api.gateway.endpoints[1].location=http://${accounts.queryside.service.host}:8080
api.gateway.endpoints[2].path=[/]*api/accounts.*
api.gateway.endpoints[2].method=POST
api.gateway.endpoints[2].location=http://${accounts.commandside.service.host}:8080
api.gateway.endpoints[3].path=[/]*api/customers.*
api.gateway.endpoints[3].method=GET
api.gateway.endpoints[3].location=http://${customers.queryside.service.host}:8080
api.gateway.endpoints[4].path=[/]*api/customers.*
api.gateway.endpoints[1].path=[/]*accounts.*
api.gateway.endpoints[1].method=POST
api.gateway.endpoints[1].location=http://${accounts.commandside.service.host}:8080
api.gateway.endpoints[2].path=[/]*customers.*
api.gateway.endpoints[2].method=GET
api.gateway.endpoints[2].location=http://${customers.queryside.service.host}:8080
api.gateway.endpoints[3].path=[/]*customers.*
api.gateway.endpoints[3].method=POST
api.gateway.endpoints[3].location=http://${customers.commandside.service.host}:8080
api.gateway.endpoints[4].path=[/]*transfers.*
api.gateway.endpoints[4].method=POST
api.gateway.endpoints[4].location=http://${customers.commandside.service.host}:8080
api.gateway.endpoints[5].path=[/]*api/transfers.*
api.gateway.endpoints[5].method=POST
api.gateway.endpoints[5].location=http://${transfers.commandside.service.host}:8080
api.gateway.endpoints[6].path=[/]*api/customers.*
api.gateway.endpoints[6].method=DELETE
api.gateway.endpoints[6].location=http://${customers.commandside.service.host}:8080
api.gateway.endpoints[7].path=[/]*api/accounts.*
api.gateway.endpoints[7].method=DELETE
api.gateway.endpoints[7].location=http://${accounts.commandside.service.host}:8080
api.gateway.endpoints[4].location=http://${transfers.commandside.service.host}:8080

View File

@@ -18,7 +18,7 @@
<logger name="net.chrisrichardson.eventstore.javaexamples.banking" level='info'>
</logger>
<logger name="io.eventuate.activity" level='debug'>
<logger name="io.eventuate" level='debug'>
</logger>
</configuration>

View File

@@ -2,11 +2,11 @@ apply plugin: VerifyMongoDBConfigurationPlugin
dependencies {
testCompile project(":transactions-service")
testCompile project(":accounts-service")
testCompile project(":accounts-view-service")
testCompile project(":customers-service")
testCompile project(":customers-view-service")
testCompile project(":accounts-command-side-backend")
testCompile project(":transactions-command-side-backend")
testCompile project(":accounts-query-side-backend")
testCompile project(":customers-command-side-backend")
testCompile project(":customers-query-side-backend")
testCompile project(":testutil")
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"

View File

@@ -1,14 +1,14 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferBackendConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@Configuration
@Import({AccountsBackendConfiguration.class, MoneyTransferBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class})
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
@EnableAutoConfiguration
public class BankingTestConfiguration {

View File

@@ -2,12 +2,12 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend;
import io.eventuate.EntityWithIdAndVersion;
import io.eventuate.EventuateAggregateStore;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@@ -2,13 +2,12 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.ac
import io.eventuate.EntityWithIdAndVersion;
import io.eventuate.EventuateAggregateStore;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -18,7 +17,6 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.concurrent.CompletableFuture;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
@@ -57,10 +55,10 @@ public class AccountQuerySideIntegrationTest {
updatedTransaction -> Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.getEntity().getState()));
eventually(
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(fromAccount.getEntityId())),
() -> accountQueryService.findByAccountId(fromAccount.getEntityId()),
accountInfo -> Assert.assertEquals(70 * 100, accountInfo.getBalance()));
eventually(
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(toAccount.getEntityId())),
() -> accountQueryService.findByAccountId(toAccount.getEntityId()),
accountInfo -> Assert.assertEquals(380 * 100, accountInfo.getBalance()));
}
}

View File

@@ -1,16 +1,15 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferBackendConfiguration;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountsBackendConfiguration.class, MoneyTransferBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class,
AccountViewBackendConfiguration.class})
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, EventuateJdbcEventStoreConfiguration.class,
QuerySideAccountConfiguration.class})
@EnableAutoConfiguration
public class AccountQuerySideTestConfiguration {
}

View File

@@ -2,11 +2,10 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.cu
import io.eventuate.EntityWithIdAndVersion;
import io.eventuate.EventuateAggregateStore;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.customers.Customer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.customers.CustomerService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.Customer;
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.CustomerService;
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.backend.CustomerQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.ToAccountInfo;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
@@ -62,7 +61,7 @@ public class CustomerQuerySideIntegrationTest {
public void verify(QuerySideCustomer querySideCustomer) {
Assert.assertEquals(customerInfo.getName(), querySideCustomer.getName());
Assert.assertEquals(customerInfo.getSsn(), querySideCustomer.getSsn());
Assert.assertEquals(customerInfo.getUserCredentials().getEmail(), querySideCustomer.getEmail());
Assert.assertEquals(customerInfo.getEmail(), querySideCustomer.getEmail());
Assert.assertEquals(customerInfo.getPhoneNumber(), querySideCustomer.getPhoneNumber());
Assert.assertEquals(customerInfo.getAddress(), querySideCustomer.getAddress());

View File

@@ -1,14 +1,14 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.customers;
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.CustomerBackendConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.backend.CustomerViewBackendConfiguration;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.customers.CustomerConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({CustomerBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class, CustomerViewBackendConfiguration.class})
@Import({CustomerConfiguration.class, EventuateJdbcEventStoreConfiguration.class, QuerySideCustomerConfiguration.class})
@EnableAutoConfiguration
public class CustomerQuerySideTestConfiguration {
}

View File

@@ -1,6 +0,0 @@
#! /bin/bash
export JAVA_OPTS="-Xmx128m -Xms128m"
export EXTRA_INFRASTRUCTURE_SERVICES=cdcservice
export EVENTUATE_LOCAL=yes
../_build-and-test-all.sh -f docker-compose-eventuate-local.yml $* -P eventuateDriver=local

View File

@@ -1,4 +1,3 @@
#! /bin/bash
export JAVA_OPTS="-Xmx128m -Xms128m"
../_build-and-test-all.sh $*

View File

@@ -23,7 +23,6 @@ subprojects {
repositories {
mavenCentral()
jcenter()
eventuateMavenRepoUrl.split(',').each { repoUrl -> maven { url repoUrl } }
}
}

View File

@@ -1,16 +0,0 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
class EventuateDependencyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.dependencies {
if (project.hasProperty("eventuateDriver") && project.property("eventuateDriver").equals("local")) {
compile "io.eventuate.local.java:eventuate-local-java-jdbc:${project.eventuateLocalVersion}"
compile "io.eventuate.local.java:eventuate-local-java-embedded-cdc-autoconfigure:${project.eventuateLocalVersion}"
} else
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:${project.eventuateClientVersion}"
}
}
}

View File

@@ -8,5 +8,4 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
}

View File

@@ -1,13 +1,12 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.UserCredentials;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.CustomerAuthService;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.AuthRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.ErrorResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.User;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -24,9 +23,11 @@ import java.io.IOException;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* Created by popikyardo on 21.09.15.
*/
@RestController
@Validated
@RequestMapping("/api")
public class AuthController {
@Autowired
@@ -38,8 +39,8 @@ public class AuthController {
private static ObjectMapper objectMapper = new ObjectMapper();
@RequestMapping(value = "/login", method = POST)
public ResponseEntity<QuerySideCustomer> doAuth(@RequestBody @Valid UserCredentials request) throws IOException {
QuerySideCustomer customer = customerAuthService.findByEmailAndPassword(request.getEmail(), request.getPassword());
public ResponseEntity<QuerySideCustomer> doAuth(@RequestBody @Valid AuthRequest request) throws IOException {
QuerySideCustomer customer = customerAuthService.findByEmail(request.getEmail());
Token token = tokenService.allocateToken(objectMapper.writeValueAsString(new User(request.getEmail())));
return ResponseEntity.status(HttpStatus.OK).header("access-token", token.getKey())
@@ -47,7 +48,7 @@ public class AuthController {
}
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler({EmptyResultDataAccessException.class, IncorrectResultSizeDataAccessException.class})
@ExceptionHandler(IncorrectResultSizeDataAccessException.class)
public ErrorResponse customersNotFound() {
return new ErrorResponse("Customer not found");
}

View File

@@ -3,21 +3,20 @@ package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
/**
* Created by popikyardo on 19.10.15.
*/
public class AuthRequest {
@NotBlank
@Email
private String email;
@NotBlank
private String password;
public AuthRequest() {
}
public AuthRequest(String email, String password) {
public AuthRequest(String email) {
this.email = email;
this.password = password;
}
public String getEmail() {
@@ -27,12 +26,4 @@ public class AuthRequest {
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -1,80 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Address;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Name;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.CustomerAuthService;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.User;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.MediaType;
import org.springframework.security.core.token.DefaultToken;
import org.springframework.security.core.token.TokenService;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AuthControllerIntegrationTestConfiguration.class)
@IntegrationTest
@WebAppConfiguration
public class AuthControllerIntegrationTest {
private MockMvc mockMvc;
@Mock
TokenService tokenService;
@Mock
CustomerAuthService customerAuthService;
@InjectMocks
AuthController authController;
private static ObjectMapper om = new ObjectMapper();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(authController).build();
}
@Test
public void shouldLogin() throws Exception {
when(customerAuthService.findByEmailAndPassword("my@email.com", "my_password")).thenReturn(new QuerySideCustomer("id", new Name("test", "test"), "my@email.com", "my_password", "ssn", "", new Address(), null));
when(customerAuthService.findByEmailAndPassword("not_my@email.com", "not_my_password")).thenThrow(new EmptyResultDataAccessException(1));
when(tokenService.allocateToken(om.writeValueAsString(new User("my@email.com")))).thenReturn(new DefaultToken("key", System.currentTimeMillis(), ""));
mockMvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\" : \"my@email.com\", \"password\" : \"my_password\"}")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(result -> {
assertTrue(result.getResponse().getContentAsString().contains("id"));
assertTrue(result.getResponse().getContentAsString().contains("my@email.com"));
assertTrue(result.getResponse().getContentAsString().contains("my_password"));
});
mockMvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\" : \"not_my@email.com\", \"password\" : \"not_my_password\"}")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
}

View File

@@ -1,12 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(AuthConfiguration.class)
@EnableAutoConfiguration
public class AuthControllerIntegrationTestConfiguration {
}

View File

@@ -2,7 +2,6 @@ apply plugin: 'java'
dependencies {
compile project(":common")
compile project(":customers-query-side-common")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"

View File

@@ -1,6 +1,5 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.filter.StatelessAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -23,6 +22,9 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
import java.security.SecureRandom;
/**
* Created by popikyardo on 21.09.15.
*/
@Configuration
@ComponentScan
@EnableWebSecurity
@@ -41,14 +43,21 @@ public class AuthConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth.inMemoryAuthentication();
auth.userDetailsService(userDetailsServiceBean());
}
@Override
public UserDetailsService userDetailsServiceBean() {
return email -> {
QuerySideCustomer customer = customerAuthService.findByEmail(email);
return new User(email, customer.getPassword(), true, true, true, true,
/* QuerySideCustomer customer = customerAuthService.findByEmail(email);
if (customer != null) {
return new User(email);
} else {
throw new UsernameNotFoundException(String.format("could not find the customer '%s'", email));
}*/
//authorize everyone with basic authentication
return new User(email, "", true, true, true, true,
AuthorityUtils.createAuthorityList("USER"));
};
}
@@ -66,16 +75,13 @@ public class AuthConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.httpBasic()
.and()
http.csrf().disable()
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/customers", "/api/login").permitAll()
.antMatchers("/api/**").permitAll()
.anyRequest().permitAll()
.and()
.antMatchers("/index.html", "/", "/**.js", "/**.css").permitAll()
.antMatchers("/swagger-ui.html", "/v2/api-docs").permitAll()
.antMatchers(HttpMethod.POST, "/customers", "/login").permitAll()
.anyRequest().authenticated().and()
.addFilterAfter(new StatelessAuthenticationFilter(tokenAuthenticationService), BasicAuthenticationFilter.class);
}

View File

@@ -2,6 +2,9 @@ package net.chrisrichardson.eventstore.javaexamples.banking.commonauth;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Created by popikyardo on 21.09.15.
*/
@ConfigurationProperties(locations = "classpath:auth.properties", ignoreUnknownFields = false, prefix = "auth")
public class AuthProperties {
private String serverSecret;

View File

@@ -1,6 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
@@ -8,6 +8,4 @@ import java.util.List;
interface CustomerAuthRepository extends MongoRepository<QuerySideCustomer, String> {
List<QuerySideCustomer> findByEmail(String email);
List<QuerySideCustomer> findByEmailAndPassword(String email, String password);
}

View File

@@ -1,8 +1,9 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import java.util.List;
/**
* Created by Main on 15.02.2016.
@@ -15,18 +16,13 @@ public class CustomerAuthService {
}
public QuerySideCustomer findByEmail(String email) {
QuerySideCustomer result = DataAccessUtils.uniqueResult(customerAuthRepository.findByEmail(email));
if (result==null)
List<QuerySideCustomer> customers = customerAuthRepository.findByEmail(email);
if (customers.isEmpty())
throw new EmptyResultDataAccessException(1);
return result;
}
public QuerySideCustomer findByEmailAndPassword(String email, String password) {
QuerySideCustomer result = DataAccessUtils.uniqueResult(customerAuthRepository.findByEmailAndPassword(email, password));
if (result==null)
throw new EmptyResultDataAccessException(1);
return result;
//TODO: add unique email constraint
/* else if(customers.size()>1)
throw new IncorrectResultSizeDataAccessException(1, customers.size());*/
else
return customers.get(0);
}
}

View File

@@ -12,6 +12,9 @@ import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Created by popikyardo on 23.09.15.
*/
@Service
public class TokenAuthenticationService {

View File

@@ -11,6 +11,9 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Created by popikyardo on 23.09.15.
*/
public class StatelessAuthenticationFilter extends GenericFilterBean {
private final TokenAuthenticationService tokenAuthenticationService;

View File

@@ -10,6 +10,9 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* Created by popikyardo on 23.09.15.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class User implements UserDetails {

View File

@@ -5,6 +5,9 @@ import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* Created by popikyardo on 23.09.15.
*/
public class UserAuthentication implements Authentication {
private final User user;

View File

@@ -1,6 +1,5 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.utils;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.UserCredentials;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.http.*;
import org.springframework.util.Assert;
@@ -8,17 +7,15 @@ import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
import static org.junit.Assert.assertEquals;
/**
* Created by Main on 18.02.2016.
*/
public class BasicAuthUtils {
public static HttpHeaders basicAuthHeaders(UserCredentials userCredentials) {
public static HttpHeaders basicAuthHeaders(String username) {
return new HttpHeaders() {
{
String auth = userCredentials.getEmail() + ":" + userCredentials.getPassword();
String auth = username + ":";
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
@@ -27,24 +24,23 @@ public class BasicAuthUtils {
};
}
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, UserCredentials userCredentials) {
return doBasicAuthenticatedRequest(restTemplate, url, httpMethod, responseType, null, userCredentials);
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType) {
return doBasicAuthenticatedRequest(restTemplate, url, httpMethod, responseType, null);
}
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, Object requestObject, UserCredentials userCredentials) {
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, Object requestObject) {
HttpEntity httpEntity;
if (requestObject != null) {
httpEntity = new HttpEntity<>(requestObject, BasicAuthUtils.basicAuthHeaders(userCredentials));
httpEntity = new HttpEntity<>(requestObject, BasicAuthUtils.basicAuthHeaders("test_user@mail.com"));
} else {
httpEntity = new HttpEntity(BasicAuthUtils.basicAuthHeaders(userCredentials));
httpEntity = new HttpEntity(BasicAuthUtils.basicAuthHeaders("test_user@mail.com"));
}
ResponseEntity<T> responseEntity = restTemplate.exchange(url,
httpMethod,
httpEntity,
responseType);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
Assert.isTrue(HttpStatus.OK == responseEntity.getStatusCode(), "Bad response: " + responseEntity.getStatusCode());
return responseEntity.getBody();
}
}

View File

@@ -1,8 +1,10 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts;
import io.eventuate.Event;
import java.math.BigDecimal;
public class AccountChangedEvent extends AccountEvent {
public class AccountChangedEvent implements Event {
protected BigDecimal amount;
protected String transactionId;

View File

@@ -1,6 +1,8 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts;
public class AccountDebitFailedDueToInsufficientFundsEvent extends AccountEvent {
import io.eventuate.Event;
public class AccountDebitFailedDueToInsufficientFundsEvent implements Event {
private String transactionId;
private AccountDebitFailedDueToInsufficientFundsEvent() {

View File

@@ -1,4 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts;
public class AccountDeletedEvent extends AccountEvent {
}

View File

@@ -1,8 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts;
import io.eventuate.Event;
import io.eventuate.EventEntity;
@EventEntity(entity="net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.Account")
public abstract class AccountEvent implements Event{
}

Some files were not shown because too many files have changed in this diff Show More