finish 3rd practical
This commit is contained in:
20
AGENTS.md
20
AGENTS.md
@@ -1,20 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Build/Test Commands
|
||||
- Build: `gradle build`
|
||||
- Test all: `gradle test --rerun-tasks`
|
||||
- Run single test: `gradle test --tests "FreightIntegrationTest.getByIdReturnsEntity"`
|
||||
- Run app: `gradle bootRun`
|
||||
|
||||
## Code Style Guidelines
|
||||
- Package structure: `ua.com.dxrkness.{controller,service,repository,model}`
|
||||
- Use Spring Boot annotations (@RestController, @Service, etc.)
|
||||
- Records for models with Jackson @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
- Constructor injection for dependencies
|
||||
- JSpecify annotations (@NullMarked, @Nullable) for null safety
|
||||
- Integration tests with @SpringBootTest and parameterized tests
|
||||
- REST controllers support both JSON and XML media types
|
||||
- Use ResponseEntity for HTTP responses with proper status codes
|
||||
- OpenAPI documentation with Swagger annotations on controllers
|
||||
|
||||
# Use pdftotext tool to read PDF files!
|
||||
@@ -15,8 +15,6 @@ dependencies {
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.web.test)
|
||||
implementation(libs.spring.boot.starter.hateoas)
|
||||
developmentOnly(libs.spring.boot.devtools)
|
||||
|
||||
// http client
|
||||
implementation(libs.apache.http.client)
|
||||
@@ -24,15 +22,15 @@ dependencies {
|
||||
// openapi docs
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// xml
|
||||
implementation(libs.jackson.dataformat.xml)
|
||||
|
||||
// testing
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
implementation(libs.jspecify)
|
||||
|
||||
implementation(project(":models"))
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
@@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class LogisticsApplication {
|
||||
public class FreightServiceApp {
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(LogisticsApplication.class, args);
|
||||
SpringApplication.run(FreightServiceApp.class, args);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.service.FreightService;
|
||||
@@ -58,8 +57,8 @@ public class FreightController {
|
||||
description = "Freight not found"
|
||||
)
|
||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public Freight getById(@PathVariable("id") long id) {
|
||||
return service.getById(id);
|
||||
public List<Freight> getById(@PathVariable("id") long id) {
|
||||
return List.of(service.getById(id));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -1,6 +1,5 @@
|
||||
package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
@@ -21,8 +20,9 @@ public final class FreightService {
|
||||
}
|
||||
|
||||
public Freight add(Freight freight) {
|
||||
int id = repo.add(freight);
|
||||
return new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
||||
var newFreight = new Freight(repo.lastId(), freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
||||
repo.add(newFreight);
|
||||
return newFreight;
|
||||
}
|
||||
|
||||
public Freight getById(long id) {
|
||||
5
freight-service/src/main/resources/application.yaml
Normal file
5
freight-service/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: freight-service
|
||||
server:
|
||||
port: 8080
|
||||
@@ -4,6 +4,7 @@ spring-boot-plugin = "4.0.0"
|
||||
jspecify = "1.0.0"
|
||||
springdoc = "3.0.0"
|
||||
apache-http-client = "4.5.14"
|
||||
json-schema-validator = "3.0.0"
|
||||
|
||||
[plugins]
|
||||
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
|
||||
@@ -16,17 +17,20 @@ spring-boot-starter-hateoas = { group = "org.springframework.boot", name = "spri
|
||||
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
|
||||
|
||||
# http client
|
||||
apache-http-client = { group = "org.apache.httpcomponents", name = "httpclient", version.ref ="apache-http-client" }
|
||||
apache-http-client = { group = "org.apache.httpcomponents", name = "httpclient", version.ref = "apache-http-client" }
|
||||
|
||||
# openapi
|
||||
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
|
||||
|
||||
# xml
|
||||
# jackson
|
||||
jackson-dataformat-xml = { group = "tools.jackson.dataformat", name = "jackson-dataformat-xml" }
|
||||
oldjackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind" }
|
||||
|
||||
# testing
|
||||
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
|
||||
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
|
||||
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
||||
|
||||
jspecify = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" }
|
||||
jspecify = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" }
|
||||
|
||||
json-schema-validator = { group = 'com.networknt', name = 'json-schema-validator', version.ref = 'json-schema-validator' }
|
||||
20
models/build.gradle.kts
Normal file
20
models/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
alias(libs.plugins.spring.boot)
|
||||
}
|
||||
|
||||
group = "ua.com.dxrkness"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// spring
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
|
||||
// jackson
|
||||
api(libs.jackson.dataformat.xml)
|
||||
implementation(libs.oldjackson.databind)
|
||||
}
|
||||
92
populate.fish
Executable file
92
populate.fish
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
function publish_correct
|
||||
set vehicle '{
|
||||
"id": 444,
|
||||
"brand": "test vehicle",
|
||||
"model": "test vehicle",
|
||||
"license_plate": "AB4444AB",
|
||||
"year": 1980,
|
||||
"capacity_kg": 3321,
|
||||
"status": "AVAILABLE"
|
||||
}'
|
||||
|
||||
set freight1 '{
|
||||
"id": 444,
|
||||
"name": "test freight",
|
||||
"description": "test freight",
|
||||
"weight_kg": 444,
|
||||
"dimensions": {
|
||||
"width_cm": 4,
|
||||
"height_cm": 4,
|
||||
"length_cm": 4
|
||||
},
|
||||
"status": "PENDING"
|
||||
}'
|
||||
|
||||
set freight2 '{
|
||||
"id": 555,
|
||||
"name": "test freight 2",
|
||||
"description": "test freight 2",
|
||||
"weight_kg": 555,
|
||||
"dimensions": {
|
||||
"width_cm": 8,
|
||||
"height_cm": 8,
|
||||
"length_cm": 8
|
||||
},
|
||||
"status": "IN_TRANSIT"
|
||||
}'
|
||||
|
||||
set route '{
|
||||
"id": 444,
|
||||
"vehicle_id": 1,
|
||||
"freight_id": [
|
||||
1, 2
|
||||
],
|
||||
"start_location": "string",
|
||||
"end_location": "string",
|
||||
"distance_km": 0.1,
|
||||
"estimated_duration_hours": 0.1,
|
||||
"status": "PLANNED"
|
||||
}'
|
||||
|
||||
printf '\n\n'
|
||||
curl -X POST http://localhost:8081/vehicles \
|
||||
-H "Content-Type: application/json" \
|
||||
-d $vehicle
|
||||
|
||||
printf '\n\n'
|
||||
curl -X POST http://localhost:8080/freights \
|
||||
-H "Content-Type: application/json" \
|
||||
-d $freight1
|
||||
|
||||
printf '\n\n'
|
||||
curl -X POST http://localhost:8080/freights \
|
||||
-H "Content-Type: application/json" \
|
||||
-d $freight2
|
||||
|
||||
printf '\n\n'
|
||||
curl -X POST http://localhost:8082/routes \
|
||||
-H "Content-Type: application/json" \
|
||||
-d $route
|
||||
end
|
||||
|
||||
|
||||
function add_invalid_route
|
||||
set route '{
|
||||
"id": 444,
|
||||
"vehicle_id": 444,
|
||||
"freight_id": [
|
||||
1, 2
|
||||
],
|
||||
"start_location": "string",
|
||||
"end_location": "string",
|
||||
"distance_km": 0.1,
|
||||
"estimated_duration_hours": 0.1,
|
||||
"status": "PLANNED"
|
||||
}'
|
||||
|
||||
curl -X POST http://localhost:8082/routes \
|
||||
-H "Content-Type: application/json" \
|
||||
-d $route
|
||||
end
|
||||
38
route-service/build.gradle.kts
Normal file
38
route-service/build.gradle.kts
Normal file
@@ -0,0 +1,38 @@
|
||||
plugins {
|
||||
id("java")
|
||||
alias(libs.plugins.spring.boot)
|
||||
}
|
||||
|
||||
group = "ua.com.dxrkness"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// spring
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.web.test)
|
||||
|
||||
// http client
|
||||
implementation(libs.apache.http.client)
|
||||
|
||||
// openapi docs
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// testing
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
implementation(libs.jspecify)
|
||||
|
||||
implementation(project(":models"))
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ua.com.dxrkness;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class RouteServiceApp {
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(RouteServiceApp.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package ua.com.dxrkness.client;
|
||||
|
||||
import com.networknt.schema.InputFormat;
|
||||
import com.networknt.schema.Schema;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class FreightClient {
|
||||
private final RestClient freightRestClient;
|
||||
private final Schema freightSchema;
|
||||
private final JsonMapper mapper = JsonMapper.shared();
|
||||
|
||||
public FreightClient(RestClient freightRestClient, Schema freightSchema) {
|
||||
this.freightRestClient = freightRestClient;
|
||||
this.freightSchema = freightSchema;
|
||||
}
|
||||
|
||||
public Freight getById(long id) {
|
||||
var resp = freightRestClient.get().uri("/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
|
||||
throw new ResponseStatusException(res.getStatusCode(), res.getStatusText());
|
||||
})
|
||||
.body(String.class);
|
||||
System.out.println(resp);
|
||||
List<com.networknt.schema.Error> errs = freightSchema.validate(resp, InputFormat.JSON);
|
||||
if (!errs.isEmpty()) {
|
||||
System.out.println("Some errors have occured!");
|
||||
errs.forEach(err -> {
|
||||
System.out.print(err);
|
||||
});
|
||||
}
|
||||
return mapper.readValue(resp, new TypeReference<List<Freight>>() {
|
||||
}).getFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package ua.com.dxrkness.client;
|
||||
|
||||
import com.networknt.schema.Error;
|
||||
import com.networknt.schema.InputFormat;
|
||||
import com.networknt.schema.Schema;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class VehicleClient {
|
||||
private final RestClient vehicleRestClient;
|
||||
private final Schema vehicleSchema;
|
||||
private final JsonMapper mapper = JsonMapper.shared();
|
||||
|
||||
public VehicleClient(RestClient vehicleRestClient, Schema vehicleSchema) {
|
||||
this.vehicleRestClient = vehicleRestClient;
|
||||
this.vehicleSchema = vehicleSchema;
|
||||
}
|
||||
|
||||
public Vehicle getById(long id) {
|
||||
var resp = vehicleRestClient.get().uri("/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
|
||||
throw new ResponseStatusException(res.getStatusCode(), res.getStatusText());
|
||||
})
|
||||
.body(String.class);
|
||||
System.out.println(resp);
|
||||
List<Error> errs = vehicleSchema.validate(resp, InputFormat.JSON);
|
||||
if (!errs.isEmpty()) {
|
||||
System.out.println("Some errors have occured!");
|
||||
errs.forEach(err -> {
|
||||
System.out.print(err);
|
||||
});
|
||||
}
|
||||
return mapper.readValue(resp, new TypeReference<List<Vehicle>>() {
|
||||
}).getFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ua.com.dxrkness.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@Configuration
|
||||
public class ClientConfiguration {
|
||||
@Bean
|
||||
public RestClient vehicleRestClient() {
|
||||
return RestClient.create("http://localhost:8081/vehicles");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestClient freightRestClient() {
|
||||
return RestClient.create("http://localhost:8080/freights");
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@ import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ua.com.dxrkness.client.FreightClient;
|
||||
import ua.com.dxrkness.client.VehicleClient;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
import ua.com.dxrkness.service.FreightService;
|
||||
import ua.com.dxrkness.service.RouteService;
|
||||
import ua.com.dxrkness.service.VehicleService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -28,15 +28,15 @@ import java.util.List;
|
||||
@NullMarked
|
||||
public class RouteController {
|
||||
private final RouteService routeService;
|
||||
private final VehicleService vehicleService;
|
||||
private final FreightService freightService;
|
||||
private final VehicleClient vehicleClient;
|
||||
private final FreightClient freightClient;
|
||||
|
||||
public RouteController(RouteService routeService,
|
||||
VehicleService vehicleService,
|
||||
FreightService freightService) {
|
||||
VehicleClient vehicleClient,
|
||||
FreightClient freightClient) {
|
||||
this.routeService = routeService;
|
||||
this.vehicleService = vehicleService;
|
||||
this.freightService = freightService;
|
||||
this.vehicleClient = vehicleClient;
|
||||
this.freightClient = freightClient;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -105,7 +105,7 @@ public class RouteController {
|
||||
public Vehicle getVehicleById(
|
||||
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return vehicleService.getById(routeService.getById(id).vehicleId());
|
||||
return vehicleClient.getById(routeService.getById(id).vehicleId());
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -126,7 +126,7 @@ public class RouteController {
|
||||
var route = routeService.getById(id);
|
||||
var freights = new ArrayList<Freight>();
|
||||
for (var freightId : route.freightId()) {
|
||||
freights.add(freightService.getById(freightId));
|
||||
freights.add(freightClient.getById(freightId));
|
||||
}
|
||||
return freights;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.client.FreightClient;
|
||||
import ua.com.dxrkness.client.VehicleClient;
|
||||
import ua.com.dxrkness.dto.FreightDto;
|
||||
import ua.com.dxrkness.dto.VehicleDto;
|
||||
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||
@@ -15,15 +17,15 @@ import java.util.List;
|
||||
@Service
|
||||
public final class RouteService {
|
||||
private final RouteRepository repo;
|
||||
private final VehicleService vehicleService;
|
||||
private final FreightService freightService;
|
||||
private final VehicleClient vehicleClient;
|
||||
private final FreightClient freightClient;
|
||||
|
||||
public RouteService(RouteRepository repo,
|
||||
VehicleService vehicleService,
|
||||
FreightService freightService) {
|
||||
VehicleClient vehicleClient,
|
||||
FreightClient freightClient) {
|
||||
this.repo = repo;
|
||||
this.vehicleService = vehicleService;
|
||||
this.freightService = freightService;
|
||||
this.vehicleClient = vehicleClient;
|
||||
this.freightClient = freightClient;
|
||||
}
|
||||
|
||||
public List<Route> getAll() {
|
||||
@@ -42,8 +44,7 @@ public final class RouteService {
|
||||
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
|
||||
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
|
||||
|
||||
int id = repo.add(route);
|
||||
return new Route(id,
|
||||
var newRoute = new Route(repo.lastId(),
|
||||
route.vehicleId(),
|
||||
route.freightId(),
|
||||
route.startLocation(),
|
||||
@@ -51,11 +52,13 @@ public final class RouteService {
|
||||
route.distanceKm(),
|
||||
route.estimatedDurationHours(),
|
||||
route.status());
|
||||
repo.add(route);
|
||||
return newRoute;
|
||||
}
|
||||
|
||||
private VehicleDto validateAndGetVehicle(long vehicleId) {
|
||||
try {
|
||||
var vehicle = vehicleService.getById(vehicleId);
|
||||
var vehicle = vehicleClient.getById(vehicleId);
|
||||
return VehicleDto.fromVehicle(vehicle);
|
||||
} catch (VehicleNotFoundException e) {
|
||||
throw new VehicleNotFoundException(
|
||||
@@ -67,7 +70,7 @@ public final class RouteService {
|
||||
List<FreightDto> freightDtos = new java.util.ArrayList<>();
|
||||
for (long freightId : freightIds) {
|
||||
try {
|
||||
var freight = freightService.getById(freightId);
|
||||
var freight = freightClient.getById(freightId);
|
||||
freightDtos.add(FreightDto.fromFreight(freight));
|
||||
} catch (FreightNotFoundException e) {
|
||||
throw new FreightNotFoundException(
|
||||
5
route-service/src/main/resources/application.yaml
Normal file
5
route-service/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: route-service
|
||||
server:
|
||||
port: 8082
|
||||
@@ -1,2 +1,4 @@
|
||||
rootProject.name = "itroi"
|
||||
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
||||
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
||||
|
||||
include("models", "vehicle-service", "route-service", "freight-service", "shared")
|
||||
35
shared/build.gradle.kts
Normal file
35
shared/build.gradle.kts
Normal file
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
alias(libs.plugins.spring.boot)
|
||||
}
|
||||
|
||||
group = "ua.com.dxrkness"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// spring
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
// http client
|
||||
implementation(libs.apache.http.client)
|
||||
|
||||
// xml
|
||||
implementation(libs.jackson.dataformat.xml)
|
||||
|
||||
implementation(libs.jspecify)
|
||||
|
||||
implementation(project(":models"))
|
||||
|
||||
api(libs.json.schema.validator);
|
||||
|
||||
implementation("org.jruby.joni:joni:2.2.6")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ua.com.dxrkness.config;
|
||||
|
||||
import com.networknt.schema.*;
|
||||
import com.networknt.schema.regex.JoniRegularExpressionFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JsonSchemaConfiguration {
|
||||
@Bean
|
||||
SchemaRegistry schemaRegistry() {
|
||||
var schemaRegistryConfig = SchemaRegistryConfig.builder()
|
||||
.regularExpressionFactory(JoniRegularExpressionFactory.getInstance())
|
||||
.failFast(true).build();
|
||||
|
||||
return SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
|
||||
builder -> builder.schemaRegistryConfig(schemaRegistryConfig)
|
||||
/*
|
||||
* This creates a mapping from $id which starts with
|
||||
* https://www.example.org/schema to the retrieval IRI classpath:schema.
|
||||
*/
|
||||
.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers
|
||||
.mapPrefix("https://nure.ua/itroi", "classpath:json-schema")));
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Schema vehicleSchema(SchemaRegistry schemaRegistry) {
|
||||
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/vehicle-schema.json"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Schema routeSchema(SchemaRegistry schemaRegistry) {
|
||||
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/route-schema.json"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Schema freightSchema(SchemaRegistry schemaRegistry) {
|
||||
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/freight-schema.json"));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||
import ua.com.dxrkness.exception.RouteNotFoundException;
|
||||
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||
@@ -28,6 +29,11 @@ public class GlobalExceptionHandler {
|
||||
return constructExceptionBody(HttpStatus.BAD_REQUEST, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ErrorResponse handleResponseStatusException(ResponseStatusException ex) {
|
||||
return ex;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ErrorResponse handleGeneralException(Exception ex) {
|
||||
return constructExceptionBody(HttpStatus.INTERNAL_SERVER_ERROR, ex);
|
||||
@@ -5,9 +5,11 @@ import ua.com.dxrkness.model.Identifiable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
abstract class CrudRepository<T extends Identifiable> {
|
||||
protected final List<T> STORAGE = new ArrayList<>();
|
||||
private final AtomicLong id = new AtomicLong(0);
|
||||
|
||||
public List<T> getAll() {
|
||||
return new ArrayList<>(STORAGE); // defensive copy
|
||||
@@ -55,4 +57,8 @@ abstract class CrudRepository<T extends Identifiable> {
|
||||
public void deleteAll() {
|
||||
STORAGE.clear();
|
||||
}
|
||||
|
||||
public long lastId() {
|
||||
return id.incrementAndGet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
|
||||
|
||||
@Service
|
||||
@NullMarked
|
||||
public class PopulateService {
|
||||
|
||||
public PopulateService() {
|
||||
}
|
||||
|
||||
public void populate(int vals){
|
||||
for (int i = 0; i < vals; i++) {
|
||||
var f = new Freight(i, "", "", 0, null, null);
|
||||
// freightRepository.add(f);
|
||||
}
|
||||
|
||||
for (int i = 0; i < vals; i++) {
|
||||
var v = new Vehicle(i, "", "", "", 0, 0, null);
|
||||
// vehicleRepository.add(v);
|
||||
}
|
||||
|
||||
for (int i = 0; i < vals; i++) {}
|
||||
// routeRepository.add(new Route(i, i, List.of((long)i), "", "", 0., 0., null));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
// routeRepository.deleteAll();
|
||||
// freightRepository.deleteAll();
|
||||
// vehicleRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
70
shared/src/main/resources/json-schema/freight-schema.json
Normal file
70
shared/src/main/resources/json-schema/freight-schema.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://nure.ua/itroi/freight-schema.json",
|
||||
"title": "Freight management schema",
|
||||
"description": "Describes domain objects of freights",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 128
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"weight_kg": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 4294967296
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width_cm": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 150000
|
||||
},
|
||||
"height_cm": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 150000
|
||||
},
|
||||
"length_cm": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 150000
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"width_cm",
|
||||
"height_cm",
|
||||
"length_cm"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PENDING",
|
||||
"IN_TRANSIT",
|
||||
"DELIVERED"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"weight_kg",
|
||||
"dimensions",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
58
shared/src/main/resources/json-schema/route-schema.json
Normal file
58
shared/src/main/resources/json-schema/route-schema.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://nure.ua/itroi/route-schema.json",
|
||||
"title": "Routes management schema",
|
||||
"description": "Describes domain objects of routes",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"vehicle_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"freight_id": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"start_location": {
|
||||
"type": "string"
|
||||
},
|
||||
"end_location": {
|
||||
"type": "string"
|
||||
},
|
||||
"distance_km": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"estimated_duration_hours": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PLANNED",
|
||||
"IN_PROGRESS",
|
||||
"COMPLETED",
|
||||
"CANCELLED"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"vehicle_id",
|
||||
"freight_id",
|
||||
"start_location",
|
||||
"end_location",
|
||||
"distance_km",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
59
shared/src/main/resources/json-schema/vehicle-schema.json
Normal file
59
shared/src/main/resources/json-schema/vehicle-schema.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://nure.ua/itroi/vehicle-schema.json",
|
||||
"title": "Vehicles management schema",
|
||||
"description": "Describes domain objects of vehicles",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"brand": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"maxLength": 64
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128
|
||||
},
|
||||
"license_plate": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{2}[0-9]{4}[A-Z]{2}$",
|
||||
"minLength": 8,
|
||||
"maxLength": 8
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"minimum": 1980
|
||||
},
|
||||
"capacity_kg": {
|
||||
"type": "number",
|
||||
"minimum": 100,
|
||||
"exclusiveMaximum": 4294967296
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"AVAILABLE",
|
||||
"IN_TRANSIT",
|
||||
"MAINTENANCE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"brand",
|
||||
"model",
|
||||
"license_plate",
|
||||
"year",
|
||||
"capacity_kg",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,8 @@ class InterServiceCommunicationTest {
|
||||
.expectBody(String.class)
|
||||
.consumeWith(res -> {
|
||||
String body = res.getResponseBody();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found").contains("vehicle");
|
||||
BDDAssertions.then(body).isNotNull();
|
||||
BDDAssertions.then(body).contains("Not Found").contains("vehicle");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,8 +115,8 @@ class InterServiceCommunicationTest {
|
||||
.expectBody(String.class)
|
||||
.consumeWith(res -> {
|
||||
String body = res.getResponseBody();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found").contains("freight");
|
||||
BDDAssertions.then(body).isNotNull();
|
||||
BDDAssertions.then(body).contains("Not Found").contains("freight");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,15 +7,11 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.service.PopulateService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
@@ -64,8 +60,8 @@ class SubResourcesAndFilteringTest {
|
||||
.expectBody(String.class)
|
||||
.consumeWith(res -> {
|
||||
String body = res.getResponseBody();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found");
|
||||
BDDAssertions.then(body).isNotNull();
|
||||
BDDAssertions.then(body).contains("Not Found");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,8 +81,8 @@ class SubResourcesAndFilteringTest {
|
||||
.expectBody(String.class)
|
||||
.consumeWith(res -> {
|
||||
String body = res.getResponseBody();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found");
|
||||
BDDAssertions.then(body).isNotNull();
|
||||
BDDAssertions.then(body).contains("Not Found");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,8 +102,8 @@ class SubResourcesAndFilteringTest {
|
||||
.expectBody(String.class)
|
||||
.consumeWith(res -> {
|
||||
String body = res.getResponseBody();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found");
|
||||
BDDAssertions.then(body).isNotNull();
|
||||
BDDAssertions.then(body).contains("Not Found");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
@NullMarked
|
||||
package ua.com.dxrkness;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -1,48 +0,0 @@
|
||||
package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
import ua.com.dxrkness.repository.FreightRepository;
|
||||
import ua.com.dxrkness.repository.RouteRepository;
|
||||
import ua.com.dxrkness.repository.VehicleRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@NullMarked
|
||||
public class PopulateService {
|
||||
private final FreightRepository freightRepository;
|
||||
private final RouteRepository routeRepository;
|
||||
private final VehicleRepository vehicleRepository;
|
||||
|
||||
public PopulateService(FreightRepository freightRepository, RouteRepository routeRepository, VehicleRepository vehicleRepository) {
|
||||
this.freightRepository = freightRepository;
|
||||
this.routeRepository = routeRepository;
|
||||
this.vehicleRepository = vehicleRepository;
|
||||
}
|
||||
|
||||
public void populate(int vals){
|
||||
for (int i = 0; i < vals; i++) {
|
||||
var f = new Freight(i, "", "", 0, null, null);
|
||||
freightRepository.add(f);
|
||||
}
|
||||
|
||||
for (int i = 0; i < vals; i++) {
|
||||
var v = new Vehicle(i, "", "", "", 0, 0, null);
|
||||
vehicleRepository.add(v);
|
||||
}
|
||||
|
||||
for (int i = 0; i < vals; i++)
|
||||
routeRepository.add(new Route(i, i, List.of((long)i), "", "", 0., 0., null));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
routeRepository.deleteAll();
|
||||
freightRepository.deleteAll();
|
||||
vehicleRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
spring:
|
||||
application:
|
||||
name: itroi
|
||||
38
vehicle-service/build.gradle.kts
Normal file
38
vehicle-service/build.gradle.kts
Normal file
@@ -0,0 +1,38 @@
|
||||
plugins {
|
||||
id("java")
|
||||
alias(libs.plugins.spring.boot)
|
||||
}
|
||||
|
||||
group = "ua.com.dxrkness"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// spring
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.web.test)
|
||||
|
||||
// http client
|
||||
implementation(libs.apache.http.client)
|
||||
|
||||
// openapi docs
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// testing
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
implementation(libs.jspecify)
|
||||
|
||||
implementation(project(":models"))
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ua.com.dxrkness;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class VehicleServiceApp {
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(VehicleServiceApp.class, args);
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,10 @@ public class VehicleController {
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle found")
|
||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public Vehicle getById(
|
||||
public List<Vehicle> getById(
|
||||
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return service.getById(id);
|
||||
return List.of(service.getById(id));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -26,8 +26,9 @@ public final class VehicleService {
|
||||
}
|
||||
|
||||
public Vehicle add(Vehicle veh) {
|
||||
int id = repo.add(veh);
|
||||
return new Vehicle(id, veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
|
||||
var newVehicle = new Vehicle(repo.lastId(), veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
|
||||
repo.add(newVehicle);
|
||||
return newVehicle;
|
||||
}
|
||||
|
||||
public Vehicle getById(long id) {
|
||||
5
vehicle-service/src/main/resources/application.yaml
Normal file
5
vehicle-service/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: vehicle-service
|
||||
server:
|
||||
port: 8081
|
||||
Reference in New Issue
Block a user