2 Commits

Author SHA256 Message Date
dxrknesss 4f1fa86833 finish 4th practical 2025-12-21 18:14:27 +02:00
dxrknesss d203544a6f finish 3rd practical 2025-12-17 12:37:39 +02:00
67 changed files with 1212 additions and 651 deletions
+24
View File
@@ -0,0 +1,24 @@
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(platform(libs.spring.cloud.dependencies))
implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webflux")
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,11 @@
package ua.com.dxrkness.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
@@ -0,0 +1,18 @@
server:
port: 8079
spring:
cloud:
gateway:
server:
webflux:
discovery:
locator:
enabled: true
lower-case-service-id: true
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:8070/eureka
register-with-eureka: false
+22
View File
@@ -0,0 +1,22 @@
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(platform(libs.spring.cloud.dependencies))
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,13 @@
package ua.com.dxrkness;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
@@ -0,0 +1,11 @@
server:
port: 8070
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
@@ -13,10 +13,10 @@ repositories {
dependencies {
// spring
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation(platform(libs.spring.cloud.dependencies))
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
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 +24,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);
}
}
@@ -7,8 +7,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import ua.com.dxrkness.dto.FreightRequest;
import ua.com.dxrkness.dto.FreightResponse;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.service.FreightService;
@@ -41,9 +39,9 @@ public class FreightController {
@Parameter(description = "Filter freights by status (PENDING, IN_TRANSIT, DELIVERED)")
@RequestParam(name = "status", required = false) Freight.@Nullable Status status) {
if (status != null) {
return service.getByStatus(status).stream().map(FreightResponse::toEntity).toList();
return service.getByStatus(status);
}
return service.getAll().stream().map(FreightResponse::toEntity).toList();
return service.getAll();
}
@Operation(
@@ -59,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).toEntity();
public List<Freight> getById(@PathVariable("id") long id) {
return List.of(service.getById(id));
}
@Operation(
@@ -76,8 +74,8 @@ public class FreightController {
description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
)
@PostMapping
public Freight add(@RequestBody FreightRequest newFreight) {
return service.add(newFreight).toEntity();
public Freight add(@RequestBody Freight newFreight) {
return service.add(newFreight);
}
@Operation(
@@ -93,8 +91,8 @@ public class FreightController {
description = "Invalid input"
)
@PutMapping("/{id}")
public Freight update(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
return service.update(id, newFreight).toEntity();
public Freight update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return service.update(id, newFreight);
}
@Operation(
@@ -110,8 +108,8 @@ public class FreightController {
description = "Invalid input"
)
@PatchMapping("/{id}")
public Freight updatePatch(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
return service.update(id, newFreight).toEntity();
public Freight updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return service.update(id, newFreight);
}
@Operation(
@@ -128,6 +126,6 @@ public class FreightController {
)
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public Freight delete(@PathVariable("id") long id) {
return service.delete(id).toEntity();
return service.delete(id);
}
}
@@ -0,0 +1,54 @@
package ua.com.dxrkness.service;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.repository.FreightRepository;
import java.util.List;
@Service
public final class FreightService {
private final FreightRepository repo;
public FreightService(FreightRepository repo) {
this.repo = repo;
}
public List<Freight> getAll() {
return repo.getAll();
}
public Freight add(Freight freight) {
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) {
Freight freight = repo.getById(id);
if (freight == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return freight;
}
public List<Freight> getByStatus(Freight.Status status) {
return repo.getAll().stream()
.filter(f -> f.status() == status)
.toList();
}
public Freight update(long id, Freight newFreight) {
newFreight = new Freight(id, newFreight.name(), newFreight.description(), newFreight.weightKg(), newFreight.dimensions(), newFreight.status());
return repo.update(id, newFreight);
}
public Freight delete(long id) {
var deleted = repo.delete(id);
if (deleted == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return deleted;
}
}
@@ -0,0 +1,9 @@
spring:
application:
name: freight-service
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:8070/eureka
+10 -3
View File
@@ -4,6 +4,8 @@ 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"
spring-cloud = "2025.1.0"
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
@@ -14,19 +16,24 @@ spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-b
spring-boot-starter-web-test = { group = "org.springframework.boot", name = "spring-boot-starter-webmvc-test" }
spring-boot-starter-hateoas = { group = "org.springframework.boot", name = "spring-boot-starter-hateoas" }
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
spring-cloud-dependencies = { group = "org.springframework.cloud", name = "spring-cloud-dependencies", version.ref = "spring-cloud" }
# 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" }
oldjackson-dataformat-xml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-xml" }
# 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' }
+21
View File
@@ -0,0 +1,21 @@
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)
api(libs.oldjackson.dataformat.xml)
implementation(libs.oldjackson.databind)
}
@@ -6,6 +6,7 @@ import ua.com.dxrkness.model.Freight;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record FreightDto(
long id,
String name,
String description,
int weightKg,
@@ -14,16 +15,7 @@ public record FreightDto(
) {
public static FreightDto fromFreight(Freight freight) {
return new FreightDto(
freight.name(),
freight.description(),
freight.weightKg(),
freight.dimensions(),
freight.status()
);
}
public static FreightDto fromResponse(FreightResponse freight) {
return new FreightDto(
freight.id(),
freight.name(),
freight.description(),
freight.weightKg(),
@@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Vehicle;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VehicleResponse(
public record VehicleDto(
long id,
String brand,
String model,
@@ -14,8 +14,8 @@ public record VehicleResponse(
int capacityKg,
Vehicle.Status status
) {
public static VehicleResponse fromVehicle(Vehicle vehicle) {
return new VehicleResponse(
public static VehicleDto fromVehicle(Vehicle vehicle) {
return new VehicleDto(
vehicle.id(),
vehicle.brand(),
vehicle.model(),
@@ -25,16 +25,4 @@ public record VehicleResponse(
vehicle.status()
);
}
public Vehicle toVehicle() {
return new Vehicle(
this.id(),
this.brand(),
this.model(),
this.licensePlate(),
this.year(),
this.capacityKg(),
this.status()
);
}
}
Executable
+93
View File
@@ -0,0 +1,93 @@
#!/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:8079/vehicle-service/vehicles \
-H "Content-Type: application/json" \
-d $vehicle
printf '\n\n'
curl -X POST http://localhost:8079/freight-service/freights \
-H "Content-Type: application/json" \
-d $freight1
printf '\n\n'
curl -X POST http://localhost:8079/freight-service/freights \
-H "Content-Type: application/json" \
-d $freight2
printf '\n\n'
curl -X POST http://localhost:8079/route-service/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"
}'
# 404 status is expected, since vehicle svc propagates it's error code to route svc
curl -X POST http://localhost:8082/routes \
-H "Content-Type: application/json" \
-d $route
end
+41
View File
@@ -0,0 +1,41 @@
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(platform(libs.spring.cloud.dependencies))
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
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.beans.factory.annotation.Qualifier;
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.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(@Qualifier("freightRestClient") RestClient.Builder freightRestClient, Schema freightSchema) {
this.freightRestClient = freightRestClient.build();
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,48 @@
package ua.com.dxrkness.client;
import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import org.springframework.beans.factory.annotation.Qualifier;
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(@Qualifier("vehicleRestClient") RestClient.Builder vehicleRestClient, Schema vehicleSchema) {
this.vehicleRestClient = vehicleRestClient.build();
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,28 @@
package ua.com.dxrkness.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.client.RestClient;
@Configuration
public class ClientConfiguration {
@Bean
@LoadBalanced
public RestClient.Builder vehicleRestClient() {
return RestClient.builder().baseUrl("http://vehicle-service/vehicles");
}
@Bean
@LoadBalanced
public RestClient.Builder freightRestClient() {
return RestClient.builder().baseUrl("http://freight-service/freights");
}
@Bean
@Primary
public RestClient.Builder defaultRestClient() {
return RestClient.builder();
}
}
@@ -8,18 +8,15 @@ 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.dto.RouteRequest;
import ua.com.dxrkness.dto.RouteResponse;
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;
import java.util.stream.Stream;
@RestController
@RequestMapping(
@@ -31,15 +28,15 @@ import java.util.stream.Stream;
@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(
@@ -59,19 +56,19 @@ public class RouteController {
@Parameter(description = "Filter routes by status (PLANNED, IN_PROGRESS, COMPLETED, CANCELLED)")
@RequestParam(name = "status", required = false) Route.@Nullable Status status) {
if (status != null) {
return routeService.getByStatus(status).stream().map(RouteResponse::toEntity).toList();
return routeService.getByStatus(status);
}
if (vehicleId != null) {
return routeService.getByVehicleId(vehicleId).stream().map(RouteResponse::toEntity).toList();
return routeService.getByVehicleId(vehicleId);
}
if (freightId != null) {
var route = routeService.getByFreightId(freightId);
if (route == null) {
return List.of();
}
return Stream.of(route).map(RouteResponse::toEntity).toList();
return List.of(route);
}
return routeService.getAll().stream().map(RouteResponse::toEntity).toList();
return routeService.getAll();
}
@Operation(
@@ -90,7 +87,7 @@ public class RouteController {
public Route getById(
@Parameter(description = "ID of the route to retrieve", required = true)
@PathVariable("id") long id) {
return routeService.getById(id).toEntity();
return routeService.getById(id);
}
@Operation(
@@ -108,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()).toVehicle();
return vehicleClient.getById(routeService.getById(id).vehicleId());
}
@Operation(
@@ -129,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).toEntity());
freights.add(freightClient.getById(freightId));
}
return freights;
}
@@ -156,8 +153,8 @@ public class RouteController {
description = "Route object to be created. Must include valid vehicleId and freightId list.",
required = true
)
@RequestBody RouteRequest newRoute) {
return routeService.add(newRoute).toEntity();
@RequestBody Route newRoute) {
return routeService.add(newRoute);
}
@Operation(
@@ -180,8 +177,8 @@ public class RouteController {
description = "Updated route object",
required = true
)
@RequestBody RouteRequest newRoute) {
return routeService.update(id, newRoute).toEntity();
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
}
@Operation(
@@ -204,8 +201,8 @@ public class RouteController {
description = "Route object with fields to update",
required = true
)
@RequestBody RouteRequest newRoute) {
return routeService.update(id, newRoute).toEntity();
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
}
@@ -225,6 +222,6 @@ public class RouteController {
public Route delete(
@Parameter(description = "ID of the route to delete", required = true)
@PathVariable("id") long id) {
return routeService.delete(id).toEntity();
return routeService.delete(id);
}
}
@@ -2,9 +2,9 @@ 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.RouteRequest;
import ua.com.dxrkness.dto.RouteResponse;
import ua.com.dxrkness.dto.VehicleDto;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.exception.RouteNotFoundException;
@@ -17,48 +17,49 @@ 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<RouteResponse> getAll() {
return repo.getAll().stream().map(RouteResponse::fromRoute).toList();
public List<Route> getAll() {
return repo.getAll();
}
public RouteResponse getById(long id) {
public Route getById(long id) {
Route route = repo.getById(id);
if (route == null) {
throw new RouteNotFoundException("Route with ID " + id + " not found.");
}
return RouteResponse.fromRoute(route);
return route;
}
public RouteResponse add(RouteRequest route) {
public Route add(Route route) {
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
int id = repo.add(route.toEntity());
return RouteResponse.fromRoute(new Route(id,
var newRoute = new Route(repo.lastId(),
route.vehicleId(),
route.freightId(),
route.startLocation(),
route.endLocation(),
route.distanceKm(),
route.estimatedDurationHours(),
route.status()));
route.status());
repo.add(newRoute);
return newRoute;
}
private VehicleDto validateAndGetVehicle(long vehicleId) {
try {
var vehicle = vehicleService.getById(vehicleId);
return VehicleDto.fromResponse(vehicle);
var vehicle = vehicleClient.getById(vehicleId);
return VehicleDto.fromVehicle(vehicle);
} catch (VehicleNotFoundException e) {
throw new VehicleNotFoundException(
"Cannot create or update route: Referenced vehicle with ID " + vehicleId + " not found.");
@@ -69,8 +70,8 @@ public final class RouteService {
List<FreightDto> freightDtos = new java.util.ArrayList<>();
for (long freightId : freightIds) {
try {
var freight = freightService.getById(freightId);
freightDtos.add(FreightDto.fromResponse(freight));
var freight = freightClient.getById(freightId);
freightDtos.add(FreightDto.fromFreight(freight));
} catch (FreightNotFoundException e) {
throw new FreightNotFoundException(
"Cannot create or update route: Referenced freight with ID " + freightId + " not found.");
@@ -79,26 +80,25 @@ public final class RouteService {
return freightDtos;
}
public @Nullable RouteResponse getByFreightId(long freightId) {
return RouteResponse.fromRoute(repo.getByFreightId(freightId));
public @Nullable Route getByFreightId(long freightId) {
return repo.getByFreightId(freightId);
}
public List<RouteResponse> getByVehicleId(long vehicleId) {
return repo.getByVehicleId(vehicleId).stream().map(RouteResponse::fromRoute).toList();
public List<Route> getByVehicleId(long vehicleId) {
return repo.getByVehicleId(vehicleId);
}
public List<RouteResponse> getByStatus(Route.Status status) {
public List<Route> getByStatus(Route.Status status) {
return repo.getAll().stream()
.filter(r -> r.status() == status)
.map(RouteResponse::fromRoute)
.toList();
}
public RouteResponse update(long id, RouteRequest newRoute) {
public Route update(long id, Route newRoute) {
VehicleDto vehicleDto = validateAndGetVehicle(newRoute.vehicleId());
List<FreightDto> freightDtos = validateAndGetFreights(newRoute.freightId());
var newRouteEntity = new Route(id,
newRoute = new Route(id,
newRoute.vehicleId(),
newRoute.freightId(),
newRoute.startLocation(),
@@ -106,14 +106,14 @@ public final class RouteService {
newRoute.distanceKm(),
newRoute.estimatedDurationHours(),
newRoute.status());
return RouteResponse.fromRoute(repo.update(id, newRouteEntity));
return repo.update(id, newRoute);
}
public RouteResponse delete(long id) {
public Route delete(long id) {
var deleted = repo.delete(id);
if (deleted == null) {
throw new RouteNotFoundException("Route with ID " + id + " not found.");
}
return RouteResponse.fromRoute(deleted);
return deleted;
}
}
@@ -0,0 +1,13 @@
spring:
application:
name: route-service
cloud:
loadbalancer:
cache:
enabled: false
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:8070/eureka
+3 -1
View File
@@ -1,2 +1,4 @@
rootProject.name = "itroi"
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
include("models", "vehicle-service", "route-service", "freight-service", "shared", "api-gateway", "eureka-server")
+35
View 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,228 @@
package ua.com.dxrkness.client;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.xml.XmlMapper;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.model.Vehicle;
import java.util.List;
public class ApplicationClient {
private static final String BASE_URL = "http://localhost:8079";
private static final JsonMapper jsonMapper = JsonMapper.builder().build();
private static final XmlMapper xmlMapper = XmlMapper.builder().build();
static void main() {
try (CloseableHttpClient client = HttpClients.createDefault()) {
testVehicles(client);
testFreights(client);
testRoutes(client);
testSubResources(client);
testStatusFiltering(client);
testErrorCases(client);
testInterServiceValidation(client);
} catch (Exception e) {
IO.println("error: " + e.getMessage());
e.printStackTrace();
}
}
private static void testVehicles(CloseableHttpClient client) {
IO.println("--- VEHICLES ---");
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles"), "GET all vehicles");
Vehicle newVehicle = new Vehicle(0, "Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
String vehicleXml = xmlMapper.writeValueAsString(newVehicle);
HttpPost post = new HttpPost(BASE_URL + "/vehicle-service/vehicles");
post.setEntity(new StringEntity(vehicleXml, "UTF-8"));
post.setHeader("Content-Type", "application/xml");
post.setHeader("Accept", "application/xml");
String createdVehicle = execute(client, post, "POST new vehicle");
var get = new HttpGet(BASE_URL + "/vehicle-service/vehicles/1");
get.setHeader("Content-Type", "application/xml");
execute(client, get, "GET vehicle by ID=1");
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles/999"), "GET vehicle by ID=999 (not found)");
Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
var vehiclePut = createHttpRequest(updateVehicle, "/vehicle-service/vehicles/1", HttpMethod.PUT);
execute(client, vehiclePut, "PUT update vehicle ID=1");
Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
var vehiclePatch = createHttpRequest(patchVehicle, "/vehicle-service/vehicles/1", HttpMethod.PATCH);
execute(client, vehiclePatch, "PATCH update vehicle ID=1");
execute(client, new HttpDelete(BASE_URL + "/vehicle-service/vehicles/999"), "DELETE vehicle ID=999 (not found)");
IO.println();
}
private static void testFreights(CloseableHttpClient client) {
IO.println("--- FREIGHTS ---");
execute(client, new HttpGet(BASE_URL + "/freight-service" + "/freights"), "GET all freights");
Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200);
Freight newFreight = new Freight(1, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
var freightPost = createHttpRequest(newFreight, "/freight-service/freights", HttpMethod.POST);
execute(client, freightPost, "POST new freight");
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/1"), "GET freight by ID=1");
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/999"), "GET freight by ID=999 (not found)");
Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
var freightPut = createHttpRequest(updateFreight, "/freight-service/freights/1", HttpMethod.PUT);
execute(client, freightPut, "PUT update freight ID=1");
Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
var freightPatch = createHttpRequest(patchFreight, "/freight-service/freights/1", HttpMethod.PATCH);
execute(client, freightPatch, "PATCH update freight ID=1");
execute(client, new HttpDelete(BASE_URL + "/freight-service/freights/999"), "DELETE freight ID=999 (not found)");
IO.println();
}
private static void testRoutes(CloseableHttpClient client) {
IO.println("--- ROUTES ---");
Route newRoute = new Route(1, 1, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var routePost = createHttpRequest(newRoute, "/route-service/routes", HttpMethod.POST);
execute(client, routePost, "POST new route");
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes"), "GET all routes");
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes/1"), "GET route by ID=1");
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes/999"), "GET route by ID=999 (not found)");
Route updateRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
var routePut = createHttpRequest(updateRoute, "/route-service/routes/1", HttpMethod.PUT);
execute(client, routePut, "PUT update route ID=1");
Route patchRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
var routePatch = createHttpRequest(patchRoute, "/route-service/routes/1", HttpMethod.PATCH);
execute(client, routePatch, "PATCH update route ID=1");
execute(client, new HttpDelete(BASE_URL + "/route-service" + "/routes/999"), "DELETE route ID=999 (not found)");
IO.println();
}
private static void testSubResources(CloseableHttpClient client) {
IO.println("--- SUB-RESOURCES ---");
execute(client, new HttpGet(BASE_URL + "/route-service/routes/1/freights"), "GET freights for route ID=1 (sub-resource)");
execute(client, new HttpGet(BASE_URL + "/route-service/routes/1/vehicle"), "GET vehicle for route ID=1 (sub-resource)");
IO.println();
}
private static void testStatusFiltering(CloseableHttpClient client) {
IO.println("--- STATUS FILTERING ---");
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles?status=AVAILABLE"), "GET vehicles with status=AVAILABLE");
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles?status=IN_TRANSIT"), "GET vehicles with status=IN_TRANSIT");
execute(client, new HttpGet(BASE_URL + "/freight-service/freights?status=PENDING"), "GET freights with status=PENDING");
execute(client, new HttpGet(BASE_URL + "/freight-service/freights?status=DELIVERED"), "GET freights with status=DELIVERED");
execute(client, new HttpGet(BASE_URL + "/route-service/routes?status=PLANNED"), "GET routes with status=PLANNED");
execute(client, new HttpGet(BASE_URL + "/route-service/routes?status=COMPLETED"), "GET routes with status=COMPLETED");
IO.println();
}
private static void testErrorCases(CloseableHttpClient client) {
IO.println("--- ERROR CASES (TESTING EXCEPTION HANDLING) ---");
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles/999"), "GET non-existent vehicle (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/-1"), "GET freight with invalid ID (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/route-service/routes/-1"), "GET route with invalid ID (404 Not Found)");
IO.println();
}
private static void testInterServiceValidation(CloseableHttpClient client) {
IO.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---");
// Test 1: Create route with valid references (should succeed)
IO.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:");
Route validRoute = new Route(1, 1, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var validRoutePost = createHttpRequest(validRoute, "/route-service/routes", HttpMethod.POST);
execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
// Test 2: Create route with invalid vehicle (should fail with 404)
IO.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:");
Route invalidVehicleRoute = new Route(1, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var invalidRoutePost = createHttpRequest(invalidVehicleRoute, "/route-service/routes", HttpMethod.POST);
execute(client, invalidRoutePost, "POST route with invalid vehicle ID, should result in 404");
// Test 3: Create route with invalid freight (should fail with 404)
IO.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:");
Route invalidFreightRoute = new Route(1, 1, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var invalidFreightPost = createHttpRequest(invalidFreightRoute, "/route-service/routes", HttpMethod.POST);
execute(client, invalidFreightPost, "POST route with invalid freight ID, should result in 404");
}
private static <T> HttpUriRequest createHttpRequest(T entity, String path, HttpMethod method) {
String entityJson = jsonMapper.writeValueAsString(entity);
HttpEntityEnclosingRequestBase req;
if (method == HttpMethod.GET) {
return new HttpGet(BASE_URL + path);
} else if (method == HttpMethod.POST) {
req = new HttpPost(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PUT) {
req = new HttpPut(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PATCH) {
req = new HttpPatch(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.DELETE) {
return new HttpDelete(BASE_URL + path);
} else {
throw new IllegalArgumentException("method is invalid");
}
}
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
try {
IO.println("[" + request.getMethod() + "] " + description);
CloseableHttpResponse response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "";
IO.println("Status: " + statusCode);
if (!body.isEmpty()) {
IO.println("Response: " + body);
}
if (HttpStatus.valueOf(statusCode).isError()) {
IO.println("ERROR: Request failed with status " + statusCode);
}
IO.println();
return body;
} catch (Exception e) {
IO.println("EXCEPTION: " + e.getMessage());
e.printStackTrace();
IO.println();
return null;
}
}
}
@@ -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();
}
}
@@ -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"
]
}
}
@@ -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"
]
}
}
@@ -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,231 +0,0 @@
package ua.com.dxrkness.client;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.dataformat.xml.XmlMapper;
import ua.com.dxrkness.dto.FreightRequest;
import ua.com.dxrkness.dto.RouteRequest;
import ua.com.dxrkness.dto.VehicleRequest;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.model.Vehicle;
import java.util.List;
public class ApplicationClient {
private static final String BASE_URL = "http://localhost:8080";
private static final ObjectMapper jsonMapper = new ObjectMapper();
private static final XmlMapper xmlMapper = new XmlMapper();
static void main() {
try (CloseableHttpClient client = HttpClients.createDefault()) {
testVehicles(client);
testFreights(client);
testRoutes(client);
testSubResources(client);
testStatusFiltering(client);
testErrorCases(client);
testInterServiceValidation(client);
} catch (Exception e) {
System.out.println("error: " + e.getMessage());
e.printStackTrace();
}
}
private static void testVehicles(CloseableHttpClient client) throws Exception {
System.out.println("--- VEHICLES ---");
execute(client, new HttpGet(BASE_URL + "/vehicles"), "GET all vehicles");
var newVehicle = new VehicleRequest("Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
String vehicleXml = xmlMapper.writeValueAsString(newVehicle);
HttpPost post = new HttpPost(BASE_URL + "/vehicles");
post.setEntity(new StringEntity(vehicleXml, "UTF-8"));
post.setHeader("Content-Type", "application/xml");
post.setHeader("Accept", "application/xml");
String createdVehicle = execute(client, post, "POST new vehicle");
var get = new HttpGet(BASE_URL + "/vehicles/0");
get.setHeader("Content-Type", "application/xml");
execute(client, get, "GET vehicle by ID=0");
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET vehicle by ID=999 (not found)");
var updateVehicle = new VehicleRequest("Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
var vehiclePut = createHttpRequest(updateVehicle, "/vehicles/0", HttpMethod.PUT);
execute(client, vehiclePut, "PUT update vehicle ID=0");
var patchVehicle = new VehicleRequest("Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
var vehiclePatch = createHttpRequest(patchVehicle, "/vehicles/0", HttpMethod.PATCH);
execute(client, vehiclePatch, "PATCH update vehicle ID=0");
execute(client, new HttpDelete(BASE_URL + "/vehicles/999"), "DELETE vehicle ID=999 (not found)");
System.out.println();
}
private static void testFreights(CloseableHttpClient client) throws Exception {
System.out.println("--- FREIGHTS ---");
execute(client, new HttpGet(BASE_URL + "/freights"), "GET all freights");
Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200);
var newFreight = new FreightRequest("Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
var freightPost = createHttpRequest(newFreight, "/freights", HttpMethod.POST);
execute(client, freightPost, "POST new freight");
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=0");
execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)");
var updateFreight = new FreightRequest("Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
var freightPut = createHttpRequest(updateFreight, "/freights/0", HttpMethod.PUT);
execute(client, freightPut, "PUT update freight ID=0");
var patchFreight = new FreightRequest("Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
var freightPatch = createHttpRequest(patchFreight, "/freights/0", HttpMethod.PATCH);
execute(client, freightPatch, "PATCH update freight ID=0");
execute(client, new HttpDelete(BASE_URL + "/freights/999"), "DELETE freight ID=999 (not found)");
System.out.println();
}
private static void testRoutes(CloseableHttpClient client) throws Exception {
System.out.println("--- ROUTES ---");
var newRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var routePost = createHttpRequest(newRoute, "/routes", HttpMethod.POST);
execute(client, routePost, "POST new route");
execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes");
execute(client, new HttpGet(BASE_URL + "/routes/0"), "GET route by ID=0");
execute(client, new HttpGet(BASE_URL + "/routes/999"), "GET route by ID=999 (not found)");
var updateRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
var routePut = createHttpRequest(updateRoute, "/routes/0", HttpMethod.PUT);
execute(client, routePut, "PUT update route ID=0");
var patchRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
var routePatch = createHttpRequest(patchRoute, "/routes/0", HttpMethod.PATCH);
execute(client, routePatch, "PATCH update route ID=0");
execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)");
System.out.println();
}
private static void testSubResources(CloseableHttpClient client) throws Exception {
System.out.println("--- SUB-RESOURCES ---");
execute(client, new HttpGet(BASE_URL + "/routes/0/freights"), "GET freights for route ID=0 (sub-resource)");
execute(client, new HttpGet(BASE_URL + "/routes/0/vehicle"), "GET vehicle for route ID=0 (sub-resource)");
System.out.println();
}
private static void testStatusFiltering(CloseableHttpClient client) throws Exception {
System.out.println("--- STATUS FILTERING ---");
execute(client, new HttpGet(BASE_URL + "/vehicles?status=AVAILABLE"), "GET vehicles with status=AVAILABLE");
execute(client, new HttpGet(BASE_URL + "/vehicles?status=IN_TRANSIT"), "GET vehicles with status=IN_TRANSIT");
execute(client, new HttpGet(BASE_URL + "/freights?status=PENDING"), "GET freights with status=PENDING");
execute(client, new HttpGet(BASE_URL + "/freights?status=DELIVERED"), "GET freights with status=DELIVERED");
execute(client, new HttpGet(BASE_URL + "/routes?status=PLANNED"), "GET routes with status=PLANNED");
execute(client, new HttpGet(BASE_URL + "/routes?status=COMPLETED"), "GET routes with status=COMPLETED");
System.out.println();
}
private static void testErrorCases(CloseableHttpClient client) throws Exception {
System.out.println("--- ERROR CASES (TESTING EXCEPTION HANDLING) ---");
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET non-existent vehicle (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/freights/-1"), "GET freight with invalid ID (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/routes/-1"), "GET route with invalid ID (404 Not Found)");
System.out.println();
}
private static void testInterServiceValidation(CloseableHttpClient client) throws Exception {
System.out.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---");
// Test 1: Create route with valid references (should succeed)
System.out.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:");
var validRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var validRoutePost = createHttpRequest(validRoute, "/routes", HttpMethod.POST);
execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
// Test 2: Create route with invalid vehicle (should fail with 404)
System.out.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:");
Route invalidVehicleRoute = new Route(0, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var invalidRoutePost = createHttpRequest(invalidVehicleRoute, "/routes", HttpMethod.POST);
execute(client, invalidRoutePost, "POST route with invalid vehicle ID, should result in 404");
// Test 3: Create route with invalid freight (should fail with 404)
System.out.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:");
Route invalidFreightRoute = new Route(0, 0, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var invalidFreightPost = createHttpRequest(invalidFreightRoute, "/routes", HttpMethod.POST);
execute(client, invalidFreightPost, "POST route with invalid freight ID, should result in 404");
}
private static <T> HttpUriRequest createHttpRequest(T entity, String path, HttpMethod method) {
String entityJson = jsonMapper.writeValueAsString(entity);
HttpEntityEnclosingRequestBase req;
if (method == HttpMethod.GET) {
return new HttpGet(BASE_URL + path);
} else if (method == HttpMethod.POST) {
req = new HttpPost(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PUT) {
req = new HttpPut(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PATCH) {
req = new HttpPatch(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.DELETE) {
return new HttpDelete(BASE_URL + path);
} else {
throw new IllegalArgumentException("method is invalid");
}
}
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
try {
System.out.println("[" + request.getMethod() + "] " + description);
CloseableHttpResponse response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "";
System.out.println("Status: " + statusCode);
if (!body.isEmpty()) {
System.out.println("Response: " + body);
}
if (HttpStatus.valueOf(statusCode).isError()) {
System.out.println("ERROR: Request failed with status " + statusCode);
}
System.out.println();
return body;
} catch (Exception e) {
System.out.println("EXCEPTION: " + e.getMessage());
e.printStackTrace();
System.out.println();
return null;
}
}
}
@@ -1,19 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Freight;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record FreightRequest(
String name,
String description,
int weightKg,
Freight.Dimensions dimensions,
Freight.Status status
) {
public Freight toEntity() {
return new Freight(0, name, description, weightKg, dimensions, status);
}
}
@@ -1,23 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Freight;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record FreightResponse(
String name,
String description,
int weightKg,
Freight.Dimensions dimensions,
Freight.Status status
) {
public Freight toEntity() {
return new Freight(0, name, description, weightKg, dimensions, status);
}
public static FreightResponse fromEntity(Freight freight) {
return new FreightResponse(freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
}
}
@@ -1,21 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Route;
import java.util.List;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record RouteRequest(long vehicleId,
List<Long> freightId,
String startLocation,
String endLocation,
Double distanceKm,
Double estimatedDurationHours,
Route.Status status) {
public Route toEntity() {
return new Route(0, vehicleId, freightId, startLocation, endLocation, distanceKm, estimatedDurationHours, status);
}
}
@@ -1,25 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Route;
import java.util.List;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record RouteResponse(long vehicleId,
List<Long> freightId,
String startLocation,
String endLocation,
Double distanceKm,
Double estimatedDurationHours,
Route.Status status) {
public Route toEntity() {
return new Route(0, vehicleId, freightId, startLocation, endLocation, distanceKm, estimatedDurationHours, status);
}
public static RouteResponse fromRoute(Route route) {
return new RouteResponse(route.vehicleId(), route.freightId(), route.startLocation(), route.endLocation(), route.distanceKm(), route.estimatedDurationHours(), route.status());
}
}
@@ -1,52 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Vehicle;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VehicleDto(
long id,
String brand,
String model,
String licensePlate,
int year,
int capacityKg,
Vehicle.Status status
) {
public static VehicleDto fromVehicle(Vehicle vehicle) {
return new VehicleDto(
vehicle.id(),
vehicle.brand(),
vehicle.model(),
vehicle.licensePlate(),
vehicle.year(),
vehicle.capacityKg(),
vehicle.status()
);
}
public static VehicleDto fromResponse(VehicleResponse vehicle) {
return new VehicleDto(
vehicle.id(),
vehicle.brand(),
vehicle.model(),
vehicle.licensePlate(),
vehicle.year(),
vehicle.capacityKg(),
vehicle.status()
);
}
public Vehicle toVehicle() {
return new Vehicle(
this.id(),
this.brand(),
this.model(),
this.licensePlate(),
this.year(),
this.capacityKg(),
this.status()
);
}
}
@@ -1,20 +0,0 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Vehicle;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VehicleRequest(
String brand,
String model,
String licensePlate,
int year,
int capacityKg,
Vehicle.Status status
) {
public Vehicle toEntity() {
return new Vehicle(0, brand, model, licensePlate, year, capacityKg, status);
}
}
@@ -1,4 +0,0 @@
@NullMarked
package ua.com.dxrkness;
import org.jspecify.annotations.NullMarked;
@@ -1,57 +0,0 @@
package ua.com.dxrkness.service;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.dto.FreightRequest;
import ua.com.dxrkness.dto.FreightResponse;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.repository.FreightRepository;
import java.util.List;
@Service
public final class FreightService {
private final FreightRepository repo;
public FreightService(FreightRepository repo) {
this.repo = repo;
}
public List<FreightResponse> getAll() {
return repo.getAll().stream().map(FreightResponse::fromEntity).toList();
}
public FreightResponse add(FreightRequest freight) {
int id = repo.add(freight.toEntity());
return FreightResponse.fromEntity(new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status()));
}
public FreightResponse getById(long id) {
Freight freight = repo.getById(id);
if (freight == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return FreightResponse.fromEntity(freight);
}
public List<FreightResponse> getByStatus(Freight.Status status) {
return repo.getAll().stream()
.filter(f -> f.status() == status)
.map(FreightResponse::fromEntity)
.toList();
}
public FreightResponse update(long id, FreightRequest newFreight) {
var newFreightEntity = new Freight(id, newFreight.name(), newFreight.description(), newFreight.weightKg(), newFreight.dimensions(), newFreight.status());
return FreightResponse.fromEntity(repo.update(id, newFreightEntity));
}
public FreightResponse delete(long id) {
var deleted = repo.delete(id);
if (deleted == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return FreightResponse.fromEntity(deleted);
}
}
@@ -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();
}
}
-3
View File
@@ -1,3 +0,0 @@
spring:
application:
name: itroi
+41
View File
@@ -0,0 +1,41 @@
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(platform(libs.spring.cloud.dependencies))
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
implementation("org.springframework.boot:spring-boot-starter-aop:4.0.0-M2")
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);
}
}
@@ -0,0 +1,24 @@
package ua.com.dxrkness.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Value("${server.port}")
private String port;
@Before("execution(* ua.com.dxrkness.controller.VehicleController.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
logger.info(">>> [Instance Port: {}] - Calling method: {}", port, methodName);
}
}
@@ -8,8 +8,6 @@ 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.dto.VehicleRequest;
import ua.com.dxrkness.dto.VehicleResponse;
import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.service.VehicleService;
@@ -40,9 +38,9 @@ public class VehicleController {
@Parameter(description = "Filter vehicles by status (AVAILABLE, IN_TRANSIT, MAINTENANCE)")
@RequestParam(name = "status", required = false) Vehicle.@Nullable Status status) {
if (status != null) {
return service.getByStatus(status).stream().map(VehicleResponse::toVehicle).toList();
return service.getByStatus(status);
}
return service.getAll().stream().map(VehicleResponse::toVehicle).toList();
return service.getAll();
}
@Operation(
@@ -52,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).toVehicle();
return List.of(service.getById(id));
}
@Operation(
@@ -70,8 +68,8 @@ public class VehicleController {
description = "Vehicle object to be created",
required = true
)
@RequestBody VehicleRequest newVehicle) {
return service.add(newVehicle).toVehicle();
@RequestBody Vehicle newVehicle) {
return service.add(newVehicle);
}
@Operation(
@@ -88,8 +86,8 @@ public class VehicleController {
description = "Updated vehicle object",
required = true
)
@RequestBody VehicleRequest newVehicle) {
return service.update(id, newVehicle).toVehicle();
@RequestBody Vehicle newVehicle) {
return service.update(id, newVehicle);
}
@Operation(
@@ -106,8 +104,8 @@ public class VehicleController {
description = "Vehicle object with fields to update",
required = true
)
@RequestBody VehicleRequest newVehicle) {
return service.update(id, newVehicle).toVehicle();
@RequestBody Vehicle newVehicle) {
return service.update(id, newVehicle);
}
@@ -121,6 +119,6 @@ public class VehicleController {
public Vehicle delete(
@Parameter(description = "ID of the vehicle to delete", required = true)
@PathVariable("id") long id) {
return service.delete(id).toVehicle();
return service.delete(id);
}
}
@@ -1,8 +1,6 @@
package ua.com.dxrkness.service;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.dto.VehicleRequest;
import ua.com.dxrkness.dto.VehicleResponse;
import ua.com.dxrkness.exception.VehicleNotFoundException;
import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.repository.VehicleRepository;
@@ -17,46 +15,46 @@ public final class VehicleService {
this.repo = repo;
}
public List<VehicleResponse> getAll() {
return repo.getAll().stream().map(VehicleResponse::fromVehicle).toList();
public List<Vehicle> getAll() {
return repo.getAll();
}
public List<VehicleResponse> getByStatus(Vehicle.Status status) {
public List<Vehicle> getByStatus(Vehicle.Status status) {
return repo.getAll().stream()
.filter(v -> v.status() == status)
.map(VehicleResponse::fromVehicle)
.toList();
}
public VehicleResponse add(VehicleRequest veh) {
int id = repo.add(veh.toEntity());
return new VehicleResponse(id, veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
public Vehicle add(Vehicle veh) {
var newVehicle = new Vehicle(repo.lastId(), veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
repo.add(newVehicle);
return newVehicle;
}
public VehicleResponse getById(long id) {
public Vehicle getById(long id) {
Vehicle vehicle = repo.getById(id);
if (vehicle == null) {
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
}
return VehicleResponse.fromVehicle(vehicle);
return vehicle;
}
public VehicleResponse update(long id, VehicleRequest newVehicle) {
var newVehicleEntity = new Vehicle(id,
public Vehicle update(long id, Vehicle newVehicle) {
newVehicle = new Vehicle(id,
newVehicle.brand(),
newVehicle.model(),
newVehicle.licensePlate(),
newVehicle.year(),
newVehicle.capacityKg(),
newVehicle.status());
return VehicleResponse.fromVehicle(repo.update(id, newVehicleEntity));
return repo.update(id, newVehicle);
}
public VehicleResponse delete(long id) {
public Vehicle delete(long id) {
var deleted = repo.delete(id);
if (deleted == null) {
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
}
return VehicleResponse.fromVehicle(deleted);
return deleted;
}
}
@@ -0,0 +1,9 @@
spring:
application:
name: vehicle-service
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:8070/eureka