spring boot application
This commit is contained in:
@@ -8,8 +8,8 @@ RUN apt-get update && apt-get -y install maven
|
||||
WORKDIR /opt/shipping
|
||||
|
||||
COPY pom.xml /opt/shipping/
|
||||
RUN mvn dependency:resolve
|
||||
COPY src /opt/shipping/src/
|
||||
RUN mvn compile
|
||||
RUN mvn package
|
||||
|
||||
#
|
||||
|
@@ -1,23 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
shipping:
|
||||
build:
|
||||
context: .
|
||||
image: foo
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- mysql
|
||||
networks:
|
||||
- test
|
||||
|
||||
mysql:
|
||||
image: robotshop/rs-mysql-db
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
networks:
|
||||
- test
|
||||
|
||||
|
||||
networks:
|
||||
test:
|
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.instana</groupId>
|
||||
<artifactId>shipping</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>1.0</version>
|
||||
<name>shipping service</name>
|
||||
<description>Shipping calculations</description>
|
||||
|
||||
@@ -27,12 +27,26 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.12</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
|
||||
public class Calculator {
|
||||
private double latitude = 0;
|
||||
private double longitude = 0;
|
||||
|
||||
Calculator(double latitdue, double longitude) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
Calculator(City city) {
|
||||
this.latitude = city.getLatitude();
|
||||
this.longitude = city.getLongitude();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between this location and the target location.
|
||||
* Use decimal lat/long degrees
|
||||
* Formula is Haversine https://www.movable-type.co.uk/scripts/latlong.html
|
||||
**/
|
||||
public long getDistance(double targetLatitude, double targetLongitude) {
|
||||
double distance = 0.0;
|
||||
double earthRadius = 6371e3; // meters
|
||||
|
||||
// convert to radians
|
||||
double latitudeR = Math.toRadians(this.latitude);
|
||||
double targetLatitudeR = Math.toRadians(targetLatitude);
|
||||
// difference in Radians
|
||||
double diffLatR = Math.toRadians(targetLatitude - this.latitude);
|
||||
double diffLongR = Math.toRadians(targetLongitude - this.longitude);
|
||||
|
||||
double a = Math.sin(diffLatR / 2.0) * Math.sin(diffLatR / 2.0)
|
||||
+ Math.cos(latitudeR) * Math.cos(targetLatitudeR)
|
||||
* Math.sin(diffLongR / 2.0) * Math.sin(diffLongR);
|
||||
|
||||
double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a));
|
||||
|
||||
return (long)Math.rint(earthRadius * c / 1000.0);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,75 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
public class CartHelper {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CartHelper.class);
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
public CartHelper(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
// TODO - Remove deprecated calls
|
||||
public String addToCart(String id, String data) {
|
||||
logger.info("add shipping to cart {}", id);
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
|
||||
CloseableHttpClient httpClient = null;
|
||||
try {
|
||||
// set timeout to 5 secs
|
||||
HttpParams httpParams = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
|
||||
|
||||
httpClient = HttpClients.createDefault();
|
||||
HttpPost postRequest = new HttpPost(baseUrl + id);
|
||||
StringEntity payload = new StringEntity(data);
|
||||
payload.setContentType("application/json");
|
||||
postRequest.setEntity(payload);
|
||||
CloseableHttpResponse res = httpClient.execute(postRequest);
|
||||
|
||||
if (res.getStatusLine().getStatusCode() == 200) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(res.getEntity().getContent()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
buffer.append(line);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Failed with code {}", res.getStatusLine().getStatusCode());
|
||||
}
|
||||
try {
|
||||
res.close();
|
||||
} catch(IOException e) {
|
||||
logger.warn("httpresponse", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.warn("http client exception", e);
|
||||
} finally {
|
||||
if (httpClient != null) {
|
||||
try {
|
||||
httpClient.close();
|
||||
} catch(IOException e) {
|
||||
logger.warn("httpclient", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this will be empty on error
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
@@ -1,20 +1,24 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Column;
|
||||
|
||||
/*
|
||||
* Bean for City
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "cities")
|
||||
public class City {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long id;
|
||||
private long uuid;
|
||||
|
||||
@Column(name = "country_code")
|
||||
private String code;
|
||||
private String city;
|
||||
private String name;
|
||||
@@ -22,6 +26,10 @@ public class City {
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
|
||||
public long getUuid() {
|
||||
return this.uuid;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
@@ -3,9 +3,15 @@ package com.instana.robotshop.shipping;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface CityRepository extends CrudRepository<City, Long> {
|
||||
List<City> findByCode(String code);
|
||||
|
||||
@Query(
|
||||
value = "select c from City c where c.code = ?1 and c.city like ?2%"
|
||||
)
|
||||
List<City> match(String code, String text);
|
||||
|
||||
City findById(long id);
|
||||
}
|
||||
|
@@ -0,0 +1,47 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/*
|
||||
* Bean for Code
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "codes")
|
||||
public class Code {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long uuid;
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
public long getUuid() {
|
||||
return this.uuid;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Code: %s Name: %s", this.code, this.name);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
public interface CodeRepository extends PagingAndSortingRepository<Code, Long> {
|
||||
|
||||
Iterable<Code> findAll();
|
||||
|
||||
Code findById(long id);
|
||||
}
|
@@ -1,22 +1,126 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@RestController
|
||||
public class Controller {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Controller.class);
|
||||
|
||||
@GetMapping("/cities")
|
||||
public List<City> cities(@RequestParam(value = "code") String code) {
|
||||
private String CART_URL = String.format("http://%s/shipping/", getenv("CART_ENDPOINT", "cart"));
|
||||
|
||||
@Autowired
|
||||
private CityRepository cityrepo;
|
||||
|
||||
@Autowired
|
||||
private CodeRepository coderepo;
|
||||
|
||||
private String getenv(String key, String def) {
|
||||
String val = System.getenv(key);
|
||||
val = val == null ? def : val;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public String health() {
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@GetMapping("/count")
|
||||
public String count() {
|
||||
long count = cityrepo.count();
|
||||
|
||||
return String.valueOf(count);
|
||||
}
|
||||
|
||||
@GetMapping("/codes")
|
||||
public Iterable<Code> codes() {
|
||||
logger.info("all codes");
|
||||
|
||||
Iterable<Code> codes = coderepo.findAll(Sort.by(Sort.Direction.ASC, "name"));
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
@GetMapping("/cities/{code}")
|
||||
public List<City> cities(@PathVariable String code) {
|
||||
logger.info("cities by code {}", code);
|
||||
|
||||
return new <City>ArrayList();
|
||||
List<City> cities = cityrepo.findByCode(code);
|
||||
|
||||
return cities;
|
||||
}
|
||||
|
||||
@GetMapping("/match/{code}/{text}")
|
||||
public List<City> match(@PathVariable String code, @PathVariable String text) {
|
||||
logger.info("match code {} text {}", code, text);
|
||||
|
||||
if (text.length() < 3) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
List<City> cities = cityrepo.match(code, text);
|
||||
/*
|
||||
* This is a dirty hack to limit the result size
|
||||
* I'm sure there is a more spring boot way to do this
|
||||
* TODO - neater
|
||||
*/
|
||||
if (cities.size() > 10) {
|
||||
cities = cities.subList(0, 9);
|
||||
}
|
||||
|
||||
return cities;
|
||||
}
|
||||
|
||||
@GetMapping("/calc/{id}")
|
||||
public Ship caclc(@PathVariable long id) {
|
||||
double homeLatitude = 51.164896;
|
||||
double homeLongitude = 7.068792;
|
||||
|
||||
logger.info("Calculation for {}", id);
|
||||
|
||||
City city = cityrepo.findById(id);
|
||||
if (city == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "city not found");
|
||||
}
|
||||
|
||||
Calculator calc = new Calculator(city);
|
||||
long distance = calc.getDistance(homeLatitude, homeLongitude);
|
||||
// avoid rounding
|
||||
double cost = Math.rint(distance * 5) / 100.0;
|
||||
Ship ship = new Ship(distance, cost);
|
||||
logger.info("shipping {}", ship);
|
||||
|
||||
return ship;
|
||||
}
|
||||
|
||||
// enforce content type
|
||||
@PostMapping(path = "/confirm/{id}", consumes = "application/json", produces = "application/json")
|
||||
public String confirm(@PathVariable String id, @RequestBody String body) {
|
||||
logger.info("confirm id: {}", id);
|
||||
logger.info("body {}", body);
|
||||
|
||||
CartHelper helper = new CartHelper(CART_URL);
|
||||
String cart = helper.addToCart(id, body);
|
||||
|
||||
if (cart.equals("")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "cart not found");
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.jdbc.datasource.AbstractDataSource;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
|
||||
class RetryableDataSource extends AbstractDataSource {
|
||||
private DataSource delegate;
|
||||
|
||||
public RetryableDataSource(DataSource delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(maxAttempts = 10, backoff = @Backoff(multiplier = 2.3, maxDelay = 30000))
|
||||
public Connection getConnection() throws SQLException {
|
||||
return delegate.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(maxAttempts = 10, backoff = @Backoff(multiplier = 2.3, maxDelay = 30000))
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
return delegate.getConnection(username, password);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,41 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
/**
|
||||
* Bean to hold shipping information
|
||||
**/
|
||||
public class Ship {
|
||||
private long distance;
|
||||
private double cost;
|
||||
|
||||
public Ship() {
|
||||
this.distance = 0;
|
||||
this.cost = 0.0;
|
||||
}
|
||||
|
||||
public Ship(long distance, double cost) {
|
||||
this.distance = distance;
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
public void setDistance(long distance) {
|
||||
this.distance = distance;
|
||||
}
|
||||
|
||||
public void setCost(double cost) {
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
return this.distance;
|
||||
}
|
||||
|
||||
public double getCost() {
|
||||
return this.cost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Distance: %d Cost: %f", distance, cost);
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,43 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableRetry
|
||||
public class ShippingServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ShippingServiceApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BeanPostProcessor dataSourceWrapper() {
|
||||
return new DataSourcePostProcessor();
|
||||
}
|
||||
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
private class DataSourcePostProcessor implements BeanPostProcessor {
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
|
||||
if (bean instanceof DataSource) {
|
||||
bean = new RetryableDataSource((DataSource)bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1,6 @@
|
||||
spring.jmx.enabled=true
|
||||
|
||||
management.endpoint.info.enabled=true
|
||||
management.endpoint.health.enabled=true
|
||||
management.endpoint.metrics.enabled=true
|
||||
management.endpoint.env.enabled=true
|
||||
|
Reference in New Issue
Block a user