Load simulation module

This commit is contained in:
Kenny Bastani
2017-02-19 18:09:47 -05:00
parent 92d70f62c7
commit 54cbb71733
62 changed files with 55043 additions and 91 deletions

View File

@@ -2,20 +2,22 @@ package demo.account.controller;
import demo.account.domain.Account;
import demo.account.domain.AccountService;
import demo.account.domain.Accounts;
import demo.account.event.AccountEvent;
import demo.event.EventService;
import demo.event.Events;
import demo.order.domain.Order;
import demo.order.domain.Orders;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Optional;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@@ -32,6 +34,11 @@ public class AccountController {
this.eventService = eventService;
}
@RequestMapping(path = "/accounts")
public ResponseEntity getAccounts(@RequestBody(required = false) PageRequest pageRequest) {
return new ResponseEntity<>(getAccountsResource(pageRequest), HttpStatus.OK);
}
@PostMapping(path = "/accounts")
public ResponseEntity createAccount(@RequestBody Account account) {
return Optional.ofNullable(createAccountResource(account))
@@ -89,6 +96,13 @@ public class AccountController {
.orElseThrow(() -> new RuntimeException("Could not get account events"));
}
@RequestMapping(path = "/accounts/{id}/orders/{orderId}")
public ResponseEntity getAccountOrders(@PathVariable Long id, @PathVariable Long orderId) {
return Optional.of(getAccountOrderResource(id, orderId))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("Could not get account events"));
}
@RequestMapping(path = "/accounts/{id}/commands")
public ResponseEntity getCommands(@PathVariable Long id) {
return Optional.ofNullable(getCommandsResource(id))
@@ -136,7 +150,7 @@ public class AccountController {
public ResponseEntity postOrder(@PathVariable Long id, @RequestBody Order order) {
return Optional.ofNullable(accountService.get(id))
.map(a -> a.postOrder(order))
.map(o -> new ResponseEntity<>(o, HttpStatus.CREATED))
.map(o -> new ResponseEntity<>(getAccountOrderResource(id, o.getIdentity()), HttpStatus.CREATED))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@@ -170,6 +184,50 @@ public class AccountController {
return getAccountResource(accountService.update(account));
}
private Accounts getAccountsResource(PageRequest pageRequest) {
if (pageRequest == null) {
pageRequest = new PageRequest(1, 20);
}
Page<Account> accountPage = accountService.findAll(pageRequest.first());
Accounts accounts = new Accounts(accountPage);
accounts.add(new Link(new UriTemplate(linkTo(AccountController.class).slash("accounts").toUri().toString())
.with("page", TemplateVariable.VariableType.REQUEST_PARAM)
.with("size", TemplateVariable.VariableType.REQUEST_PARAM), "self"));
return accounts;
}
private Order getAccountOrderResource(Long accountId, Long orderId) {
Account account = accountService.get(accountId);
Assert.notNull(account, "Account could not be found");
Order order = account.getOrders().getContent().stream().filter(o -> Objects
.equals(o.getIdentity(), orderId))
.findFirst()
.orElseGet(null);
Assert.notNull(order, "The order for the account could not be found");
order.removeLinks();
order.add(
linkTo(AccountController.class)
.slash("accounts")
.slash(accountId)
.slash("orders")
.slash(orderId)
.withSelfRel(),
linkTo(AccountController.class)
.slash("accounts")
.slash(accountId)
.withRel("account")
);
return order;
}
private Orders getAccountOrdersResource(Long accountId) {
Account account = accountService.get(accountId);
Assert.notNull(account, "Account could not be found");

View File

@@ -5,6 +5,8 @@ import demo.account.event.AccountEventType;
import demo.account.repository.AccountRepository;
import demo.domain.Service;
import org.apache.log4j.Logger;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.util.Assert;
@org.springframework.stereotype.Service
@@ -17,6 +19,10 @@ public class AccountService extends Service<Account, Long> {
this.accountRepository = accountRepository;
}
public Page<Account> findAll(Pageable pageable) {
return accountRepository.findAll(pageable);
}
/**
* Register a new {@link Account} and handle a synchronous event flow for account creation
*

View File

@@ -0,0 +1,63 @@
package demo.account.domain;
import org.springframework.data.domain.Page;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
import java.io.Serializable;
public class Accounts extends Resources<Account> {
private PageModel page;
public Accounts(Page<Account> accountPage) {
super(accountPage.getContent());
page = new PageModel(accountPage);
}
public Accounts(Iterable<Account> content, Link... links) {
super(content, links);
}
public Accounts(Iterable<Account> content, Iterable<Link> links) {
super(content, links);
}
public PageModel getPage() {
return page;
}
class PageModel implements Serializable {
private int number;
private int size;
private int totalPages;
private long totalElements;
public PageModel() {
}
public PageModel(Page page) {
number = page.getNumber();
size = page.getSize();
totalPages = page.getTotalPages();
totalElements = page.getTotalElements();
}
public int getNumber() {
return number;
}
public int getSize() {
return size;
}
public int getTotalPages() {
return totalPages;
}
public long getTotalElements() {
return totalElements;
}
}
}

View File

@@ -1,6 +1,7 @@
package demo.order.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import demo.domain.Aggregate;
import demo.domain.Module;
import demo.order.event.OrderEvent;
@@ -105,6 +106,7 @@ public class Order extends Aggregate<OrderEvent, Long> {
return orderEvents;
}
@JsonProperty("orderId")
@Override
public Long getIdentity() {
return id;

View File

@@ -72,6 +72,12 @@ discovery:
image: discovery
ports:
- 8761:8761
environment:
- SPRING_PROFILES_ACTIVE=docker
- DOCKER_IP=$DOCKER_IP
net: host
dashboard:
image: dashboard
environment:
- SPRING_PROFILES_ACTIVE=docker
- DOCKER_IP=$DOCKER_IP

View File

@@ -3,8 +3,10 @@ package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
@@ -13,4 +15,9 @@ public class OrderWorker {
public static void main(String[] args) {
SpringApplication.run(OrderWorker.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -6,6 +6,8 @@ import demo.order.event.OrderEvent;
import demo.order.event.OrderEventType;
import demo.order.event.OrderEvents;
import org.apache.log4j.Logger;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.client.Traverson;
@@ -14,31 +16,57 @@ import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
@Service
public class StateFactory {
final private Logger log = Logger.getLogger(StateFactory.class);
final private StateService stateService;
final private RestTemplate restTemplate;
final private DiscoveryClient discoveryClient;
public StateFactory(StateService stateService) {
public StateFactory(StateService stateService, RestTemplate restTemplate, DiscoveryClient discoveryClient) {
this.stateService = stateService;
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
}
public Order apply(OrderEvent orderEvent) {
Assert.notNull(orderEvent, "Cannot apply a null event");
Assert.notNull(orderEvent.getId(), "The event payload's identity link was not found");
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("order-web");
Assert.notEmpty(serviceInstances, "No instances available for order-web");
Integer instanceNum = new Random().nextInt(serviceInstances.size());
ServiceInstance orderService = serviceInstances.get(instanceNum);
URI orderHref = getLoadBalanceUri(orderService, URI.create(orderEvent.getLink("order").getHref()));
URI selfHref = getLoadBalanceUri(orderService, URI.create(orderEvent.getLink("self").getHref()));
orderEvent.getLinks()
.replaceAll(a -> Objects.equals(a.getRel(), "order") ? new Link(orderHref
.toString(), "order") : a);
orderEvent.getLinks()
.replaceAll(a -> Objects.equals(a.getRel(), "self") ? new Link(selfHref
.toString(), "self") : a);
StateMachine<OrderStatus, OrderEventType> stateMachine = getStateMachine(orderEvent);
stateMachine.stop();
return stateMachine.getExtendedState().get("order", Order.class);
}
private URI getLoadBalanceUri(ServiceInstance serviceInstance, URI uri) {
return URI.create(uri.toString()
.replace(uri.getHost(), serviceInstance.getHost())
.replace(":" + uri.getPort(), ":" + String.valueOf(serviceInstance.getPort())));
}
private StateMachine<OrderStatus, OrderEventType> getStateMachine(OrderEvent orderEvent) {
Link eventId = orderEvent.getId();
log.info(String.format("Order event received: %s", eventId));
@@ -67,13 +95,18 @@ public class StateFactory {
}
private OrderEvents getEventLog(OrderEvent event) {
// Replace the host and port of the link with an available instance
URI orderHref = URI.create(event.getLink("order")
.getHref());
// Follow the hypermedia link to fetch the attached order
Traverson traverson = new Traverson(
URI.create(event.getLink("order")
.getHref()),
Traverson traverson = new Traverson(orderHref,
MediaTypes.HAL_JSON
);
traverson.setRestOperations(restTemplate);
// Get the event log for the attached order resource
return traverson.follow("events")
.toEntity(OrderEvents.class)

View File

@@ -1,6 +1,8 @@
package demo.order.event;
import com.fasterxml.jackson.annotation.JsonIgnore;
import demo.domain.AbstractEntity;
import org.springframework.hateoas.Link;
public class OrderEvent extends AbstractEntity {
@@ -21,6 +23,12 @@ public class OrderEvent extends AbstractEntity {
this.type = type;
}
@JsonIgnore
@Override
public Link getId() {
return super.getId();
}
@Override
public String toString() {
return "OrderEvent{" +

View File

@@ -39,7 +39,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dataflow-dependencies</artifactId>
<version>1.1.0.RELEASE</version>
<version>1.1.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

View File

@@ -33,19 +33,51 @@ public class ImportResources implements ApplicationListener<ApplicationReadyEven
// Deploy a set of event stream definitions
List<StreamApp> streams = Arrays.asList(
new StreamApp("account-stream",
"account-web: account-web | account-worker: account-worker"),
new StreamApp("order-stream",
"order-web: order-web | order-worker: order-worker"),
new StreamApp("payment-stream",
"payment-web: payment-web | payment-worker: payment-worker"),
new StreamApp("warehouse-stream",
"warehouse-web: warehouse-web | warehouse-worker: warehouse-worker"));
new StreamApp("account-event-stream",
"account-web > :account-stream"),
new StreamApp("account-event-processor",
":account-stream > account-worker"),
new StreamApp("account-event-counter",
":account-stream > field-value-counter --field-name=type --name=account-events"),
new StreamApp("order-event-stream",
"order-web > :order-stream"),
new StreamApp("order-event-processor",
":order-stream > order-worker"),
new StreamApp("order-event-counter",
":order-stream > field-value-counter --field-name=type --name=order-events"),
new StreamApp("payment-event-stream",
"payment-web > :payment-stream"),
new StreamApp("payment-event-processor",
":payment-stream > payment-worker"),
new StreamApp("payment-event-counter",
":payment-stream > field-value-counter --field-name=type --name=payment-events"),
new StreamApp("warehouse-event-stream",
"warehouse-web > :warehouse-stream"),
new StreamApp("warehouse-event-processor",
":warehouse-stream > warehouse-worker"),
new StreamApp("warehouse-event-counter",
":warehouse-stream > field-value-counter --field-name=type --name=warehouse-events"),
new StreamApp("account-load-simulator", "time --time-unit=SECONDS --initial-delay=60 " +
"--fixed-delay=30 | " +
"load-simulator --domain=ACCOUNT --operation=CREATE > :load-log"),
new StreamApp("inventory-load-simulator", "time --time-unit=SECONDS --initial-delay=60 " +
"--fixed-delay=1 | " +
"load-simulator --domain=INVENTORY --operation=CREATE --range=10 > :load-log"),
new StreamApp("order-load-simulator", "time --time-unit=SECONDS --initial-delay=80 " +
"--fixed-delay=5 | " +
"load-simulator --command=POST_ORDER --domain=ACCOUNT --operation=CREATE --range=5 > :load-log"),
new StreamApp("account-counter",
":account-stream > counter --name-expression=payload.type.toString()"),
new StreamApp("order-counter",
":order-stream > counter --name-expression=payload.type.toString()"),
new StreamApp("warehouse-counter",
":warehouse-stream > counter --name-expression=payload.type.toString()"));
// Deploy the streams in parallel
streams.parallelStream()
.forEach(stream -> dataFlowTemplate.streamOperations()
.createStream(stream.getName(), stream.getDefinition(), true));
} catch (MalformedURLException e) {
e.printStackTrace();
}

View File

@@ -5,4 +5,5 @@ sink.order-worker=maven://org.kbastani:order-worker:0.0.1-SNAPSHOT
source.payment-web=maven://org.kbastani:payment-web:0.0.1-SNAPSHOT
sink.payment-worker=maven://org.kbastani:payment-worker:0.0.1-SNAPSHOT
source.warehouse-web=maven://org.kbastani:warehouse-web:0.0.1-SNAPSHOT
sink.warehouse-worker=maven://org.kbastani:warehouse-worker:0.0.1-SNAPSHOT
sink.warehouse-worker=maven://org.kbastani:warehouse-worker:0.0.1-SNAPSHOT
processor.load-simulator=maven://org.kbastani:load-simulator:1.0-SNAPSHOT

View File

@@ -26,5 +26,6 @@
<modules>
<module>discovery</module>
<module>data-flow-server</module>
<module>stream-modules</module>
</modules>
</project>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>load-simulator</artifactId>
<packaging>jar</packaging>
<name>load-simulator</name>
<description>Simulates load on event-driven microservices</description>
<parent>
<groupId>org.kbastani</groupId>
<artifactId>stream-modules</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>0.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,5 @@
package demo;
public enum Command {
POST_ORDER
}

View File

@@ -0,0 +1,8 @@
package demo;
public enum Domain {
ACCOUNT,
ORDER,
WAREHOUSE,
INVENTORY
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import com.github.javafaker.Faker;
import demo.account.domain.Account;
import demo.account.domain.AccountStatus;
import demo.account.service.AccountService;
import demo.domain.Address;
import demo.domain.AddressType;
import demo.inventory.domain.Inventory;
import demo.inventory.domain.InventoryStatus;
import demo.order.domain.LineItem;
import demo.order.domain.Order;
import demo.warehouse.domain.Warehouse;
import demo.warehouse.service.WarehouseService;
import org.apache.log4j.Logger;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
@MessageEndpoint
public class LoadProcessor {
private static final Logger log = Logger.getLogger(LoadProcessor.class);
private final LoadSimulatorProperties properties;
private final AccountService accountService;
private final WarehouseService warehouseService;
public LoadProcessor(LoadSimulatorProperties properties, AccountService accountService,
WarehouseService warehouseService) {
this.properties = properties;
this.accountService = accountService;
this.warehouseService = warehouseService;
}
@ServiceActivator(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
public Object process(Message<?> message) {
return handleRequest(message);
}
private String handleRequest(Message<?> message) {
StringBuffer sb = new StringBuffer();
switch (properties.getDomain()) {
case ACCOUNT:
accountOperation(properties.getOperation(), sb);
break;
case ORDER:
break;
case WAREHOUSE:
break;
case INVENTORY:
inventoryOperation(properties.getOperation(), sb);
break;
}
return sb.toString();
}
private void inventoryOperation(Operation operation, StringBuffer sb) {
Faker faker = new Faker();
switch (operation) {
case CREATE:
Warehouse warehouse = null;
try {
warehouse = warehouseService.get(1L);
} catch (Exception ex) {
log.error(ex);
}
if (warehouse == null) {
// Create the first warehouse
warehouse = warehouseService.create(new Warehouse(new Address(faker.address().streetAddress(),
faker.address().buildingNumber(),
faker.address().state(),
faker.address().city(),
faker.address().country(),
Integer.parseInt(faker.address().zipCode()))));
sb.append("[Warehouse created]\n");
}
List<Inventory> inventory = new ArrayList<>();
LongStream.range(0, properties.getRange())
.forEach(a -> inventory.add(new Inventory(InventoryStatus.INVENTORY_CREATED, "SKU-" + a)));
List<Inventory> results = warehouseService.addInventory(inventory, 1L);
sb.append(String.format("[%s inventory added to warehouse]\n", results.size()));
break;
}
}
private void accountOperation(Operation operation, StringBuffer sb) {
Faker faker = new Faker();
switch (operation) {
case CREATE:
if (properties.getCommand() == Command.POST_ORDER) {
// Post new order to the accounts
LongStream.range(1, properties.getRange() + 1)
.forEach(a -> {
try {
Order order = new Order(a, new Address(faker.address().streetAddress(),
faker.address().buildingNumber(),
faker.address().state(),
faker.address().city(),
faker.address().country(),
Integer.parseInt(faker.address().zipCode().substring(0, 4)),
AddressType.SHIPPING));
IntStream.range(0, new Random().nextInt(5))
.forEach(i -> order.getLineItems()
.add(new LineItem(faker.commerce().productName(),
"SKU-" + i, new Random().nextInt(3), Double
.parseDouble(faker.commerce().price(.99, 50.0)), .06)));
Order result = accountService.postOrder(a, order);
sb.append(String.format("[New order posted to account %s]\n", a));
} catch (Exception ex) {
log.error(ex);
}
});
} else {
LongStream.range(0, properties.getRange())
.forEach(a -> accountService
.create(new Account(faker.name().firstName(), faker.name().lastName(), faker
.internet()
.emailAddress(), AccountStatus.ACCOUNT_CREATED)));
sb.append(String.format("[%s new accounts created]\n", properties.getRange()));
}
break;
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class LoadSimulatorApplication {
public static void main(String[] args) {
SpringApplication.run(LoadSimulatorApplication.class, args);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.config.SpelExpressionConverterConfiguration;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.client.RestTemplate;
@Configuration
@EnableBinding(Processor.class)
@Import(SpelExpressionConverterConfiguration.class)
@EnableConfigurationProperties(LoadSimulatorProperties.class)
public class LoadSimulatorConfiguration {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties
public class LoadSimulatorProperties {
private Domain domain = Domain.ACCOUNT;
private Operation operation = Operation.CREATE;
private Command command;
private Long range = 1L;
public Domain getDomain() {
return domain;
}
public void setDomain(Domain domain) {
this.domain = domain;
}
public Operation getOperation() {
return operation;
}
public void setOperation(Operation operation) {
this.operation = operation;
}
public Command getCommand() {
return command;
}
public void setCommand(Command command) {
this.command = command;
}
public Long getRange() {
return range;
}
public void setRange(Long range) {
this.range = range;
}
}

View File

@@ -0,0 +1,5 @@
package demo;
public enum Operation {
CREATE
}

View File

@@ -0,0 +1,85 @@
package demo.account.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import java.util.ArrayList;
import java.util.List;
public class Account {
private Long id;
private List<AccountEvent> accountEvents = new ArrayList<>();
private String firstName;
private String lastName;
private String email;
private AccountStatus status;
public Account() {
}
public Account(String firstName, String lastName, String email, AccountStatus status) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.status = status;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public AccountStatus getStatus() {
return status;
}
public void setStatus(AccountStatus status) {
this.status = status;
}
@JsonIgnore
public List<AccountEvent> getEvents() {
return accountEvents;
}
@JsonProperty("accountId")
public Long getIdentity() {
return id;
}
public void setIdentity(Long id) {
this.id = id;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/
public Link getId() {
return new Link(new UriTemplate("http://account-web/v1/accounts/{id}").with("id", TemplateVariable.VariableType
.PATH_VARIABLE)
.expand(getIdentity())
.toString()).withSelfRel();
}
}

View File

@@ -0,0 +1,38 @@
package demo.account.domain;
/**
* The domain event {@link AccountEvent} tracks the type and state of events as
* applied to the {@link Account} domain object. This event resource can be used
* to event source the aggregate state of {@link Account}.
* <p>
* This event resource also provides a transaction log that can be used to append
* actions to the event.
*
* @author kbastani
*/
public class AccountEvent {
private AccountEventType type;
public AccountEvent() {
}
public AccountEvent(AccountEventType type) {
this.type = type;
}
public AccountEventType getType() {
return type;
}
public void setType(AccountEventType type) {
this.type = type;
}
@Override
public String toString() {
return "AccountEvent{" +
"type=" + type +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,15 @@
package demo.account.domain;
/**
* The {@link AccountEventType} represents a collection of possible events that describe
* state transitions of {@link AccountStatus} on the {@link Account} aggregate.
*
* @author kbastani
*/
public enum AccountEventType {
ACCOUNT_CREATED,
ACCOUNT_CONFIRMED,
ACCOUNT_ACTIVATED,
ACCOUNT_SUSPENDED,
ACCOUNT_ARCHIVED
}

View File

@@ -0,0 +1,17 @@
package demo.account.domain;
/**
* The {@link AccountStatus} describes the state of an {@link Account}.
* The aggregate state of a {@link Account} is sourced from attached domain
* events in the form of {@link demo.event.AccountEvent}.
*
* @author kbastani
*/
public enum AccountStatus {
ACCOUNT_CREATED,
ACCOUNT_PENDING,
ACCOUNT_CONFIRMED,
ACCOUNT_ACTIVE,
ACCOUNT_SUSPENDED,
ACCOUNT_ARCHIVED
}

View File

@@ -0,0 +1,109 @@
package demo.account.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import demo.account.domain.Account;
import demo.order.domain.Order;
import org.apache.log4j.Logger;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class AccountService {
private final Logger log = Logger.getLogger(this.getClass());
private final RestTemplate restTemplate;
public AccountService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Account get(Long accountId) {
Account result;
try {
result = restTemplate.getForObject(new UriTemplate("http://account-web/v1/accounts/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(accountId), Account.class);
} catch (RestClientResponseException ex) {
log.error("Get account failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Account create(Account account) {
Account result;
try {
result = restTemplate.postForObject(new UriTemplate("http://account-web/v1/accounts").expand(),
account, Account.class);
} catch (RestClientResponseException ex) {
log.error("Create account failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Account update(Account account) {
Account result;
try {
result = restTemplate.exchange(new RequestEntity<>(account, HttpMethod.PUT,
new UriTemplate("http://account-web/v1/accounts/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(account.getIdentity())), Account.class).getBody();
} catch (RestClientResponseException ex) {
log.error("Update account failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public boolean delete(Long accountId) {
try {
restTemplate.delete(new UriTemplate("http://account-web/v1/accounts/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE).expand(accountId));
} catch (RestClientResponseException ex) {
log.error("Delete account failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return true;
}
public Order postOrder(Long accountId, Order order) {
Order result;
try {
result = restTemplate.postForObject(new UriTemplate(String
.format("http://account-web/v1/accounts/%s/commands/postOrder", accountId)).expand(),
order, Order.class);
} catch (RestClientResponseException ex) {
log.error("Post order to account has failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
private String getHttpStatusMessage(RestClientResponseException ex) {
Map<String, String> errorMap = new HashMap<>();
try {
errorMap = new ObjectMapper()
.readValue(ex.getResponseBodyAsString(), errorMap
.getClass());
} catch (IOException e) {
e.printStackTrace();
}
return errorMap.getOrDefault("message", null);
}
}

View File

@@ -0,0 +1,36 @@
package demo.domain;
import org.springframework.hateoas.ResourceSupport;
public class AbstractEntity extends ResourceSupport {
private Long createdAt;
private Long lastModified;
public AbstractEntity() {
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getLastModified() {
return lastModified;
}
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
@Override
public String toString() {
return "BaseEntity{" +
"createdAt=" + createdAt +
", lastModified=" + lastModified +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,101 @@
package demo.domain;
import java.io.Serializable;
public class Address implements Serializable {
private String street1, street2, state, city, country;
private Integer zipCode;
private AddressType addressType;
public Address() {
}
public Address(String street1, String street2, String state,
String city, String country, Integer zipCode) {
this.street1 = street1;
this.street2 = street2;
this.state = state;
this.city = city;
this.country = country;
this.zipCode = zipCode;
}
public Address(String street1, String street2, String state,
String city, String country, Integer zipCode, AddressType addressType) {
this.street1 = street1;
this.street2 = street2;
this.state = state;
this.city = city;
this.country = country;
this.zipCode = zipCode;
this.addressType = addressType;
}
public String getStreet1() {
return street1;
}
public void setStreet1(String street1) {
this.street1 = street1;
}
public String getStreet2() {
return street2;
}
public void setStreet2(String street2) {
this.street2 = street2;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public Integer getZipCode() {
return zipCode;
}
public void setZipCode(Integer zipCode) {
this.zipCode = zipCode;
}
public AddressType getAddressType() {
return addressType;
}
public void setAddressType(AddressType addressType) {
this.addressType = addressType;
}
@Override
public String toString() {
return "Address{" +
"street1='" + street1 + '\'' +
", street2='" + street2 + '\'' +
", state='" + state + '\'' +
", city='" + city + '\'' +
", country='" + country + '\'' +
", zipCode=" + zipCode +
'}';
}
}

View File

@@ -0,0 +1,6 @@
package demo.domain;
public enum AddressType {
SHIPPING,
BILLING
}

View File

@@ -0,0 +1,64 @@
package demo.inventory.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import demo.domain.AbstractEntity;
import org.springframework.hateoas.Link;
import java.util.ArrayList;
import java.util.List;
public class Inventory extends AbstractEntity {
private Long id;
private InventoryStatus status;
private List<InventoryEvent> events = new ArrayList<>();
private String productId;
public Inventory() {
}
public Inventory(InventoryStatus status, String productId) {
this.status = status;
this.productId = productId;
}
@JsonProperty("inventoryId")
public Long getIdentity() {
return this.id;
}
public void setIdentity(Long id) {
this.id = id;
}
public InventoryStatus getStatus() {
return status;
}
public void setStatus(InventoryStatus status) {
this.status = status;
}
public List<InventoryEvent> getEvents() {
return events;
}
public void setEvents(List<InventoryEvent> events) {
this.events = events;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/
@Override
public Link getId() {
return getLink("self");
}
}

View File

@@ -0,0 +1,36 @@
package demo.inventory.domain;
import demo.domain.AbstractEntity;
/**
* The domain event {@link InventoryEvent} tracks the type and state of events as applied to the {@link Inventory} domain
* object. This event resource can be used to event source the aggregate state of {@link Inventory}.
*
* @author kbastani
*/
public class InventoryEvent extends AbstractEntity {
private InventoryEventType type;
public InventoryEvent() {
}
public InventoryEvent(InventoryEventType type) {
this.type = type;
}
public InventoryEventType getType() {
return type;
}
public void setType(InventoryEventType type) {
this.type = type;
}
@Override
public String toString() {
return "InventoryEvent{" +
"type=" + type +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,15 @@
package demo.inventory.domain;
/**
* The {@link InventoryEventType} represents a collection of possible events that describe state transitions of
* {@link InventoryStatus} on the {@link Inventory} aggregate.
*
* @author Kenny Bastani
*/
public enum InventoryEventType {
INVENTORY_CREATED,
RESERVATION_PENDING,
RESERVATION_CONNECTED,
INVENTORY_RESERVED,
INVENTORY_RELEASED
}

View File

@@ -0,0 +1,7 @@
package demo.inventory.domain;
import org.springframework.hateoas.Resources;
public class InventoryEvents extends Resources<InventoryEvent> {
}

View File

@@ -0,0 +1,9 @@
package demo.inventory.domain;
public enum InventoryStatus {
INVENTORY_CREATED,
RESERVATION_PENDING,
RESERVATION_CONNECTED,
INVENTORY_RESERVED,
INVENTORY_RELEASED
}

View File

@@ -0,0 +1,71 @@
package demo.order.domain;
public class LineItem {
private String name, productId;
private Integer quantity;
private Double price, tax;
public LineItem() {
}
public LineItem(String name, String productId, Integer quantity,
Double price, Double tax) {
this.name = name;
this.productId = productId;
this.quantity = quantity;
this.price = price;
this.tax = tax;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Double getTax() {
return tax;
}
public void setTax(Double tax) {
this.tax = tax;
}
@Override
public String toString() {
return "LineItem{" +
"name='" + name + '\'' +
", productId='" + productId + '\'' +
", quantity=" + quantity +
", price=" + price +
", tax=" + tax +
'}';
}
}

View File

@@ -0,0 +1,119 @@
package demo.order.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import demo.domain.Address;
import demo.domain.AddressType;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Order {
private Long id;
private Long createdAt;
private Long lastModified;
private List<OrderEvent> orderEvents = new ArrayList<>();
private OrderStatus status;
private Set<LineItem> lineItems = new HashSet<>();
private Address shippingAddress;
private Long accountId, paymentId;
public Order() {
this.status = OrderStatus.ORDER_CREATED;
}
public Order(Long accountId, Address shippingAddress) {
this();
this.accountId = accountId;
this.shippingAddress = shippingAddress;
if (shippingAddress.getAddressType() == null)
this.shippingAddress.setAddressType(AddressType.SHIPPING);
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getLastModified() {
return lastModified;
}
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public Set<LineItem> getLineItems() {
return lineItems;
}
public void setLineItems(Set<LineItem> lineItems) {
this.lineItems = lineItems;
}
public Address getShippingAddress() {
return shippingAddress;
}
public void setShippingAddress(Address shippingAddress) {
this.shippingAddress = shippingAddress;
}
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public Long getPaymentId() {
return paymentId;
}
public void setPaymentId(Long paymentId) {
this.paymentId = paymentId;
}
@JsonIgnore
public List<OrderEvent> getEvents() {
return orderEvents;
}
@JsonProperty("orderId")
public Long getIdentity() {
return id;
}
public void setIdentity(Long id) {
this.id = id;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/
public Link getId() {
return new Link(new UriTemplate("http://order-web/v1/orders/{id}").with("id", TemplateVariable.VariableType
.PATH_VARIABLE)
.expand(getIdentity())
.toString()).withSelfRel();
}
}

View File

@@ -0,0 +1,83 @@
package demo.order.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* The domain event {@link OrderEvent} tracks the type and state of events as applied to the {@link Order} domain
* object. This event resource can be used to event source the aggregate state of {@link Order}.
* <p>
* This event resource also provides a transaction log that can be used to append actions to the event.
*
* @author Kenny Bastani
*/
public class OrderEvent {
private Long eventId;
private OrderEventType type;
@JsonIgnore
private Order order;
private Long createdAt, lastModified;
public OrderEvent() {
}
public OrderEvent(OrderEventType type) {
this.type = type;
}
public OrderEvent(OrderEventType type, Order order) {
this.type = type;
this.order = order;
}
public Long getEventId() {
return eventId;
}
public void setEventId(Long id) {
eventId = id;
}
public OrderEventType getType() {
return type;
}
public void setType(OrderEventType type) {
this.type = type;
}
public Order getEntity() {
return order;
}
public void setEntity(Order entity) {
this.order = entity;
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getLastModified() {
return lastModified;
}
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
@Override
public String toString() {
return "OrderEvent{" +
"eventId=" + eventId +
", type=" + type +
", order=" + order +
", createdAt=" + createdAt +
", lastModified=" + lastModified +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,21 @@
package demo.order.domain;
/**
* The {@link OrderEventType} represents a collection of possible events that describe state transitions of
* {@link OrderStatus} on the {@link Order} aggregate.
*
* @author Kenny Bastani
*/
public enum OrderEventType {
ORDER_CREATED,
ACCOUNT_CONNECTED,
RESERVATION_PENDING,
INVENTORY_RESERVED,
RESERVATION_SUCCEEDED,
RESERVATION_FAILED,
PAYMENT_CREATED,
PAYMENT_CONNECTED,
PAYMENT_PENDING,
PAYMENT_SUCCEEDED,
PAYMENT_FAILED
}

View File

@@ -0,0 +1,17 @@
package demo.order.domain;
public enum OrderStatus {
ORDER_CREATED,
ACCOUNT_CONNECTED,
RESERVATION_PENDING,
INVENTORY_RESERVED,
RESERVATION_SUCCEEDED,
RESERVATION_FAILED,
PAYMENT_CREATED,
PAYMENT_CONNECTED,
PAYMENT_PENDING,
PAYMENT_SUCCEEDED,
PAYMENT_FAILED,
ORDER_SUCCEEDED,
ORDER_FAILED
}

View File

@@ -0,0 +1,23 @@
package demo.order.domain;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
public class Orders extends Resources<Order> {
/**
* Creates an empty {@link Resources} instance.
*/
public Orders() {
}
/**
* Creates a {@link Resources} instance with the given content and {@link Link}s (optional).
*
* @param content must not be {@literal null}.
* @param links the links to be added to the {@link Resources}.
*/
public Orders(Iterable<Order> content, Link... links) {
super(content, links);
}
}

View File

@@ -0,0 +1,109 @@
package demo.order.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import demo.order.domain.Order;
import demo.order.domain.Orders;
import org.apache.log4j.Logger;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@org.springframework.stereotype.Service
public class OrderService {
private final Logger log = Logger.getLogger(this.getClass());
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Order get(Long orderId) {
Order result;
try {
result = restTemplate.getForObject(new UriTemplate("http://order-web/v1/orders/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(orderId), Order.class);
} catch (RestClientResponseException ex) {
log.error("Get order failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Order create(Order order) {
Order result;
try {
result = restTemplate.postForObject(new UriTemplate("http://order-web/v1/orders").expand(),
order, Order.class);
} catch (RestClientResponseException ex) {
log.error("Create order failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Order update(Order order) {
Order result;
try {
result = restTemplate.exchange(new RequestEntity<>(order, HttpMethod.PUT,
new UriTemplate("http://order-web/v1/orders/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(order.getIdentity())), Order.class).getBody();
} catch (RestClientResponseException ex) {
log.error("Update order failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public boolean delete(Long orderId) {
try {
restTemplate.delete(new UriTemplate("http://order-web/v1/orders/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE).expand(orderId));
} catch (RestClientResponseException ex) {
log.error("Delete order failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return true;
}
public Orders findOrdersByAccountId(Long accountId) {
Orders result;
try {
result = restTemplate
.getForObject(new UriTemplate("http://order-web/v1/orders/search/findOrdersByAccountId")
.with("accountId", TemplateVariable.VariableType.REQUEST_PARAM)
.expand(accountId), Orders.class);
} catch (RestClientResponseException ex) {
log.error("Delete order failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
private String getHttpStatusMessage(RestClientResponseException ex) {
Map<String, String> errorMap = new HashMap<>();
try {
errorMap = new ObjectMapper()
.readValue(ex.getResponseBodyAsString(), errorMap
.getClass());
} catch (IOException e) {
e.printStackTrace();
}
return errorMap.getOrDefault("message", null);
}
}

View File

@@ -0,0 +1,76 @@
package demo.warehouse.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import demo.domain.AbstractEntity;
import demo.domain.Address;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import java.util.ArrayList;
import java.util.List;
public class Warehouse extends AbstractEntity {
private Long id;
private List<WarehouseEvent> events = new ArrayList<>();
private Address address;
private WarehouseStatus status;
public Warehouse() {
}
public Warehouse(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public WarehouseStatus getStatus() {
return status;
}
public void setStatus(WarehouseStatus status) {
this.status = status;
}
@JsonProperty("warehouseId")
public Long getIdentity() {
return id;
}
public void setIdentity(Long id) {
this.id = id;
}
public List<WarehouseEvent> getEvents() {
return events;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/
@Override
public Link getId() {
return new Link(new UriTemplate("http://warehouse-web/v1/warehouses/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(getIdentity())
.toString()).withSelfRel();
}
@Override
public String toString() {
return "Warehouse{" +
"id=" + id +
", events=" + events +
", address=" + address +
", status=" + status +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,79 @@
package demo.warehouse.domain;
/**
* The domain event {@link WarehouseEvent} tracks the type and state of events as applied to the {@link Warehouse}
* domain
* object. This event resource can be used to event source the aggregate state of {@link Warehouse}.
*
* @author kbastani
*/
public class WarehouseEvent {
private Long eventId;
private WarehouseEventType type;
private Warehouse entity;
private Long createdAt;
private Long lastModified;
public WarehouseEvent() {
}
public WarehouseEvent(WarehouseEventType type) {
this.type = type;
}
public WarehouseEvent(WarehouseEventType type, Warehouse entity) {
this.type = type;
this.entity = entity;
}
public Long getEventId() {
return eventId;
}
public void setEventId(Long id) {
eventId = id;
}
public WarehouseEventType getType() {
return type;
}
public void setType(WarehouseEventType type) {
this.type = type;
}
public Warehouse getEntity() {
return entity;
}
public void setEntity(Warehouse entity) {
this.entity = entity;
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getLastModified() {
return lastModified;
}
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
public String toString() {
return "WarehouseEvent{" +
"eventId=" + eventId +
", type=" + type +
", entity=" + entity +
", createdAt=" + createdAt +
", lastModified=" + lastModified +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,11 @@
package demo.warehouse.domain;
/**
* The {@link WarehouseEventType} represents a collection of possible events that describe state transitions of
* {@link WarehouseStatus} on the {@link Warehouse} aggregate.
*
* @author Kenny Bastani
*/
public enum WarehouseEventType {
WAREHOUSE_CREATED
}

View File

@@ -0,0 +1,5 @@
package demo.warehouse.domain;
public enum WarehouseStatus {
WAREHOUSE_CREATED
}

View File

@@ -0,0 +1,23 @@
package demo.warehouse.domain;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
public class Warehouses extends Resources<Warehouse> {
/**
* Creates an empty {@link Resources} instance.
*/
public Warehouses() {
}
/**
* Creates a {@link Resources} instance with the given content and {@link Link}s (optional).
*
* @param content must not be {@literal null}.
* @param links the links to be added to the {@link Resources}.
*/
public Warehouses(Iterable<Warehouse> content, Link... links) {
super(content, links);
}
}

View File

@@ -0,0 +1,112 @@
package demo.warehouse.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import demo.inventory.domain.Inventory;
import demo.warehouse.domain.Warehouse;
import org.apache.log4j.Logger;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@org.springframework.stereotype.Service
public class WarehouseService {
private final Logger log = Logger.getLogger(this.getClass());
private final RestTemplate restTemplate;
public WarehouseService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Warehouse get(Long warehouseId) {
Warehouse result;
try {
result = restTemplate.getForObject(new UriTemplate("http://warehouse-web/v1/warehouses/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(warehouseId), Warehouse.class);
} catch (RestClientResponseException ex) {
log.error("Get warehouse failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Warehouse create(Warehouse warehouse) {
Warehouse result;
try {
result = restTemplate.postForObject(new UriTemplate("http://warehouse-web/v1/warehouses").expand(),
warehouse, Warehouse.class);
} catch (RestClientResponseException ex) {
log.error("Create warehouse failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public Warehouse update(Warehouse warehouse) {
Warehouse result;
try {
result = restTemplate.exchange(new RequestEntity<>(warehouse, HttpMethod.PUT,
new UriTemplate("http://warehouse-web/v1/warehouses/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE)
.expand(warehouse.getIdentity())), Warehouse.class).getBody();
} catch (RestClientResponseException ex) {
log.error("Update warehouse failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
public boolean delete(Long warehouseId) {
try {
restTemplate.delete(new UriTemplate("http://warehouse-web/v1/warehouses/{id}")
.with("id", TemplateVariable.VariableType.PATH_VARIABLE).expand(warehouseId));
} catch (RestClientResponseException ex) {
log.error("Delete warehouse failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return true;
}
public List<Inventory> addInventory(List<Inventory> inventory, Long warehouseId) {
List<Inventory> result = new ArrayList<>();
try {
inventory.parallelStream().forEach(item -> {
result.add(restTemplate.postForObject(new UriTemplate(String
.format("http://warehouse-web/v1/warehouses/%s/inventory", warehouseId))
.expand(), item, Inventory.class));
});
} catch (RestClientResponseException ex) {
log.error("Add warehouse inventory failed", ex);
throw new IllegalStateException(getHttpStatusMessage(ex), ex);
}
return result;
}
private String getHttpStatusMessage(RestClientResponseException ex) {
Map<String, String> errorMap = new HashMap<>();
try {
errorMap = new ObjectMapper()
.readValue(ex.getResponseBodyAsString(), errorMap
.getClass());
} catch (IOException e) {
e.printStackTrace();
}
return errorMap.getOrDefault("message", null);
}
}

View File

@@ -0,0 +1,45 @@
spring:
profiles:
active: development
server:
port: 0
---
spring:
profiles: development
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
---
spring:
profiles: docker
eureka:
client:
service-url:
defaultZone: http://${DOCKER_IP:192.168.99.100}:8761/eureka
registryFetchIntervalSeconds: 5
instance:
hostname: ${DOCKER_IP:192.168.99.100}
instance-id: ${spring.application.name}:${random.int}
leaseRenewalIntervalInSeconds: 5
---
spring:
profiles: test
eureka:
client:
enabled: false
---
spring:
profiles: cloud
eureka:
instance:
hostname: ${vcap.application.uris[0]:localhost}
nonSecurePort: 80
metadataMap:
instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${server.port}}}
leaseRenewalIntervalInSeconds: 5
client:
region: default
registryFetchIntervalSeconds: 5
serviceUrl:
defaultZone: ${vcap.services.discovery-service.credentials.uri:http://localhost:8761}/eureka/

View File

@@ -0,0 +1,4 @@
spring:
application:
name: load-simulator
---

View File

@@ -0,0 +1,26 @@
package demo;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.junit.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.json.JsonPropertyAccessor;
import java.text.ParseException;
public class DateTimeTests {
@Test
public void testDateTime() throws ParseException {
DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
System.out.print(new SpelExpressionParser().parseRaw("new org.joda.time.DateTime(new java.lang.Long('1487145898898')).toLocalDateTime().toString()").getValue());
dateFormatter.parseDateTime(new DateTime(new Long("1487145898898")).toLocalDateTime().toString());
JsonPropertyAccessor.ToStringFriendlyJsonNode jsonNode = new JsonPropertyAccessor.ToStringFriendlyJsonNode(JsonNodeFactory.instance.textNode("test"));
new SpelExpressionParser().parseExpression("toString()").getValue(jsonNode, CharSequence.class);
}
}

View File

@@ -0,0 +1,18 @@
package demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class LoadSimulatorApplicationTests {
@Test
public void contextLoads() {
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>stream-modules</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>stream-modules</name>
<parent>
<groupId>org.kbastani</groupId>
<artifactId>platform-services</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<modules>
<module>load-simulator</module>
</modules>
</project>

View File

@@ -2,8 +2,11 @@ package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
@@ -13,4 +16,10 @@ public class DashboardApplication {
public static void main(String[] args) {
SpringApplication.run(DashboardApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -12,6 +12,10 @@ zuul:
account-web: /account/**
warehouse-web: /warehouse/**
order-web: /order/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
---
spring:
profiles: docker
@@ -20,6 +24,7 @@ zuul:
routes:
account-web: /account/**
warehouse-web: /warehouse/**
order-web: /order/**
eureka:
client:
service-url:

View File

@@ -67,6 +67,7 @@
<script src="src/js/client/api/warehouseApi.js"></script>
<script src="src/js/client/api/orderApi.js"></script>
<script src="src/js/client/api/accountApi.js"></script>
<script src="src/js/util/faker.js"></script>
<script src="src/js/root.js"></script>
</body>

View File

@@ -117,12 +117,17 @@ var loadAccount = function (id, callback) {
$.each(document._links, function (k, v) {
var commandBtn = $("<div class='command-btn'>" + "<input class='command-href' type='hidden' value='" + v.href + "'>" + "<input type='button' class='btn btn-default' value='" + k + "'></div>");
$(commandBtn).click(function () {
appDispatcher.handle(generate("GET", null, $(this).find(".command-href").val()), function (cmd) {
accountStatus = cmd.status;
updateAccountStatus(accountStatus);
$(formSelect).text('');
$(formSelect).jsonForm(accountForm.apply(formSelect, cmd));
});
if (k != "postOrder") {
appDispatcher.handle(generate("GET", null, $(this).find(".command-href").val()), function (cmd) {
accountStatus = cmd.status;
updateAccountStatus(accountStatus);
$(formSelect).text('');
$(formSelect).jsonForm(accountForm.apply(formSelect, cmd));
});
} else {
postOrderCommand($(this).find(".command-href").val());
}
});
$(commandSelect).append(commandBtn);
});

View File

@@ -227,7 +227,9 @@ var loadOrder = function (id, callback) {
updateOrderStatus(eventList[0].type);
callback();
} else if (lastOrderSize < eventList.length) {
appendRow(".order-events", eventList.filter(function (x) {
appendRow(".order-events", eventList.sort(function (a, b) {
return a.createdAt - b.createdAt;
}).filter(function (x) {
return eventList.indexOf(x) > lastOrderSize - 1;
}), true, updateOrderStatus);
lastOrderSize = eventList.length;
@@ -318,12 +320,14 @@ var renderOrderFlow = function (callback, orderStatus) {
});
g.setEdge(2, 3, {
label: "RESERVATION_ADDED",
minlen: 2
minlen: 1
});
g.setEdge(3, 4, {
label: "RESERVATION_SUCCEEDED"
label: "RESERVATION_SUCCEEDED",
labelpos: 'r',
minlen: 2
});
g.setEdge(2, 8, {
g.setEdge(3, 8, {
label: "RESERVATION_FAILED",
labelpos: 'l'
});
@@ -366,7 +370,7 @@ var renderOrderFlow = function (callback, orderStatus) {
inner.call(render, g);
var draw = function (isUpdate) {
var graphWidth = g.graph().width + 35;
var graphHeight = g.graph().height + 200;
var graphHeight = g.graph().height + 100;
var width = parseInt(svg.style("width").replace(/px/, ""));
var height = parseInt(svg.style("height").replace(/px/, ""));
var zoomScale = Math.min(width / graphWidth, height / graphHeight);

View File

@@ -13,6 +13,15 @@ var warehouseApi = {
}
};
var inventoryApi = {
get: function (id, callback) {
appDispatcher.handle(generate("GET", null, "/warehouse/v1/warehouses/" + id + "/inventory"), callback);
},
post: function (id, body, callback) {
appDispatcher.handle(generate("POST", body, "/warehouse/v1/warehouses/" + id + "/inventory"), callback);
}
};
var warehouseForm = {
apply: function (selector, value) {
return {

View File

@@ -11,7 +11,12 @@ var appDispatcher = {
.success(callback)
.error(function (err) {
$(".modal").modal();
$(".modal-body").text($.parseJSON(err.responseText).message);
if (err.statusCode().status == 404) {
$(".modal-body").text("The requested resource was not found");
callback(err);
} else {
$(".modal-body").text($.parseJSON(err.responseText).message);
}
});
}
};

View File

@@ -1,6 +1,9 @@
var app = angular.module("myApp", ["ngRoute"]);
var polling = true;
var pageId;
var windowService;
var locationService;
var scopeService;
app.config(function ($routeProvider) {
$routeProvider.when("/", {
templateUrl: "/src/partials/main.html",
@@ -15,9 +18,17 @@ app.config(function ($routeProvider) {
templateUrl: "/src/partials/warehouse.html",
cache: false
});
}).directive('loader', ['$routeParams', function ($routeParams) {
}).directive('loader', ['$routeParams', '$window', '$location', function ($routeParams, $window, $location) {
return {
link: function (scope, element, attr) {
scope.domain = attr.domain;
pageId = $routeParams.id;
windowService = $window;
locationService = $location;
scopeService = scope;
$('#accordion').collapse({
toggle: false
});
@@ -29,7 +40,7 @@ app.config(function ($routeProvider) {
}).on('hidden.bs.collapse', function () {
$(this).parent().find('.panel-title').removeClass("expanded").addClass("expandable");
});
pageId = $routeParams.id;
var selector = ".item-title";
var handleRoute = function (after) {
switch (attr.domain) {
@@ -62,24 +73,75 @@ app.config(function ($routeProvider) {
break;
case "home":
$(selector).text("Warehouse " + 1);
loadWarehouse(1, after);
var loadHomeAccount = function () {
accountApi.get(1, function (acctRes) {
if (acctRes.statusCode != null) {
if (acctRes.statusCode().status == 404) {
setupAccounts(1, function (result) {
if (result.statusCode == null) {
accountApi.get(1, function (newRes) {
$(".account-form").jsonForm(accountForm.apply(".account-form", newRes));
});
}
});
}
} else {
$(".account-form").jsonForm(accountForm.apply(".account-form", acctRes));
}
});
};
var loadHomeWarehouse = function () {
loadWarehouse(1, after, function (res) {
$(".modal").modal('hide');
if (res.statusCode().status == 404) {
// Create the first warehouse and reload page
setupWarehouses(1, function (result) {
if (result.statusCode == null) {
console.log(result);
addInventory(1, 5, 20, function () {
console.log("Inventory added");
loadHomeWarehouse();
});
}
});
}
});
};
loadHomeAccount();
loadHomeWarehouse();
break;
}
};
var doPoll = function () {
if (polling) {
setTimeout(function () {
handleRoute(doPoll);
if (scope.domain == scopeService.domain) {
handleRoute(doPoll);
}
}, 2000);
}
};
if (polling) {
polling = false;
handleRoute(function () {
setTimeout(function () {
polling = true;
doPoll();
}, 2000);
if (attr.domain != "home") {
if (polling) {
polling = false;
if (scope.domain == scopeService.domain) {
handleRoute(function () {
setTimeout(function () {
polling = true;
doPoll();
}, 2000);
});
}
}
} else {
handleRoute(function() {
});
}
return attr;
@@ -89,49 +151,168 @@ app.config(function ($routeProvider) {
var lastWarehouseSize = 0;
var warehouseHref = null;
var warehouseId = null;
var loadWarehouse = function (id, callback) {
warehouseId = warehouseId || id;
var getWarehouse = function (after) {
warehouseApi.get(id, function (res) {
var selector = ".warehouse-form";
$(selector).text('');
$(selector).jsonForm(warehouseForm.apply(selector, res));
warehouseHref = res._links.inventory.href;
if (after != null) after();
});
};
var loadWarehouseEvents = function () {
traverson.from(warehouseHref).getResource(function (error, document) {
if (error) {
console.error('Could not fetch events for the warehouse')
callback();
} else {
var inventoryList = document._embedded.inventoryList;
if (lastWarehouseSize < inventoryList.length && lastWarehouseSize == 0) {
lastWarehouseSize = inventoryList.length;
createTable(".warehouse-inventory", inventoryList);
callback();
} else if (lastWarehouseSize < inventoryList.length) {
appendRow(".warehouse-inventory", inventoryList.filter(function (x) {
return inventoryList.indexOf(x) > lastWarehouseSize - 1;
}));
lastWarehouseSize = inventoryList.length;
getWarehouse(function () {
callback();
});
var loadWarehouse = function (id, callback, err) {
warehouseId = warehouseId || id;
var getWarehouse = function (after) {
warehouseApi.get(id, function (res) {
console.log(res);
if (err != null ? res.statusCode != null : res != null) {
err(res);
} else {
callback();
var selector = ".warehouse-form";
$(selector).text('');
$(selector).jsonForm(warehouseForm.apply(selector, res));
warehouseHref = res._links.inventory.href;
if (after != null) after();
}
}
});
};
if (warehouseHref == null) {
getWarehouse(function () {
});
};
var loadWarehouseEvents = function () {
traverson.from(warehouseHref).getResource(function (error, document) {
if (error) {
console.error('Could not fetch events for the warehouse')
callback();
} else {
var inventoryList = document._embedded.inventoryList;
if (lastWarehouseSize < inventoryList.length && lastWarehouseSize == 0) {
lastWarehouseSize = inventoryList.length;
createTable(".warehouse-inventory", inventoryList);
callback();
} else if (lastWarehouseSize < inventoryList.length) {
appendRow(".warehouse-inventory", inventoryList.filter(function (x) {
return inventoryList.indexOf(x) > lastWarehouseSize - 1;
}).sort(function (a, b) {
return a.createdAt - b.createdAt;
}));
lastWarehouseSize = inventoryList.length;
getWarehouse(function () {
callback();
});
} else {
callback();
}
}
});
};
if (warehouseHref == null) {
getWarehouse(function () {
loadWarehouseEvents();
});
} else {
loadWarehouseEvents();
}
}
;
var setupAccounts = function (num) {
for (var i = 0; i < num; i++) {
accountApi.post(JSON.stringify(getFakeAccount()), function (res) {
console.log(res);
});
} else {
loadWarehouseEvents();
}
};
var getFakeAccount = function () {
return {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email()
};
};
var setupWarehouses = function (num, callback) {
var counter = 0;
for (var i = 0; i < num; i++) {
var item = getFakeWarehouse();
warehouseApi.post(JSON.stringify(item), function (result) {
console.log(result);
counter += 1;
if (counter == num) {
callback(result);
}
});
}
};
var addInventory = function (warehouseId, seed, num, callback) {
var counter = 0;
for (var i = 0; i < num; i++) {
var item = getFakeInventory(seed);
inventoryApi.post(warehouseId, JSON.stringify(item), function () {
counter += 1;
if (counter == num) {
callback();
}
});
}
};
var getFakeAddress = function () {
return {
street1: faker.address.streetAddress(),
state: faker.address.state(),
city: faker.address.city(),
country: faker.address.country(),
zipCode: parseInt(faker.address.zipCode().substring(0, 5))
};
};
var getFakeWarehouse = function () {
return {
address: getFakeAddress(),
status: "WAREHOUSE_CREATED"
};
};
var getFakeInventory = function (seed) {
return {
productId: "SKU-" + String("00" + Math.floor((Math.random() * seed) + 1)).slice(-5)
};
};
var getFakeLineItem = function (seed) {
return {
name: faker.commerce.productName(),
productId: "SKU-" + String("00" + seed).slice(-5),
quantity: Math.floor((Math.random() * 5) + 1),
price: faker.commerce.price(),
tax: 0.06
};
};
var postOrderCommand = function (href) {
appDispatcher.handle(generate("POST", JSON.stringify(getFakeOrder(5)), href), function (res) {
console.log(res);
if (res.statusCode == null) {
traverson.from(res._links.self.href).getResource(function (error, document) {
if (error == null) {
if (scopeService != null) {
scopeService.$apply(function () {
locationService.path("/orders/" + document.orderId);
});
}
} else {
$(".modal").modal();
$(".modal-body").text($.parseJSON(document.responseText).message);
}
});
}
});
};
var getFakeOrder = function (numProducts) {
var shippingAddress = getFakeAddress();
shippingAddress.addressType = "SHIPPING";
var lineItems = [];
var numLineItems = Math.floor((Math.random() * numProducts) + 1);
for (var i = 0; i < numLineItems; i++) {
lineItems.push(getFakeLineItem(i + 1));
}
return {
shippingAddress: shippingAddress,
lineItems: lineItems
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ function appendRow(selector, array, replay, statusCallback) {
var tbl_body = "";
var tbl_row = "";
$.each(item, function (k, v) {
if (k != "_links" && k != "lastModified") {
tbl_row += "<td>" + ((k == "createdAt") ? new Date(v).toLocaleString() : v) + "</td>";

View File

@@ -1,14 +1,26 @@
<div class="row">
<div class="well">
<div loader domain="home">
<fieldset class="control-group">
<legend class="item-title">Warehouse</legend>
<form class="warehouse-form"></form>
</fieldset>
<fieldset class="control-group">
<legend>Inventory</legend>
<div class="warehouse-inventory"></div>
</fieldset>
<div class="col-md-6">
<div class="well">
<div loader domain="home">
<fieldset class="control-group">
<legend class="item-title">Warehouse</legend>
<form class="warehouse-form"></form>
</fieldset>
<fieldset class="control-group">
<legend>Inventory</legend>
<div class="warehouse-inventory"></div>
</fieldset>
</div>
</div>
</div>
<div class="col-md-6">
<div class="well">
<div>
<fieldset class="control-group">
<legend class="page-title header-title">Account 1</legend>
<form class="account-form"></form>
</fieldset>
</div>
</div>
</div>
</div>