spring boot application

This commit is contained in:
Steve Waterworth
2020-09-01 16:30:35 +01:00
parent 7c3ffda0cb
commit f0b49233a3
14 changed files with 422 additions and 30 deletions

View File

@@ -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
#

View File

@@ -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:

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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