Compare commits
8 Commits
master
...
practical2
| Author | SHA256 | Date | |
|---|---|---|---|
|
1330350254
|
|||
|
86c88c3bd4
|
|||
|
1c162907fd
|
|||
|
cfc6f170ad
|
|||
|
d81b40ee8e
|
|||
|
ca028f9262
|
|||
|
a3665918f0
|
|||
|
ce9abe9940
|
18
AGENTS.md
18
AGENTS.md
@@ -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
|
||||
@@ -5,8 +5,13 @@ import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.dataformat.xml.XmlMapper;
|
||||
import ua.com.dxrkness.dto.FreightRequest;
|
||||
import ua.com.dxrkness.dto.RouteRequest;
|
||||
import ua.com.dxrkness.dto.VehicleRequest;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
@@ -23,9 +28,13 @@ public class ApplicationClient {
|
||||
testVehicles(client);
|
||||
testFreights(client);
|
||||
testRoutes(client);
|
||||
testSubResources(client);
|
||||
testStatusFiltering(client);
|
||||
testErrorCases(client);
|
||||
testInterServiceValidation(client);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("error: " + e.getMessage());
|
||||
System.out.println("error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +44,7 @@ public class ApplicationClient {
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/vehicles"), "GET all vehicles");
|
||||
|
||||
Vehicle newVehicle = new Vehicle(0, "Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
|
||||
var newVehicle = new VehicleRequest("Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
|
||||
String vehicleXml = xmlMapper.writeValueAsString(newVehicle);
|
||||
HttpPost post = new HttpPost(BASE_URL + "/vehicles");
|
||||
post.setEntity(new StringEntity(vehicleXml, "UTF-8"));
|
||||
@@ -48,19 +57,13 @@ public class ApplicationClient {
|
||||
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");
|
||||
var updateVehicle = new VehicleRequest("Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
|
||||
var vehiclePut = createHttpRequest(updateVehicle, "/vehicles/0", HttpMethod.PUT);
|
||||
execute(client, vehiclePut, "PUT update vehicle ID=0");
|
||||
|
||||
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");
|
||||
var patchVehicle = new VehicleRequest("Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
|
||||
var vehiclePatch = createHttpRequest(patchVehicle, "/vehicles/0", HttpMethod.PATCH);
|
||||
execute(client, vehiclePatch, "PATCH update vehicle ID=0");
|
||||
|
||||
execute(client, new HttpDelete(BASE_URL + "/vehicles/999"), "DELETE vehicle ID=999 (not found)");
|
||||
|
||||
@@ -73,29 +76,20 @@ public class ApplicationClient {
|
||||
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");
|
||||
var newFreight = new FreightRequest("Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
|
||||
var freightPost = createHttpRequest(newFreight, "/freights", HttpMethod.POST);
|
||||
execute(client, freightPost, "POST new freight");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=1");
|
||||
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=0");
|
||||
execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)");
|
||||
|
||||
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");
|
||||
var updateFreight = new FreightRequest("Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
|
||||
var freightPut = createHttpRequest(updateFreight, "/freights/0", HttpMethod.PUT);
|
||||
execute(client, freightPut, "PUT update freight ID=0");
|
||||
|
||||
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");
|
||||
var patchFreight = new FreightRequest("Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
|
||||
var freightPatch = createHttpRequest(patchFreight, "/freights/0", HttpMethod.PATCH);
|
||||
execute(client, freightPatch, "PATCH update freight ID=0");
|
||||
|
||||
execute(client, new HttpDelete(BASE_URL + "/freights/999"), "DELETE freight ID=999 (not found)");
|
||||
|
||||
@@ -105,40 +99,110 @@ public class ApplicationClient {
|
||||
private static void testRoutes(CloseableHttpClient client) throws Exception {
|
||||
System.out.println("--- ROUTES ---");
|
||||
|
||||
var newRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||
var routePost = createHttpRequest(newRoute, "/routes", HttpMethod.POST);
|
||||
execute(client, routePost, "POST new route");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/routes?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");
|
||||
var updateRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
|
||||
var routePut = createHttpRequest(updateRoute, "/routes/0", HttpMethod.PUT);
|
||||
execute(client, routePut, "PUT update route ID=0");
|
||||
|
||||
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");
|
||||
var patchRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
|
||||
var routePatch = createHttpRequest(patchRoute, "/routes/0", HttpMethod.PATCH);
|
||||
execute(client, routePatch, "PATCH update route ID=0");
|
||||
|
||||
execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)");
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static void testSubResources(CloseableHttpClient client) throws Exception {
|
||||
System.out.println("--- SUB-RESOURCES ---");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/routes/0/freights"), "GET freights for route ID=0 (sub-resource)");
|
||||
execute(client, new HttpGet(BASE_URL + "/routes/0/vehicle"), "GET vehicle for route ID=0 (sub-resource)");
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static void testStatusFiltering(CloseableHttpClient client) throws Exception {
|
||||
System.out.println("--- STATUS FILTERING ---");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/vehicles?status=AVAILABLE"), "GET vehicles with status=AVAILABLE");
|
||||
execute(client, new HttpGet(BASE_URL + "/vehicles?status=IN_TRANSIT"), "GET vehicles with status=IN_TRANSIT");
|
||||
execute(client, new HttpGet(BASE_URL + "/freights?status=PENDING"), "GET freights with status=PENDING");
|
||||
execute(client, new HttpGet(BASE_URL + "/freights?status=DELIVERED"), "GET freights with status=DELIVERED");
|
||||
execute(client, new HttpGet(BASE_URL + "/routes?status=PLANNED"), "GET routes with status=PLANNED");
|
||||
execute(client, new HttpGet(BASE_URL + "/routes?status=COMPLETED"), "GET routes with status=COMPLETED");
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static void testErrorCases(CloseableHttpClient client) throws Exception {
|
||||
System.out.println("--- ERROR CASES (TESTING EXCEPTION HANDLING) ---");
|
||||
|
||||
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET non-existent vehicle (404 Not Found)");
|
||||
execute(client, new HttpGet(BASE_URL + "/freights/-1"), "GET freight with invalid ID (404 Not Found)");
|
||||
execute(client, new HttpGet(BASE_URL + "/routes/-1"), "GET route with invalid ID (404 Not Found)");
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static void testInterServiceValidation(CloseableHttpClient client) throws Exception {
|
||||
System.out.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---");
|
||||
|
||||
// Test 1: Create route with valid references (should succeed)
|
||||
System.out.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:");
|
||||
var validRoute = new RouteRequest(0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||
var validRoutePost = createHttpRequest(validRoute, "/routes", HttpMethod.POST);
|
||||
execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
|
||||
|
||||
// Test 2: Create route with invalid vehicle (should fail with 404)
|
||||
System.out.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:");
|
||||
Route invalidVehicleRoute = new Route(0, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||
var invalidRoutePost = createHttpRequest(invalidVehicleRoute, "/routes", HttpMethod.POST);
|
||||
execute(client, invalidRoutePost, "POST route with invalid vehicle ID, should result in 404");
|
||||
|
||||
// Test 3: Create route with invalid freight (should fail with 404)
|
||||
System.out.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:");
|
||||
Route invalidFreightRoute = new Route(0, 0, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||
var invalidFreightPost = createHttpRequest(invalidFreightRoute, "/routes", HttpMethod.POST);
|
||||
execute(client, invalidFreightPost, "POST route with invalid freight ID, should result in 404");
|
||||
}
|
||||
|
||||
private static <T> HttpUriRequest createHttpRequest(T entity, String path, HttpMethod method) {
|
||||
String entityJson = jsonMapper.writeValueAsString(entity);
|
||||
HttpEntityEnclosingRequestBase req;
|
||||
|
||||
if (method == HttpMethod.GET) {
|
||||
return new HttpGet(BASE_URL + path);
|
||||
} else if (method == HttpMethod.POST) {
|
||||
req = new HttpPost(BASE_URL + path);
|
||||
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||
req.setHeader("Content-Type", "application/json");
|
||||
return req;
|
||||
} else if (method == HttpMethod.PUT) {
|
||||
req = new HttpPut(BASE_URL + path);
|
||||
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||
req.setHeader("Content-Type", "application/json");
|
||||
return req;
|
||||
} else if (method == HttpMethod.PATCH) {
|
||||
req = new HttpPatch(BASE_URL + path);
|
||||
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||
req.setHeader("Content-Type", "application/json");
|
||||
return req;
|
||||
} else if (method == HttpMethod.DELETE) {
|
||||
return new HttpDelete(BASE_URL + path);
|
||||
} else {
|
||||
throw new IllegalArgumentException("method is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
|
||||
try {
|
||||
System.out.println("[" + request.getMethod() + "] " + description);
|
||||
@@ -151,14 +215,14 @@ public class ApplicationClient {
|
||||
System.out.println("Response: " + body);
|
||||
}
|
||||
|
||||
if (statusCode >= 400) {
|
||||
System.err.println("ERROR: Request failed with status " + statusCode);
|
||||
if (HttpStatus.valueOf(statusCode).isError()) {
|
||||
System.out.println("ERROR: Request failed with status " + statusCode);
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
System.err.println("EXCEPTION: " + e.getMessage());
|
||||
System.out.println("EXCEPTION: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
System.out.println();
|
||||
return null;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package ua.com.dxrkness.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
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.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ua.com.dxrkness.dto.FreightRequest;
|
||||
import ua.com.dxrkness.dto.FreightResponse;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.service.FreightService;
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.List;
|
||||
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
|
||||
)
|
||||
@Tag(name = "Freight", description = "Freights management")
|
||||
@NullMarked
|
||||
public class FreightController {
|
||||
private final FreightService service;
|
||||
|
||||
@@ -31,19 +30,20 @@ public class FreightController {
|
||||
|
||||
@Operation(
|
||||
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(
|
||||
responseCode = "200",
|
||||
description = "Successfully retrieved list of freights (may be empty!)",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Successfully retrieved list of freights (may be empty!)"
|
||||
)
|
||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||
public List<Freight> getAll() {
|
||||
return service.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).stream().map(FreightResponse::toEntity).toList();
|
||||
}
|
||||
return service.getAll().stream().map(FreightResponse::toEntity).toList();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -52,19 +52,15 @@ public class FreightController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Successfully retrieved freight",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Successfully retrieved freight"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Freight not found"
|
||||
)
|
||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Freight> getById(@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.getById(id));
|
||||
public Freight getById(@PathVariable("id") long id) {
|
||||
return service.getById(id).toEntity();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -73,19 +69,15 @@ public class FreightController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Freight successfully created",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Freight successfully created"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid input"
|
||||
description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
|
||||
)
|
||||
@PostMapping
|
||||
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) {
|
||||
return ResponseEntity.ok(service.add(newFreight));
|
||||
public Freight add(@RequestBody FreightRequest newFreight) {
|
||||
return service.add(newFreight).toEntity();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -94,19 +86,15 @@ public class FreightController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Freight successfully updated",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Freight successfully updated"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid input"
|
||||
)
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Freight> update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
||||
return ResponseEntity.ok(service.update(id, newFreight));
|
||||
public Freight update(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
|
||||
return service.update(id, newFreight).toEntity();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -115,19 +103,15 @@ public class FreightController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Freight successfully updated",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Freight successfully updated"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid input"
|
||||
)
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<Freight> updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
||||
return ResponseEntity.ok(service.update(id, newFreight));
|
||||
public Freight updatePatch(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
|
||||
return service.update(id, newFreight).toEntity();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -136,18 +120,14 @@ public class FreightController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Freight successfully deleted",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = Freight.class)
|
||||
)
|
||||
description = "Freight successfully deleted"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Freight not found"
|
||||
)
|
||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Freight> delete(@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.delete(id));
|
||||
public Freight delete(@PathVariable("id") long id) {
|
||||
return service.delete(id).toEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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 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(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();
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,24 @@ package ua.com.dxrkness.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.tags.Tag;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ua.com.dxrkness.dto.RouteRequest;
|
||||
import ua.com.dxrkness.dto.RouteResponse;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
import ua.com.dxrkness.service.FreightService;
|
||||
import ua.com.dxrkness.service.RouteService;
|
||||
import ua.com.dxrkness.service.VehicleService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
@@ -25,128 +30,48 @@ import java.util.List;
|
||||
@Tag(name = "Route", description = "Routes management")
|
||||
@NullMarked
|
||||
public class RouteController {
|
||||
private final RouteService service;
|
||||
private final RouteService routeService;
|
||||
private final VehicleService vehicleService;
|
||||
private final FreightService freightService;
|
||||
|
||||
public RouteController(RouteService service) {
|
||||
this.service = service;
|
||||
public RouteController(RouteService routeService,
|
||||
VehicleService vehicleService,
|
||||
FreightService freightService) {
|
||||
this.routeService = routeService;
|
||||
this.vehicleService = vehicleService;
|
||||
this.freightService = freightService;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
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(
|
||||
responseCode = "200",
|
||||
description = "Successfully retrieved routes",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = Route.class)
|
||||
)
|
||||
description = "Successfully retrieved routes"
|
||||
)
|
||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||
public List<Route> getAll(
|
||||
@Parameter(description = "Filter routes by freight ID")
|
||||
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
|
||||
@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).stream().map(RouteResponse::toEntity).toList();
|
||||
}
|
||||
if (vehicleId != null) {
|
||||
return service.getByVehicleId(vehicleId);
|
||||
return routeService.getByVehicleId(vehicleId).stream().map(RouteResponse::toEntity).toList();
|
||||
}
|
||||
if (freightId != null) {
|
||||
var route = service.getByFreightId(freightId);
|
||||
var route = routeService.getByFreightId(freightId);
|
||||
if (route == null) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(route);
|
||||
return Stream.of(route).map(RouteResponse::toEntity).toList();
|
||||
}
|
||||
return service.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));
|
||||
return routeService.getAll().stream().map(RouteResponse::toEntity).toList();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -155,40 +80,151 @@ public class RouteController {
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Route found",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = Route.class)
|
||||
)
|
||||
description = "Route found"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Route not found"
|
||||
)
|
||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Route> getById(
|
||||
public Route getById(
|
||||
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.getById(id));
|
||||
return routeService.getById(id).toEntity();
|
||||
}
|
||||
|
||||
@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 vehicleService.getById(routeService.getById(id).vehicleId()).toVehicle();
|
||||
}
|
||||
|
||||
@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(freightService.getById(freightId).toEntity());
|
||||
}
|
||||
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 RouteRequest newRoute) {
|
||||
return routeService.add(newRoute).toEntity();
|
||||
}
|
||||
|
||||
@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 RouteRequest newRoute) {
|
||||
return routeService.update(id, newRoute).toEntity();
|
||||
}
|
||||
|
||||
@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 RouteRequest newRoute) {
|
||||
return routeService.update(id, newRoute).toEntity();
|
||||
}
|
||||
|
||||
|
||||
@Operation(
|
||||
summary = "Delete a route",
|
||||
description = "Delete a route by ID"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Route successfully deleted",
|
||||
content = @Content(schema = @Schema(implementation = Route.class)
|
||||
)
|
||||
description = "Route successfully deleted"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Route not found"
|
||||
)
|
||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Route> delete(
|
||||
public Route delete(
|
||||
@Parameter(description = "ID of the route to delete", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.delete(id));
|
||||
return routeService.delete(id).toEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package ua.com.dxrkness.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.tags.Tag;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ua.com.dxrkness.dto.VehicleRequest;
|
||||
import ua.com.dxrkness.dto.VehicleResponse;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
import ua.com.dxrkness.service.VehicleService;
|
||||
|
||||
@@ -32,82 +32,82 @@ public class VehicleController {
|
||||
|
||||
@Operation(
|
||||
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",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles")
|
||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||
public List<Vehicle> getAll() {
|
||||
return service.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).stream().map(VehicleResponse::toVehicle).toList();
|
||||
}
|
||||
return service.getAll().stream().map(VehicleResponse::toVehicle).toList();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Retrieve a vehicle by ID",
|
||||
description = "Get a single vehicle by its ID"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle found",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle found")
|
||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Vehicle> getById(
|
||||
public Vehicle getById(
|
||||
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.getById(id));
|
||||
return service.getById(id).toVehicle();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Create a new vehicle",
|
||||
description = "Add a new vehicle to the system"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully created",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully created")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||
@PostMapping
|
||||
public ResponseEntity<Vehicle> add(
|
||||
public Vehicle add(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "Vehicle object to be created",
|
||||
required = true,
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@RequestBody Vehicle newVehicle) {
|
||||
return ResponseEntity.ok(service.add(newVehicle));
|
||||
required = true
|
||||
)
|
||||
@RequestBody VehicleRequest newVehicle) {
|
||||
return service.add(newVehicle).toVehicle();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Update a vehicle (full)",
|
||||
description = "Update all fields of an existing vehicle by ID"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Vehicle> update(
|
||||
public Vehicle update(
|
||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||
@PathVariable("id") long id,
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "Updated vehicle object",
|
||||
required = true,
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@RequestBody Vehicle newVehicle) {
|
||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
||||
required = true
|
||||
)
|
||||
@RequestBody VehicleRequest newVehicle) {
|
||||
return service.update(id, newVehicle).toVehicle();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Update a vehicle (partial)",
|
||||
description = "Update specific fields of an existing vehicle by ID"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<Vehicle> updatePatch(
|
||||
public Vehicle updatePatch(
|
||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||
@PathVariable("id") long id,
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "Vehicle object with fields to update",
|
||||
required = true,
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@RequestBody Vehicle newVehicle) {
|
||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
||||
required = true
|
||||
)
|
||||
@RequestBody VehicleRequest newVehicle) {
|
||||
return service.update(id, newVehicle).toVehicle();
|
||||
}
|
||||
|
||||
|
||||
@@ -115,13 +115,12 @@ public class VehicleController {
|
||||
summary = "Delete a vehicle",
|
||||
description = "Delete a vehicle by its ID"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted",
|
||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted")
|
||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Vehicle> delete(
|
||||
public Vehicle delete(
|
||||
@Parameter(description = "ID of the vehicle to delete", required = true)
|
||||
@PathVariable("id") long id) {
|
||||
return ResponseEntity.ofNullable(service.delete(id));
|
||||
return service.delete(id).toVehicle();
|
||||
}
|
||||
}
|
||||
|
||||
34
src/main/java/ua/com/dxrkness/dto/FreightDto.java
Normal file
34
src/main/java/ua/com/dxrkness/dto/FreightDto.java
Normal file
@@ -0,0 +1,34 @@
|
||||
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(
|
||||
String name,
|
||||
String description,
|
||||
int weightKg,
|
||||
Freight.Dimensions dimensions,
|
||||
Freight.Status status
|
||||
) {
|
||||
public static FreightDto fromFreight(Freight freight) {
|
||||
return new FreightDto(
|
||||
freight.name(),
|
||||
freight.description(),
|
||||
freight.weightKg(),
|
||||
freight.dimensions(),
|
||||
freight.status()
|
||||
);
|
||||
}
|
||||
|
||||
public static FreightDto fromResponse(FreightResponse freight) {
|
||||
return new FreightDto(
|
||||
freight.name(),
|
||||
freight.description(),
|
||||
freight.weightKg(),
|
||||
freight.dimensions(),
|
||||
freight.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
19
src/main/java/ua/com/dxrkness/dto/FreightRequest.java
Normal file
19
src/main/java/ua/com/dxrkness/dto/FreightRequest.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record FreightRequest(
|
||||
String name,
|
||||
String description,
|
||||
int weightKg,
|
||||
Freight.Dimensions dimensions,
|
||||
Freight.Status status
|
||||
) {
|
||||
public Freight toEntity() {
|
||||
return new Freight(0, name, description, weightKg, dimensions, status);
|
||||
}
|
||||
}
|
||||
23
src/main/java/ua/com/dxrkness/dto/FreightResponse.java
Normal file
23
src/main/java/ua/com/dxrkness/dto/FreightResponse.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record FreightResponse(
|
||||
String name,
|
||||
String description,
|
||||
int weightKg,
|
||||
Freight.Dimensions dimensions,
|
||||
Freight.Status status
|
||||
) {
|
||||
public Freight toEntity() {
|
||||
return new Freight(0, name, description, weightKg, dimensions, status);
|
||||
}
|
||||
|
||||
public static FreightResponse fromEntity(Freight freight) {
|
||||
return new FreightResponse(freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
||||
}
|
||||
}
|
||||
32
src/main/java/ua/com/dxrkness/dto/RouteDto.java
Normal file
32
src/main/java/ua/com/dxrkness/dto/RouteDto.java
Normal file
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/main/java/ua/com/dxrkness/dto/RouteRequest.java
Normal file
21
src/main/java/ua/com/dxrkness/dto/RouteRequest.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record RouteRequest(long vehicleId,
|
||||
List<Long> freightId,
|
||||
String startLocation,
|
||||
String endLocation,
|
||||
Double distanceKm,
|
||||
Double estimatedDurationHours,
|
||||
Route.Status status) {
|
||||
public Route toEntity() {
|
||||
return new Route(0, vehicleId, freightId, startLocation, endLocation, distanceKm, estimatedDurationHours, status);
|
||||
}
|
||||
}
|
||||
25
src/main/java/ua/com/dxrkness/dto/RouteResponse.java
Normal file
25
src/main/java/ua/com/dxrkness/dto/RouteResponse.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record RouteResponse(long vehicleId,
|
||||
List<Long> freightId,
|
||||
String startLocation,
|
||||
String endLocation,
|
||||
Double distanceKm,
|
||||
Double estimatedDurationHours,
|
||||
Route.Status status) {
|
||||
public Route toEntity() {
|
||||
return new Route(0, vehicleId, freightId, startLocation, endLocation, distanceKm, estimatedDurationHours, status);
|
||||
}
|
||||
|
||||
public static RouteResponse fromRoute(Route route) {
|
||||
return new RouteResponse(route.vehicleId(), route.freightId(), route.startLocation(), route.endLocation(), route.distanceKm(), route.estimatedDurationHours(), route.status());
|
||||
}
|
||||
}
|
||||
52
src/main/java/ua/com/dxrkness/dto/VehicleDto.java
Normal file
52
src/main/java/ua/com/dxrkness/dto/VehicleDto.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record VehicleDto(
|
||||
long id,
|
||||
String brand,
|
||||
String model,
|
||||
String licensePlate,
|
||||
int year,
|
||||
int capacityKg,
|
||||
Vehicle.Status status
|
||||
) {
|
||||
public static VehicleDto fromVehicle(Vehicle vehicle) {
|
||||
return new VehicleDto(
|
||||
vehicle.id(),
|
||||
vehicle.brand(),
|
||||
vehicle.model(),
|
||||
vehicle.licensePlate(),
|
||||
vehicle.year(),
|
||||
vehicle.capacityKg(),
|
||||
vehicle.status()
|
||||
);
|
||||
}
|
||||
|
||||
public static VehicleDto fromResponse(VehicleResponse vehicle) {
|
||||
return new VehicleDto(
|
||||
vehicle.id(),
|
||||
vehicle.brand(),
|
||||
vehicle.model(),
|
||||
vehicle.licensePlate(),
|
||||
vehicle.year(),
|
||||
vehicle.capacityKg(),
|
||||
vehicle.status()
|
||||
);
|
||||
}
|
||||
|
||||
public Vehicle toVehicle() {
|
||||
return new Vehicle(
|
||||
this.id(),
|
||||
this.brand(),
|
||||
this.model(),
|
||||
this.licensePlate(),
|
||||
this.year(),
|
||||
this.capacityKg(),
|
||||
this.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/main/java/ua/com/dxrkness/dto/VehicleRequest.java
Normal file
20
src/main/java/ua/com/dxrkness/dto/VehicleRequest.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package ua.com.dxrkness.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record VehicleRequest(
|
||||
String brand,
|
||||
String model,
|
||||
String licensePlate,
|
||||
int year,
|
||||
int capacityKg,
|
||||
Vehicle.Status status
|
||||
) {
|
||||
public Vehicle toEntity() {
|
||||
return new Vehicle(0, brand, model, licensePlate, year, capacityKg, status);
|
||||
}
|
||||
}
|
||||
40
src/main/java/ua/com/dxrkness/dto/VehicleResponse.java
Normal file
40
src/main/java/ua/com/dxrkness/dto/VehicleResponse.java
Normal file
@@ -0,0 +1,40 @@
|
||||
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 VehicleResponse(
|
||||
long id,
|
||||
String brand,
|
||||
String model,
|
||||
String licensePlate,
|
||||
int year,
|
||||
int capacityKg,
|
||||
Vehicle.Status status
|
||||
) {
|
||||
public static VehicleResponse fromVehicle(Vehicle vehicle) {
|
||||
return new VehicleResponse(
|
||||
vehicle.id(),
|
||||
vehicle.brand(),
|
||||
vehicle.model(),
|
||||
vehicle.licensePlate(),
|
||||
vehicle.year(),
|
||||
vehicle.capacityKg(),
|
||||
vehicle.status()
|
||||
);
|
||||
}
|
||||
|
||||
public Vehicle toVehicle() {
|
||||
return new Vehicle(
|
||||
this.id(),
|
||||
this.brand(),
|
||||
this.model(),
|
||||
this.licensePlate(),
|
||||
this.year(),
|
||||
this.capacityKg(),
|
||||
this.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
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)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record Freight(long id,
|
||||
String name,
|
||||
String description,
|
||||
int weightKg,
|
||||
Dimensions dimensions,
|
||||
Status status) implements Identifiable {
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record Dimensions(int widthCm,
|
||||
int heightCm,
|
||||
int lengthCm) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record Route(long id,
|
||||
long vehicleId,
|
||||
List<Long> freightId,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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)
|
||||
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public record Vehicle(long id,
|
||||
String brand,
|
||||
String model,
|
||||
|
||||
@@ -2,6 +2,9 @@ package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.dto.FreightRequest;
|
||||
import ua.com.dxrkness.dto.FreightResponse;
|
||||
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.repository.FreightRepository;
|
||||
|
||||
@@ -15,25 +18,40 @@ public final class FreightService {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
public List<Freight> getAll() {
|
||||
return repo.getAll();
|
||||
public List<FreightResponse> getAll() {
|
||||
return repo.getAll().stream().map(FreightResponse::fromEntity).toList();
|
||||
}
|
||||
|
||||
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 FreightResponse add(FreightRequest freight) {
|
||||
int id = repo.add(freight.toEntity());
|
||||
return FreightResponse.fromEntity(new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status()));
|
||||
}
|
||||
|
||||
public @Nullable Freight getById(long id) {
|
||||
return repo.getById(id);
|
||||
public FreightResponse getById(long id) {
|
||||
Freight freight = repo.getById(id);
|
||||
if (freight == null) {
|
||||
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
|
||||
}
|
||||
return FreightResponse.fromEntity(freight);
|
||||
}
|
||||
|
||||
public 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 List<FreightResponse> getByStatus(Freight.Status status) {
|
||||
return repo.getAll().stream()
|
||||
.filter(f -> f.status() == status)
|
||||
.map(FreightResponse::fromEntity)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public @Nullable Freight delete(long id) {
|
||||
return repo.delete(id);
|
||||
public FreightResponse update(long id, FreightRequest newFreight) {
|
||||
var newFreightEntity = new Freight(id, newFreight.name(), newFreight.description(), newFreight.weightKg(), newFreight.dimensions(), newFreight.status());
|
||||
return FreightResponse.fromEntity(repo.update(id, newFreightEntity));
|
||||
}
|
||||
|
||||
public FreightResponse delete(long id) {
|
||||
var deleted = repo.delete(id);
|
||||
if (deleted == null) {
|
||||
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
|
||||
}
|
||||
return FreightResponse.fromEntity(deleted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@ package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.dto.FreightDto;
|
||||
import ua.com.dxrkness.dto.RouteRequest;
|
||||
import ua.com.dxrkness.dto.RouteResponse;
|
||||
import ua.com.dxrkness.dto.VehicleDto;
|
||||
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||
import ua.com.dxrkness.exception.RouteNotFoundException;
|
||||
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.repository.RouteRepository;
|
||||
|
||||
@@ -10,41 +17,88 @@ import java.util.List;
|
||||
@Service
|
||||
public final class RouteService {
|
||||
private final RouteRepository repo;
|
||||
private final VehicleService vehicleService;
|
||||
private final FreightService freightService;
|
||||
|
||||
public RouteService(RouteRepository repo) {
|
||||
public RouteService(RouteRepository repo,
|
||||
VehicleService vehicleService,
|
||||
FreightService freightService) {
|
||||
this.repo = repo;
|
||||
this.vehicleService = vehicleService;
|
||||
this.freightService = freightService;
|
||||
}
|
||||
|
||||
public List<Route> getAll() {
|
||||
return repo.getAll();
|
||||
public List<RouteResponse> getAll() {
|
||||
return repo.getAll().stream().map(RouteResponse::fromRoute).toList();
|
||||
}
|
||||
|
||||
public @Nullable Route getById(long id) {
|
||||
return repo.getById(id);
|
||||
public RouteResponse getById(long id) {
|
||||
Route route = repo.getById(id);
|
||||
if (route == null) {
|
||||
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||
}
|
||||
return RouteResponse.fromRoute(route);
|
||||
}
|
||||
|
||||
public Route add(Route route) {
|
||||
int id = repo.add(route);
|
||||
return new Route(id,
|
||||
public RouteResponse add(RouteRequest route) {
|
||||
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
|
||||
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
|
||||
|
||||
int id = repo.add(route.toEntity());
|
||||
return RouteResponse.fromRoute(new Route(id,
|
||||
route.vehicleId(),
|
||||
route.freightId(),
|
||||
route.startLocation(),
|
||||
route.endLocation(),
|
||||
route.distanceKm(),
|
||||
route.estimatedDurationHours(),
|
||||
route.status());
|
||||
route.status()));
|
||||
}
|
||||
|
||||
public @Nullable Route getByFreightId(long freightId) {
|
||||
return repo.getByFreightId(freightId);
|
||||
private VehicleDto validateAndGetVehicle(long vehicleId) {
|
||||
try {
|
||||
var vehicle = vehicleService.getById(vehicleId);
|
||||
return VehicleDto.fromResponse(vehicle);
|
||||
} catch (VehicleNotFoundException e) {
|
||||
throw new VehicleNotFoundException(
|
||||
"Cannot create or update route: Referenced vehicle with ID " + vehicleId + " not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public List<Route> getByVehicleId(long vehicleId) {
|
||||
return repo.getByVehicleId(vehicleId);
|
||||
private List<FreightDto> validateAndGetFreights(List<Long> freightIds) {
|
||||
List<FreightDto> freightDtos = new java.util.ArrayList<>();
|
||||
for (long freightId : freightIds) {
|
||||
try {
|
||||
var freight = freightService.getById(freightId);
|
||||
freightDtos.add(FreightDto.fromResponse(freight));
|
||||
} catch (FreightNotFoundException e) {
|
||||
throw new FreightNotFoundException(
|
||||
"Cannot create or update route: Referenced freight with ID " + freightId + " not found.");
|
||||
}
|
||||
}
|
||||
return freightDtos;
|
||||
}
|
||||
|
||||
public Route update(long id, Route newRoute) {
|
||||
newRoute = new Route(id,
|
||||
public @Nullable RouteResponse getByFreightId(long freightId) {
|
||||
return RouteResponse.fromRoute(repo.getByFreightId(freightId));
|
||||
}
|
||||
|
||||
public List<RouteResponse> getByVehicleId(long vehicleId) {
|
||||
return repo.getByVehicleId(vehicleId).stream().map(RouteResponse::fromRoute).toList();
|
||||
}
|
||||
|
||||
public List<RouteResponse> getByStatus(Route.Status status) {
|
||||
return repo.getAll().stream()
|
||||
.filter(r -> r.status() == status)
|
||||
.map(RouteResponse::fromRoute)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public RouteResponse update(long id, RouteRequest newRoute) {
|
||||
VehicleDto vehicleDto = validateAndGetVehicle(newRoute.vehicleId());
|
||||
List<FreightDto> freightDtos = validateAndGetFreights(newRoute.freightId());
|
||||
|
||||
var newRouteEntity = new Route(id,
|
||||
newRoute.vehicleId(),
|
||||
newRoute.freightId(),
|
||||
newRoute.startLocation(),
|
||||
@@ -52,10 +106,14 @@ public final class RouteService {
|
||||
newRoute.distanceKm(),
|
||||
newRoute.estimatedDurationHours(),
|
||||
newRoute.status());
|
||||
return repo.update(id, newRoute);
|
||||
return RouteResponse.fromRoute(repo.update(id, newRouteEntity));
|
||||
}
|
||||
|
||||
public @Nullable Route delete(long id) {
|
||||
return repo.delete(id);
|
||||
public RouteResponse delete(long id) {
|
||||
var deleted = repo.delete(id);
|
||||
if (deleted == null) {
|
||||
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||
}
|
||||
return RouteResponse.fromRoute(deleted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ua.com.dxrkness.service;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ua.com.dxrkness.dto.VehicleRequest;
|
||||
import ua.com.dxrkness.dto.VehicleResponse;
|
||||
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||
import ua.com.dxrkness.model.Vehicle;
|
||||
import ua.com.dxrkness.repository.VehicleRepository;
|
||||
|
||||
@@ -15,31 +17,46 @@ public final class VehicleService {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
public List<Vehicle> getAll() {
|
||||
return repo.getAll();
|
||||
public List<VehicleResponse> getAll() {
|
||||
return repo.getAll().stream().map(VehicleResponse::fromVehicle).toList();
|
||||
}
|
||||
|
||||
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 List<VehicleResponse> getByStatus(Vehicle.Status status) {
|
||||
return repo.getAll().stream()
|
||||
.filter(v -> v.status() == status)
|
||||
.map(VehicleResponse::fromVehicle)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public @Nullable Vehicle getById(long id) {
|
||||
return repo.getById(id);
|
||||
public VehicleResponse add(VehicleRequest veh) {
|
||||
int id = repo.add(veh.toEntity());
|
||||
return new VehicleResponse(id, veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
|
||||
}
|
||||
|
||||
public Vehicle update(long id, Vehicle newVehicle) {
|
||||
newVehicle = new Vehicle(id,
|
||||
public VehicleResponse getById(long id) {
|
||||
Vehicle vehicle = repo.getById(id);
|
||||
if (vehicle == null) {
|
||||
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||
}
|
||||
return VehicleResponse.fromVehicle(vehicle);
|
||||
}
|
||||
|
||||
public VehicleResponse update(long id, VehicleRequest newVehicle) {
|
||||
var newVehicleEntity = new Vehicle(id,
|
||||
newVehicle.brand(),
|
||||
newVehicle.model(),
|
||||
newVehicle.licensePlate(),
|
||||
newVehicle.year(),
|
||||
newVehicle.capacityKg(),
|
||||
newVehicle.status());
|
||||
return repo.update(id, newVehicle);
|
||||
return VehicleResponse.fromVehicle(repo.update(id, newVehicleEntity));
|
||||
}
|
||||
|
||||
public @Nullable Vehicle delete(long id) {
|
||||
return repo.delete(id);
|
||||
public VehicleResponse delete(long id) {
|
||||
var deleted = repo.delete(id);
|
||||
if (deleted == null) {
|
||||
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||
}
|
||||
return VehicleResponse.fromVehicle(deleted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
then(body).isNotNull();
|
||||
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();
|
||||
then(body).isNotNull();
|
||||
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,113 @@
|
||||
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.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import ua.com.dxrkness.model.Freight;
|
||||
import ua.com.dxrkness.model.Route;
|
||||
import ua.com.dxrkness.service.PopulateService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
@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();
|
||||
then(body).isNotNull();
|
||||
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();
|
||||
then(body).isNotNull();
|
||||
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();
|
||||
then(body).isNotNull();
|
||||
then(body).contains("Not Found");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user