Cleaning up

This commit is contained in:
Kenny Bastani
2016-12-20 17:30:24 -08:00
parent 878e40a2e5
commit 2af2788bc7
154 changed files with 2507 additions and 451 deletions

View File

@@ -0,0 +1,95 @@
<?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>order-worker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>order-worker</name>
<parent>
<groupId>org.kbastani</groupId>
<artifactId>order</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.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>${spring-statemachine-core.version}</version>
</dependency>
<dependency>
<groupId>org.kbastani</groupId>
<artifactId>spring-boot-starter-aws-lambda</artifactId>
<version>${spring-boot-starter-aws-lambda.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sts</artifactId>
<version>${aws-java-sdk-sts.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-lambda</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json-path.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>${aws-java-sdk-sts.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
@SpringBootApplication
@EnableHypermediaSupport(type = {HypermediaType.HAL})
public class OrderWorkerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderWorkerApplication.class, args);
}
}

View File

@@ -0,0 +1,92 @@
package demo.address;
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 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 +
", addressType=" + addressType +
'}';
}
}

View File

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

View File

@@ -0,0 +1,25 @@
package demo.config;
import amazon.aws.AWSLambdaConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import demo.function.LambdaFunctions;
import demo.util.LambdaUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("cloud")
public class AwsLambdaConfig {
@Bean
public LambdaFunctions lambdaInvoker(AWSLambdaConfigurerAdapter configurerAdapter) {
return configurerAdapter
.getFunctionInstance(LambdaFunctions.class);
}
@Bean
public LambdaUtil lambdaUtil(ObjectMapper objectMapper) {
return new LambdaUtil(objectMapper);
}
}

View File

@@ -0,0 +1,104 @@
package demo.config;
import demo.event.OrderEvent;
import demo.event.OrderEventType;
import demo.function.OrderFunction;
import demo.order.Order;
import demo.order.OrderStatus;
import org.apache.log4j.Logger;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import java.util.EnumSet;
/**
* A configuration adapter for describing a {@link StateMachine} factory that maps actions to functional
* expressions. Actions are executed during transitions between a source state and a target state.
* <p>
* A state machine provides a robust declarative language for describing the state of an {@link Order}
* resource given a sequence of ordered {@link demo.event.OrderEvents}. When an event is received
* in {@link demo.event.OrderEventStream}, an in-memory state machine is fully replicated given the
* {@link demo.event.OrderEvents} attached to an {@link Order} resource.
*
* @author kbastani
*/
@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatus, OrderEventType> {
final private Logger log = Logger.getLogger(StateMachineConfig.class);
/**
* Configures the initial conditions of a new in-memory {@link StateMachine} for {@link Order}.
*
* @param states is the {@link StateMachineStateConfigurer} used to describe the initial condition
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatus, OrderEventType> states) {
try {
// Describe the initial condition of the order state machine
states.withStates()
.initial(OrderStatus.PENDING)
.states(EnumSet.allOf(OrderStatus.class));
} catch (Exception e) {
throw new RuntimeException("State machine configuration failed", e);
}
}
/**
* Configures the {@link StateMachine} that describes how {@link OrderEventType} drives the state
* of an {@link Order}. Events are applied as transitions from a source {@link OrderStatus} to
* a target {@link OrderStatus}. An {@link Action} is attached to each transition, which maps to a
* function that is executed in the context of an {@link OrderEvent}.
*
* @param transitions is the {@link StateMachineTransitionConfigurer} used to describe state transitions
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEventType> transitions) {
try {
// Describe state machine transitions for orders
// TODO: Configure state machine
} catch (Exception e) {
throw new RuntimeException("Could not configure state machine transitions", e);
}
}
/**
* Functions are mapped to actions that are triggered during the replication of a state machine. Functions
* should only be executed after the state machine has completed replication. This method checks the state
* context of the machine for an {@link OrderEvent}, which signals that the state machine is finished
* replication.
* <p>
* The {@link OrderFunction} argument is only applied if an {@link OrderEvent} is provided as a
* message header in the {@link StateContext}.
*
* @param context is the state machine context that may include an {@link OrderEvent}
* @param orderFunction is the order function to apply after the state machine has completed replication
* @return an {@link OrderEvent} only if this event has not yet been processed, otherwise returns null
*/
private OrderEvent applyEvent(StateContext<OrderStatus, OrderEventType> context,
OrderFunction orderFunction) {
OrderEvent orderEvent = null;
// Log out the progress of the state machine replication
log.info("Replicate event: " + context.getMessage().getPayload());
// The machine is finished replicating when an OrderEvent is found in the message header
if (context.getMessageHeader("event") != null) {
orderEvent = (OrderEvent) context.getMessageHeader("event");
log.info("State machine replicated: " + orderEvent.getType());
// Apply the provided function to the OrderEvent
orderFunction.apply(orderEvent);
}
return orderEvent;
}
}

