Compare commits
8 Commits
master
...
practical4
| Author | SHA256 | Date | |
|---|---|---|---|
|
4f1fa86833
|
|||
|
d203544a6f
|
|||
|
1c162907fd
|
|||
|
cfc6f170ad
|
|||
|
d81b40ee8e
|
|||
|
ca028f9262
|
|||
|
a3665918f0
|
|||
|
ce9abe9940
|
@@ -1,18 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Build/Test Commands
|
|
||||||
- Build: `./gradlew build`
|
|
||||||
- Test all: `./gradlew test`
|
|
||||||
- Run single test: `./gradlew test --tests "FreightIntegrationTest.getByIdReturnsEntity"`
|
|
||||||
- Run app: `./gradlew 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
|
|
||||||
@@ -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
|
||||||
@@ -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 {
|
dependencies {
|
||||||
// spring
|
// spring
|
||||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
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)
|
||||||
implementation(libs.spring.boot.starter.web.test)
|
implementation(libs.spring.boot.starter.web.test)
|
||||||
implementation(libs.spring.boot.starter.hateoas)
|
|
||||||
developmentOnly(libs.spring.boot.devtools)
|
|
||||||
|
|
||||||
// http client
|
// http client
|
||||||
implementation(libs.apache.http.client)
|
implementation(libs.apache.http.client)
|
||||||
@@ -24,15 +24,15 @@ dependencies {
|
|||||||
// openapi docs
|
// openapi docs
|
||||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||||
|
|
||||||
// xml
|
|
||||||
implementation(libs.jackson.dataformat.xml)
|
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
testImplementation(platform(libs.junit.bom))
|
testImplementation(platform(libs.junit.bom))
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
testRuntimeOnly(libs.junit.platform.launcher)
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
implementation(libs.jspecify)
|
implementation(libs.jspecify)
|
||||||
|
|
||||||
|
implementation(project(":models"))
|
||||||
|
implementation(project(":shared"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
+2
-2
@@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class LogisticsApplication {
|
public class FreightServiceApp {
|
||||||
static void main(String[] args) {
|
static void main(String[] args) {
|
||||||
SpringApplication.run(LogisticsApplication.class, args);
|
SpringApplication.run(FreightServiceApp.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+26
-48
@@ -1,13 +1,11 @@
|
|||||||
package ua.com.dxrkness.controller;
|
package ua.com.dxrkness.controller;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import ua.com.dxrkness.model.Freight;
|
import ua.com.dxrkness.model.Freight;
|
||||||
import ua.com.dxrkness.service.FreightService;
|
import ua.com.dxrkness.service.FreightService;
|
||||||
@@ -21,7 +19,6 @@ import java.util.List;
|
|||||||
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
|
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
|
||||||
)
|
)
|
||||||
@Tag(name = "Freight", description = "Freights management")
|
@Tag(name = "Freight", description = "Freights management")
|
||||||
@NullMarked
|
|
||||||
public class FreightController {
|
public class FreightController {
|
||||||
private final FreightService service;
|
private final FreightService service;
|
||||||
|
|
||||||
@@ -31,18 +28,19 @@ public class FreightController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Retrieve all freights",
|
summary = "Retrieve all freights",
|
||||||
description = "Gets a list of all freight records in the system"
|
description = "Gets a list of all freight records in the system or filter by status"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved list of freights (may be empty!)",
|
description = "Successfully retrieved list of freights (may be empty!)"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Freight> getAll() {
|
public List<Freight> getAll(
|
||||||
|
@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);
|
||||||
|
}
|
||||||
return service.getAll();
|
return service.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,19 +50,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved freight",
|
description = "Successfully retrieved freight"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Freight not found"
|
description = "Freight not found"
|
||||||
)
|
)
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Freight> getById(@PathVariable("id") long id) {
|
public List<Freight> getById(@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return List.of(service.getById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -73,19 +67,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully created",
|
description = "Freight successfully created"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
|
||||||
)
|
)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) {
|
public Freight add(@RequestBody Freight newFreight) {
|
||||||
return ResponseEntity.ok(service.add(newFreight));
|
return service.add(newFreight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -94,19 +84,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully updated",
|
description = "Freight successfully updated"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid input"
|
||||||
)
|
)
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<Freight> update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
public Freight update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
||||||
return ResponseEntity.ok(service.update(id, newFreight));
|
return service.update(id, newFreight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -115,19 +101,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully updated",
|
description = "Freight successfully updated"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid input"
|
||||||
)
|
)
|
||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
public ResponseEntity<Freight> updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
public Freight updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
||||||
return ResponseEntity.ok(service.update(id, newFreight));
|
return service.update(id, newFreight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -136,18 +118,14 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully deleted",
|
description = "Freight successfully deleted"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Freight not found"
|
description = "Freight not found"
|
||||||
)
|
)
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Freight> delete(@PathVariable("id") long id) {
|
public Freight delete(@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
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
|
||||||
@@ -4,6 +4,8 @@ spring-boot-plugin = "4.0.0"
|
|||||||
jspecify = "1.0.0"
|
jspecify = "1.0.0"
|
||||||
springdoc = "3.0.0"
|
springdoc = "3.0.0"
|
||||||
apache-http-client = "4.5.14"
|
apache-http-client = "4.5.14"
|
||||||
|
json-schema-validator = "3.0.0"
|
||||||
|
spring-cloud = "2025.1.0"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
|
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-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-starter-hateoas = { group = "org.springframework.boot", name = "spring-boot-starter-hateoas" }
|
||||||
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
|
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
|
# 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
|
# openapi
|
||||||
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
|
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" }
|
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
|
# testing
|
||||||
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
|
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
|
||||||
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
|
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
|
||||||
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
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' }
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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)
|
||||||
|
public record FreightDto(
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
int weightKg,
|
||||||
|
Freight.Dimensions dimensions,
|
||||||
|
Freight.Status status
|
||||||
|
) {
|
||||||
|
public static FreightDto fromFreight(Freight freight) {
|
||||||
|
return new FreightDto(
|
||||||
|
freight.id(),
|
||||||
|
freight.name(),
|
||||||
|
freight.description(),
|
||||||
|
freight.weightKg(),
|
||||||
|
freight.dimensions(),
|
||||||
|
freight.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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)
|
||||||
|
public record RouteDto(
|
||||||
|
long id,
|
||||||
|
long vehicleId,
|
||||||
|
List<Long> freightId,
|
||||||
|
String startLocation,
|
||||||
|
String endLocation,
|
||||||
|
Double distanceKm,
|
||||||
|
Double estimatedDurationHours,
|
||||||
|
Route.Status status
|
||||||
|
) {
|
||||||
|
public static RouteDto fromRoute(Route route) {
|
||||||
|
return new RouteDto(
|
||||||
|
route.id(),
|
||||||
|
route.vehicleId(),
|
||||||
|
route.freightId(),
|
||||||
|
route.startLocation(),
|
||||||
|
route.endLocation(),
|
||||||
|
route.distanceKm(),
|
||||||
|
route.estimatedDurationHours(),
|
||||||
|
route.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -1,15 +1,19 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Freight(long id,
|
public record Freight(long id,
|
||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
int weightKg,
|
int weightKg,
|
||||||
Dimensions dimensions,
|
Dimensions dimensions,
|
||||||
Status status) implements Identifiable {
|
Status status) implements Identifiable {
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Dimensions(int widthCm,
|
public record Dimensions(int widthCm,
|
||||||
int heightCm,
|
int heightCm,
|
||||||
int lengthCm) {
|
int lengthCm) {
|
||||||
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Route(long id,
|
public record Route(long id,
|
||||||
long vehicleId,
|
long vehicleId,
|
||||||
List<Long> freightId,
|
List<Long> freightId,
|
||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Vehicle(long id,
|
public record Vehicle(long id,
|
||||||
String brand,
|
String brand,
|
||||||
String model,
|
String model,
|
||||||
Executable
+93
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
-114
@@ -2,18 +2,20 @@ package ua.com.dxrkness.controller;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.NullMarked;
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
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.Route;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
import ua.com.dxrkness.service.RouteService;
|
import ua.com.dxrkness.service.RouteService;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -25,128 +27,48 @@ import java.util.List;
|
|||||||
@Tag(name = "Route", description = "Routes management")
|
@Tag(name = "Route", description = "Routes management")
|
||||||
@NullMarked
|
@NullMarked
|
||||||
public class RouteController {
|
public class RouteController {
|
||||||
private final RouteService service;
|
private final RouteService routeService;
|
||||||
|
private final VehicleClient vehicleClient;
|
||||||
|
private final FreightClient freightClient;
|
||||||
|
|
||||||
public RouteController(RouteService service) {
|
public RouteController(RouteService routeService,
|
||||||
this.service = service;
|
VehicleClient vehicleClient,
|
||||||
|
FreightClient freightClient) {
|
||||||
|
this.routeService = routeService;
|
||||||
|
this.vehicleClient = vehicleClient;
|
||||||
|
this.freightClient = freightClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get routes",
|
summary = "Get routes",
|
||||||
description = "Get all routes or filter by freight ID/vehicle ID. May result in empty list!"
|
description = "Get all routes or filter by freight ID/vehicle ID/status. May result in empty list!"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved routes",
|
description = "Successfully retrieved routes"
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Route> getAll(
|
public List<Route> getAll(
|
||||||
@Parameter(description = "Filter routes by freight ID")
|
@Parameter(description = "Filter routes by freight ID")
|
||||||
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
|
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
|
||||||
@Parameter(description = "Filter routes by vehicle ID")
|
@Parameter(description = "Filter routes by vehicle ID")
|
||||||
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId) {
|
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId,
|
||||||
|
@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);
|
||||||
|
}
|
||||||
if (vehicleId != null) {
|
if (vehicleId != null) {
|
||||||
return service.getByVehicleId(vehicleId);
|
return routeService.getByVehicleId(vehicleId);
|
||||||
}
|
}
|
||||||
if (freightId != null) {
|
if (freightId != null) {
|
||||||
var route = service.getByFreightId(freightId);
|
var route = routeService.getByFreightId(freightId);
|
||||||
if (route == null) {
|
if (route == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return List.of(route);
|
return List.of(route);
|
||||||
}
|
}
|
||||||
return service.getAll();
|
return routeService.getAll();
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Create new route",
|
|
||||||
description = "Add new route"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully created",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<Route> add(
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Route object to be created",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.add(newRoute));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Update a route (full)",
|
|
||||||
description = "Update all fields of an existing route by ID"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully updated",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ResponseEntity<Route> update(
|
|
||||||
@Parameter(description = "ID of the route to update", required = true)
|
|
||||||
@PathVariable("id") long id,
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Updated route object",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.update(id, newRoute));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Update a route (partial)",
|
|
||||||
description = "Update specific fields of an existing route by ID"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully updated",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PatchMapping("/{id}")
|
|
||||||
public ResponseEntity<Route> updatePatch(
|
|
||||||
@Parameter(description = "ID of the route to update", required = true)
|
|
||||||
@PathVariable("id") long id,
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Route object with fields to update",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.update(id, newRoute));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -155,40 +77,151 @@ public class RouteController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Route found",
|
description = "Route found"
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Route not found"
|
description = "Route not found"
|
||||||
)
|
)
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Route> getById(
|
public Route getById(
|
||||||
@Parameter(description = "ID of the route to retrieve", required = true)
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return routeService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Retrieve a vehicle by route ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route and vehicle found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Route or vehicle not found"
|
||||||
|
)
|
||||||
|
@GetMapping(value = "/{id}/vehicle", consumes = MediaType.ALL_VALUE)
|
||||||
|
public Vehicle getVehicleById(
|
||||||
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
|
@PathVariable("id") long id) {
|
||||||
|
return vehicleClient.getById(routeService.getById(id).vehicleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Retrieve freights by route ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route and freights found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Route or freight not found"
|
||||||
|
)
|
||||||
|
@GetMapping(value = "/{id}/freights", consumes = MediaType.ALL_VALUE)
|
||||||
|
public List<Freight> getFreightsById(
|
||||||
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
|
@PathVariable("id") long id) {
|
||||||
|
var route = routeService.getById(id);
|
||||||
|
var freights = new ArrayList<Freight>();
|
||||||
|
for (var freightId : route.freightId()) {
|
||||||
|
freights.add(freightClient.getById(freightId));
|
||||||
|
}
|
||||||
|
return freights;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Create new route",
|
||||||
|
description = "Add new route"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully created"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data: vehicle or freight not found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Referenced vehicle or freight not found"
|
||||||
|
)
|
||||||
|
@PostMapping
|
||||||
|
public Route add(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Route object to be created. Must include valid vehicleId and freightId list.",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody Route newRoute) {
|
||||||
|
return routeService.add(newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Update a route (full)",
|
||||||
|
description = "Update all fields of an existing route by ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully updated"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data provided"
|
||||||
|
)
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Route update(
|
||||||
|
@Parameter(description = "ID of the route to update", required = true)
|
||||||
|
@PathVariable("id") long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Updated route object",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody Route newRoute) {
|
||||||
|
return routeService.update(id, newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Update a route (partial)",
|
||||||
|
description = "Update specific fields of an existing route by ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully updated"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data provided"
|
||||||
|
)
|
||||||
|
@PatchMapping("/{id}")
|
||||||
|
public Route updatePatch(
|
||||||
|
@Parameter(description = "ID of the route to update", required = true)
|
||||||
|
@PathVariable("id") long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Route object with fields to update",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody Route newRoute) {
|
||||||
|
return routeService.update(id, newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Delete a route",
|
summary = "Delete a route",
|
||||||
description = "Delete a route by ID"
|
description = "Delete a route by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Route successfully deleted",
|
description = "Route successfully deleted"
|
||||||
content = @Content(schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Route not found"
|
description = "Route not found"
|
||||||
)
|
)
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Route> delete(
|
public Route delete(
|
||||||
@Parameter(description = "ID of the route to delete", required = true)
|
@Parameter(description = "ID of the route to delete", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
return routeService.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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;
|
||||||
|
import ua.com.dxrkness.exception.RouteNotFoundException;
|
||||||
|
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
import ua.com.dxrkness.repository.RouteRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class RouteService {
|
||||||
|
private final RouteRepository repo;
|
||||||
|
private final VehicleClient vehicleClient;
|
||||||
|
private final FreightClient freightClient;
|
||||||
|
|
||||||
|
public RouteService(RouteRepository repo,
|
||||||
|
VehicleClient vehicleClient,
|
||||||
|
FreightClient freightClient) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.vehicleClient = vehicleClient;
|
||||||
|
this.freightClient = freightClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getAll() {
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route getById(long id) {
|
||||||
|
Route route = repo.getById(id);
|
||||||
|
if (route == null) {
|
||||||
|
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route add(Route route) {
|
||||||
|
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
|
||||||
|
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
|
||||||
|
|
||||||
|
var newRoute = new Route(repo.lastId(),
|
||||||
|
route.vehicleId(),
|
||||||
|
route.freightId(),
|
||||||
|
route.startLocation(),
|
||||||
|
route.endLocation(),
|
||||||
|
route.distanceKm(),
|
||||||
|
route.estimatedDurationHours(),
|
||||||
|
route.status());
|
||||||
|
repo.add(newRoute);
|
||||||
|
return newRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VehicleDto validateAndGetVehicle(long vehicleId) {
|
||||||
|
try {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FreightDto> validateAndGetFreights(List<Long> freightIds) {
|
||||||
|
List<FreightDto> freightDtos = new java.util.ArrayList<>();
|
||||||
|
for (long freightId : freightIds) {
|
||||||
|
try {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return freightDtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable Route getByFreightId(long freightId) {
|
||||||
|
return repo.getByFreightId(freightId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getByVehicleId(long vehicleId) {
|
||||||
|
return repo.getByVehicleId(vehicleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getByStatus(Route.Status status) {
|
||||||
|
return repo.getAll().stream()
|
||||||
|
.filter(r -> r.status() == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route update(long id, Route newRoute) {
|
||||||
|
VehicleDto vehicleDto = validateAndGetVehicle(newRoute.vehicleId());
|
||||||
|
List<FreightDto> freightDtos = validateAndGetFreights(newRoute.freightId());
|
||||||
|
|
||||||
|
newRoute = new Route(id,
|
||||||
|
newRoute.vehicleId(),
|
||||||
|
newRoute.freightId(),
|
||||||
|
newRoute.startLocation(),
|
||||||
|
newRoute.endLocation(),
|
||||||
|
newRoute.distanceKm(),
|
||||||
|
newRoute.estimatedDurationHours(),
|
||||||
|
newRoute.status());
|
||||||
|
return repo.update(id, newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route delete(long id) {
|
||||||
|
var deleted = repo.delete(id);
|
||||||
|
if (deleted == null) {
|
||||||
|
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
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
@@ -1,2 +1,4 @@
|
|||||||
rootProject.name = "itroi"
|
rootProject.name = "itroi"
|
||||||
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
||||||
|
|
||||||
|
include("models", "vehicle-service", "route-service", "freight-service", "shared", "api-gateway", "eureka-server")
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ua.com.dxrkness.controller;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
@NullMarked
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(exception = {
|
||||||
|
VehicleNotFoundException.class,
|
||||||
|
FreightNotFoundException.class,
|
||||||
|
RouteNotFoundException.class
|
||||||
|
})
|
||||||
|
public ErrorResponse handleVehicleNotFoundException(Exception ex) {
|
||||||
|
return constructExceptionBody(HttpStatus.NOT_FOUND, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ErrorResponse constructExceptionBody(HttpStatus errorStatus, Exception ex) {
|
||||||
|
return ErrorResponse.builder(ex, errorStatus, ex.getMessage()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class FreightNotFoundException extends RuntimeException {
|
||||||
|
public FreightNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class RouteNotFoundException extends RuntimeException {
|
||||||
|
public RouteNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class VehicleNotFoundException extends RuntimeException {
|
||||||
|
public VehicleNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -5,9 +5,11 @@ import ua.com.dxrkness.model.Identifiable;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
abstract class CrudRepository<T extends Identifiable> {
|
abstract class CrudRepository<T extends Identifiable> {
|
||||||
protected final List<T> STORAGE = new ArrayList<>();
|
protected final List<T> STORAGE = new ArrayList<>();
|
||||||
|
private final AtomicLong id = new AtomicLong(0);
|
||||||
|
|
||||||
public List<T> getAll() {
|
public List<T> getAll() {
|
||||||
return new ArrayList<>(STORAGE); // defensive copy
|
return new ArrayList<>(STORAGE); // defensive copy
|
||||||
@@ -55,4 +57,8 @@ abstract class CrudRepository<T extends Identifiable> {
|
|||||||
public void deleteAll() {
|
public void deleteAll() {
|
||||||
STORAGE.clear();
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package ua.com.dxrkness.integration;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@NullMarked
|
||||||
|
class InterServiceCommunicationTest {
|
||||||
|
private static final int AMOUNT = 100;
|
||||||
|
@Autowired
|
||||||
|
private PopulateService populateService;
|
||||||
|
|
||||||
|
private RestTestClient routesClient;
|
||||||
|
private RestTestClient vehiclesClient;
|
||||||
|
private RestTestClient freightsClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(WebApplicationContext context) {
|
||||||
|
populateService.populate(AMOUNT);
|
||||||
|
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
|
||||||
|
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
|
||||||
|
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void teardown() {
|
||||||
|
populateService.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<MediaType> mediaTypes() {
|
||||||
|
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== INTER-SERVICE VALIDATION TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithValidVehicleAndFreights_Succeeds(MediaType mediaType) {
|
||||||
|
// given - a route with valid vehicle and freight references
|
||||||
|
final var validVehicleId = 1;
|
||||||
|
final var validFreightIds = List.of(1L, 2L);
|
||||||
|
final var newRoute = new Route(0, validVehicleId, validFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should succeed because vehicle and freights exist (validated through inter-service communication)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithInvalidVehicleId_Returns404(MediaType mediaType) {
|
||||||
|
// given - a route with an invalid (non-existent) vehicle ID
|
||||||
|
final var invalidVehicleId = -1;
|
||||||
|
final var validFreightIds = List.of(1L);
|
||||||
|
final var newRoute = new Route(0, invalidVehicleId, validFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should fail with 404 because vehicle doesn't exist (inter-service validation)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found").contains("vehicle");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithInvalidFreightId_Returns404(MediaType mediaType) {
|
||||||
|
// given - a route with a valid vehicle but invalid freight ID
|
||||||
|
final var validVehicleId = 1;
|
||||||
|
final var invalidFreightIds = List.of(-1L); // Invalid freight ID
|
||||||
|
final var newRoute = new Route(0, validVehicleId, invalidFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should fail with 404 because freight doesn't exist (inter-service validation)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found").contains("freight");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void updateRoute_ValidatesReferences_OnInterServiceCall(MediaType mediaType) {
|
||||||
|
// given - an existing route and new valid references
|
||||||
|
final var routeId = 1;
|
||||||
|
final var validVehicleId = 2;
|
||||||
|
final var validFreightIds = List.of(3L, 4L);
|
||||||
|
final var updatedRoute = new Route(routeId, validVehicleId, validFreightIds,
|
||||||
|
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
|
||||||
|
|
||||||
|
// when - updating the route
|
||||||
|
// then - should succeed because vehicle and freights exist (inter-service validation)
|
||||||
|
routesClient.put().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(updatedRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void updateRoute_WithInvalidVehicleOnUpdate_Returns404(MediaType mediaType) {
|
||||||
|
// given - an existing route and new references with invalid vehicle
|
||||||
|
final var routeId = 1;
|
||||||
|
final var invalidVehicleId = -999;
|
||||||
|
final var validFreightIds = List.of(3L);
|
||||||
|
final var updatedRoute = new Route(routeId, invalidVehicleId, validFreightIds,
|
||||||
|
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
|
||||||
|
|
||||||
|
// when - updating the route
|
||||||
|
// then - should fail with 404 (inter-service validation on update)
|
||||||
|
routesClient.put().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(updatedRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== VEHICLE INTER-SERVICE TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getVehicle_UsedInRoute_ReturnsVehicleData(MediaType mediaType) {
|
||||||
|
// given - a vehicle that is used in routes
|
||||||
|
final var vehicleId = 1;
|
||||||
|
|
||||||
|
// when - retrieving the vehicle
|
||||||
|
// then - should return vehicle data (VehicleService provides data to other services)
|
||||||
|
vehiclesClient.get().uri("/{id}", vehicleId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FREIGHT INTER-SERVICE TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getFreight_UsedInRoute_ReturnsFreightData(MediaType mediaType) {
|
||||||
|
// given - a freight that is used in routes
|
||||||
|
final var freightId = 1;
|
||||||
|
|
||||||
|
// when - retrieving the freight
|
||||||
|
// then - should return freight data (FreightService provides data to other services)
|
||||||
|
freightsClient.get().uri("/{id}", freightId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package ua.com.dxrkness.integration;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import ua.com.dxrkness.service.PopulateService;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.BDDAssertions.then;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@NullMarked
|
||||||
|
class SubResourcesAndFilteringTest {
|
||||||
|
private static final int AMOUNT = 100;
|
||||||
|
@Autowired
|
||||||
|
private PopulateService populateService;
|
||||||
|
|
||||||
|
private RestTestClient vehiclesClient;
|
||||||
|
private RestTestClient routesClient;
|
||||||
|
private RestTestClient freightsClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(WebApplicationContext context) {
|
||||||
|
populateService.populate(AMOUNT);
|
||||||
|
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
|
||||||
|
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
|
||||||
|
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void teardown() {
|
||||||
|
populateService.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<MediaType> mediaTypes() {
|
||||||
|
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_VehicleNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var vehicleId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
vehiclesClient.get().uri("/{id}", vehicleId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_RouteNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var routeId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
routesClient.get().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_FreightNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var freightId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
freightsClient.get().uri("/{id}", freightId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,167 +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 tools.jackson.databind.ObjectMapper;
|
|
||||||
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: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);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.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");
|
|
||||||
|
|
||||||
Vehicle newVehicle = new Vehicle(0, "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)");
|
|
||||||
|
|
||||||
Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateVehicle);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/vehicles/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update vehicle ID=0");
|
|
||||||
|
|
||||||
Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchVehicle);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/vehicles/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "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);
|
|
||||||
Freight newFreight = new Freight(0, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
|
|
||||||
String freightJson = jsonMapper.writeValueAsString(newFreight);
|
|
||||||
HttpPost post = new HttpPost(BASE_URL + "/freights");
|
|
||||||
post.setEntity(new StringEntity(freightJson, "UTF-8"));
|
|
||||||
post.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, post, "POST new freight");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=1");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)");
|
|
||||||
|
|
||||||
Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateFreight);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/freights/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update freight ID=1");
|
|
||||||
|
|
||||||
Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchFreight);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/freights/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "PATCH update freight ID=1");
|
|
||||||
|
|
||||||
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 ---");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes?vehicleId=0"), "GET routes by vehicleId=1");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes?freightId=0"), "GET routes by freightId=1");
|
|
||||||
|
|
||||||
Route newRoute = new Route(0, 0, List.of(0L, 1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
|
||||||
String routeJson = jsonMapper.writeValueAsString(newRoute);
|
|
||||||
HttpPost post = new HttpPost(BASE_URL + "/routes");
|
|
||||||
post.setEntity(new StringEntity(routeJson, "UTF-8"));
|
|
||||||
post.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, post, "POST new route");
|
|
||||||
|
|
||||||
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)");
|
|
||||||
|
|
||||||
Route updateRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateRoute);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/routes/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update route ID=0");
|
|
||||||
|
|
||||||
Route patchRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchRoute);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/routes/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "PATCH update route ID=0");
|
|
||||||
|
|
||||||
execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)");
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (statusCode >= 400) {
|
|
||||||
System.err.println("ERROR: Request failed with status " + statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
return body;
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("EXCEPTION: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
System.out.println();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@NullMarked
|
|
||||||
package ua.com.dxrkness;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
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) {
|
|
||||||
int id = repo.add(freight);
|
|
||||||
return new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Freight getById(long id) {
|
|
||||||
return repo.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 @Nullable Freight delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,61 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Route;
|
|
||||||
import ua.com.dxrkness.repository.RouteRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public final class RouteService {
|
|
||||||
private final RouteRepository repo;
|
|
||||||
|
|
||||||
public RouteService(RouteRepository repo) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Route> getAll() {
|
|
||||||
return repo.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route getById(long id) {
|
|
||||||
return repo.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Route add(Route route) {
|
|
||||||
int id = repo.add(route);
|
|
||||||
return new Route(id,
|
|
||||||
route.vehicleId(),
|
|
||||||
route.freightId(),
|
|
||||||
route.startLocation(),
|
|
||||||
route.endLocation(),
|
|
||||||
route.distanceKm(),
|
|
||||||
route.estimatedDurationHours(),
|
|
||||||
route.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route getByFreightId(long freightId) {
|
|
||||||
return repo.getByFreightId(freightId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Route> getByVehicleId(long vehicleId) {
|
|
||||||
return repo.getByVehicleId(vehicleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Route update(long id, Route newRoute) {
|
|
||||||
newRoute = new Route(id,
|
|
||||||
newRoute.vehicleId(),
|
|
||||||
newRoute.freightId(),
|
|
||||||
newRoute.startLocation(),
|
|
||||||
newRoute.endLocation(),
|
|
||||||
newRoute.distanceKm(),
|
|
||||||
newRoute.estimatedDurationHours(),
|
|
||||||
newRoute.status());
|
|
||||||
return repo.update(id, newRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
|
||||||
import ua.com.dxrkness.repository.VehicleRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public final class VehicleService {
|
|
||||||
private final VehicleRepository repo;
|
|
||||||
|
|
||||||
public VehicleService(VehicleRepository repo) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Vehicle> getAll() {
|
|
||||||
return repo.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Vehicle getById(long id) {
|
|
||||||
return repo.getById(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 repo.update(id, newVehicle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Vehicle delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
spring:
|
|
||||||
application:
|
|
||||||
name: itroi
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-33
@@ -2,13 +2,11 @@ package ua.com.dxrkness.controller;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
import ua.com.dxrkness.service.VehicleService;
|
import ua.com.dxrkness.service.VehicleService;
|
||||||
@@ -32,12 +30,16 @@ public class VehicleController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Retrieve all vehicles",
|
summary = "Retrieve all vehicles",
|
||||||
description = "Get a list of all vehicles"
|
description = "Get a list of all vehicles or filter by status"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles",
|
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Vehicle> getAll() {
|
public List<Vehicle> getAll(
|
||||||
|
@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);
|
||||||
|
}
|
||||||
return service.getAll();
|
return service.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,69 +47,65 @@ public class VehicleController {
|
|||||||
summary = "Retrieve a vehicle by ID",
|
summary = "Retrieve a vehicle by ID",
|
||||||
description = "Get a single vehicle by its ID"
|
description = "Get a single vehicle by its ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle found",
|
@ApiResponse(responseCode = "200", description = "Vehicle found")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Vehicle> getById(
|
public List<Vehicle> getById(
|
||||||
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return List.of(service.getById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Create a new vehicle",
|
summary = "Create a new vehicle",
|
||||||
description = "Add a new vehicle to the system"
|
description = "Add a new vehicle to the system"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully created",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully created")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Vehicle> add(
|
public Vehicle add(
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Vehicle object to be created",
|
description = "Vehicle object to be created",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody Vehicle newVehicle) {
|
||||||
return ResponseEntity.ok(service.add(newVehicle));
|
return service.add(newVehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update a vehicle (full)",
|
summary = "Update a vehicle (full)",
|
||||||
description = "Update all fields of an existing vehicle by ID"
|
description = "Update all fields of an existing vehicle by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<Vehicle> update(
|
public Vehicle update(
|
||||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||||
@PathVariable("id") long id,
|
@PathVariable("id") long id,
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Updated vehicle object",
|
description = "Updated vehicle object",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody Vehicle newVehicle) {
|
||||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
return service.update(id, newVehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update a vehicle (partial)",
|
summary = "Update a vehicle (partial)",
|
||||||
description = "Update specific fields of an existing vehicle by ID"
|
description = "Update specific fields of an existing vehicle by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
public ResponseEntity<Vehicle> updatePatch(
|
public Vehicle updatePatch(
|
||||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||||
@PathVariable("id") long id,
|
@PathVariable("id") long id,
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Vehicle object with fields to update",
|
description = "Vehicle object with fields to update",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody Vehicle newVehicle) {
|
||||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
return service.update(id, newVehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -115,13 +113,12 @@ public class VehicleController {
|
|||||||
summary = "Delete a vehicle",
|
summary = "Delete a vehicle",
|
||||||
description = "Delete a vehicle by its ID"
|
description = "Delete a vehicle by its ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Vehicle> delete(
|
public Vehicle delete(
|
||||||
@Parameter(description = "ID of the vehicle to delete", required = true)
|
@Parameter(description = "ID of the vehicle to delete", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
return service.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
import ua.com.dxrkness.repository.VehicleRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class VehicleService {
|
||||||
|
private final VehicleRepository repo;
|
||||||
|
|
||||||
|
public VehicleService(VehicleRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vehicle> getAll() {
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vehicle> getByStatus(Vehicle.Status status) {
|
||||||
|
return repo.getAll().stream()
|
||||||
|
.filter(v -> v.status() == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Vehicle getById(long id) {
|
||||||
|
Vehicle vehicle = repo.getById(id);
|
||||||
|
if (vehicle == null) {
|
||||||
|
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return vehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle update(long id, Vehicle newVehicle) {
|
||||||
|
newVehicle = new Vehicle(id,
|
||||||
|
newVehicle.brand(),
|
||||||
|
newVehicle.model(),
|
||||||
|
newVehicle.licensePlate(),
|
||||||
|
newVehicle.year(),
|
||||||
|
newVehicle.capacityKg(),
|
||||||
|
newVehicle.status());
|
||||||
|
return repo.update(id, newVehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle delete(long id) {
|
||||||
|
var deleted = repo.delete(id);
|
||||||
|
if (deleted == null) {
|
||||||
|
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: vehicle-service
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
service-url:
|
||||||
|
defaultZone: http://localhost:8070/eureka
|
||||||
Reference in New Issue
Block a user