View File

@@ -0,0 +1,36 @@
package demo.domain;
import org.springframework.hateoas.ResourceSupport;
public class BaseEntity extends ResourceSupport {
private Long createdAt;
private Long lastModified;
public BaseEntity() {
}
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,28 @@
package demo.event;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
@RequestMapping("/v1")
public class EventController {
private EventService eventService;
public EventController(EventService eventService) {
this.eventService = eventService;
}
@PostMapping(path = "/events")
public ResponseEntity handleEvent(@RequestBody OrderEvent event) {
return Optional.ofNullable(eventService.apply(event))
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
.orElseThrow(() -> new RuntimeException("Apply event failed"));
}
}

View File

@@ -0,0 +1,82 @@
package demo.event;
import demo.order.Order;
import demo.order.OrderStatus;
import demo.state.StateMachineService;
import org.apache.log4j.Logger;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.client.Traverson;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
@Service
public class EventService {
final private Logger log = Logger.getLogger(EventService.class);
final private StateMachineService stateMachineService;
public EventService(StateMachineService stateMachineService) {
this.stateMachineService = stateMachineService;
}
public Order apply(OrderEvent orderEvent) {
Order result;
log.info("Order event received: " + orderEvent.getLink("self").getHref());
// Generate a state machine for computing the state of the order resource
StateMachine<OrderStatus, OrderEventType> stateMachine =
stateMachineService.getStateMachine();
// Follow the hypermedia link to fetch the attached order
Traverson traverson = new Traverson(
URI.create(orderEvent.getLink("order").getHref()),
MediaTypes.HAL_JSON
);
// Get the event log for the attached order resource
OrderEvents events = traverson.follow("events")
.toEntity(OrderEvents.class)
.getBody();
// Prepare order event message headers
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("event", orderEvent);
// Replicate the current state of the order resource
events.getContent()
.stream()
.sorted((a1, a2) -> a1.getCreatedAt().compareTo(a2.getCreatedAt()))
.forEach(e -> {
MessageHeaders headers = new MessageHeaders(null);
// Check to see if this is the current event
if (e.getLink("self").equals(orderEvent.getLink("self"))) {
headers = new MessageHeaders(headerMap);
}
// Send the event to the state machine
stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), headers));
});
// Get result
Map<Object, Object> context = stateMachine.getExtendedState()
.getVariables();
// Get the order result
result = (Order) context.getOrDefault("order", null);
// Destroy the state machine
stateMachine.stop();
return result;
}
}

View File

@@ -0,0 +1,30 @@
package demo.event;
import demo.domain.BaseEntity;
public class OrderEvent extends BaseEntity {
private OrderEventType type;
public OrderEvent() {
}
public OrderEvent(OrderEventType type) {
this.type = type;
}
public OrderEventType getType() {
return type;
}
public void setType(OrderEventType type) {
this.type = type;
}
@Override
public String toString() {
return "AccountEvent{" +
"type=" + type +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,31 @@
package demo.event;
import demo.order.Order;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.annotation.Profile;
/**
* The {@link OrderEventStream} monitors for a variety of {@link OrderEvent} domain
* events for an {@link Order}.
*
* @author kbastani
*/
@EnableAutoConfiguration
@EnableBinding(Sink.class)
@Profile({ "cloud", "development" })
public class OrderEventStream {
private EventService eventService;
public OrderEventStream(EventService eventService) {
this.eventService = eventService;
}
@StreamListener(Sink.INPUT)
public void streamListerner(OrderEvent orderEvent) {
eventService.apply(orderEvent);
}
}

View File

@@ -0,0 +1,5 @@
package demo.event;
public enum OrderEventType {
// TODO: Add event types
}

View File

@@ -0,0 +1,7 @@
package demo.event;
import org.springframework.hateoas.Resources;
public class OrderEvents extends Resources<OrderEvent> {
}

View File

@@ -0,0 +1,5 @@
package demo.function;
public interface LambdaFunctions {
// TODO: Implement
}

View File

@@ -0,0 +1,51 @@
package demo.function;
import demo.order.Order;
import demo.order.OrderStatus;
import demo.event.OrderEvent;
import demo.event.OrderEventType;
import org.apache.log4j.Logger;
import org.springframework.statemachine.StateContext;
import java.util.function.Function;
/**
* The {@link OrderFunction} is an abstraction used to map actions that are triggered by
* state transitions on a {@link demo.order.Order} resource on to a function. Mapped functions
* can take multiple forms and reside either remotely or locally on the classpath of this application.
*
* @author kbastani
*/
public abstract class OrderFunction {
final private Logger log = Logger.getLogger(OrderFunction.class);
final protected StateContext<OrderStatus, OrderEventType> context;
final protected Function<OrderEvent, Order> lambda;
/**
* Create a new instance of a class that extends {@link OrderFunction}, supplying
* a state context and a lambda function used to apply {@link OrderEvent} to a provided
* action.
*
* @param context is the {@link StateContext} for a replicated state machine
* @param lambda is the lambda function describing an action that consumes an {@link OrderEvent}
*/
public OrderFunction(StateContext<OrderStatus, OrderEventType> context,
Function<OrderEvent, Order> lambda) {
this.context = context;
this.lambda = lambda;
}
/**
* Apply an {@link OrderEvent} to the lambda function that was provided through the
* constructor of this {@link OrderFunction}.
*
* @param event is the {@link OrderEvent} to apply to the lambda function
*/
public Order apply(OrderEvent event) {
// Execute the lambda function
Order result = lambda.apply(event);
context.getExtendedState().getVariables().put("order", result);
return result;
}
}

View File

@@ -0,0 +1,74 @@
package demo.order;
/**
* A simple domain class for the {@link LineItem} concept in the order context.
*
* @author Kenny Bastani
* @author Josh Long
*/
public class LineItem {
private String name, productId;
private Integer quantity;
private Double price, tax;
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,97 @@
package demo.order;
import demo.address.Address;
import demo.address.AddressType;
import demo.domain.BaseEntity;
import demo.event.OrderEvent;
import java.util.HashSet;
import java.util.Set;
public class Order extends BaseEntity {
private Long orderId;
private String accountNumber;
private OrderStatus status;
private Set<OrderEvent> events = new HashSet<>();
private Set<LineItem> lineItems = new HashSet<>();
private Address shippingAddress;
public Order() {
}
public Order(String accountNumber, Address shippingAddress) {
this();
this.accountNumber = accountNumber;
this.shippingAddress = shippingAddress;
if (shippingAddress.getAddressType() == null)
this.shippingAddress.setAddressType(AddressType.SHIPPING);
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long id) {
this.orderId = orderId;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public Set<OrderEvent> getEvents() {
return events;
}
public void setEvents(Set<OrderEvent> events) {
this.events = events;
}
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 void addLineItem(LineItem lineItem) {
lineItems.add(lineItem);
}
@Override
public String toString() {
return "Order{" +
"orderId='" + orderId + '\'' +
", accountNumber='" + accountNumber + '\'' +
", status=" + status +
", lineItems=" + lineItems +
", shippingAddress=" + shippingAddress +
"} " + super.toString();
}
}

View File

@@ -0,0 +1,14 @@
package demo.order;
/**
* Describes the state of an {@link Order}.
*
* @author Kenny Bastani
* @author Josh Long
*/
public enum OrderStatus {
PENDING,
CONFIRMED,
SHIPPED,
DELIVERED
}

View File

@@ -0,0 +1,42 @@
package demo.state;
import demo.order.OrderStatus;
import demo.event.OrderEventType;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* The {@link StateMachineService} provides factory access to get new state machines for
* replicating the state of an {@link demo.order.Order} from {@link demo.event.OrderEvents}.
*
* @author kbastani
*/
@Service
public class StateMachineService {
private final StateMachineFactory<OrderStatus, OrderEventType> factory;
public StateMachineService(StateMachineFactory<OrderStatus, OrderEventType> factory) {
this.factory = factory;
}
/**
* Create a new state machine that is initially configured and ready for replicating
* the state of an {@link demo.order.Order} from a sequence of {@link demo.event.OrderEvent}.
*
* @return a new instance of {@link StateMachine}
*/
public StateMachine<OrderStatus, OrderEventType> getStateMachine() {
// Create a new state machine in its initial state
StateMachine<OrderStatus, OrderEventType> stateMachine =
factory.getStateMachine(UUID.randomUUID().toString());
// Start the new state machine
stateMachine.start();
return stateMachine;
}
}

View File

@@ -0,0 +1,29 @@
package demo.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
@Component
public class LambdaUtil {
private ObjectMapper objectMapper;
public LambdaUtil(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public HashMap objectToMap(Object object) {
HashMap result = null;
try {
result = objectMapper.readValue(objectMapper.writeValueAsString(object), HashMap.class);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}

View File

@@ -0,0 +1,24 @@
spring:
profiles:
active: development
---
spring:
profiles: development
cloud:
stream:
bindings:
input:
destination: account
group: account-group
contentType: 'application/json'
consumer:
durableSubscription: true
server:
port: 8081
amazon:
aws:
access-key-id: replace
access-key-secret: replace
---
spring:
profiles: test

View File

@@ -0,0 +1,4 @@
spring:
application:
name: account-worker
---

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(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ActiveProfiles("test")
public class AccountStreamModuleApplicationTests {
@Test
public void contextLoads() {
}
}