diff --git a/desm-client/src/main/java/org/example/desm/client/ApiClient.java b/desm-client/src/main/java/org/example/desm/client/ApiClient.java new file mode 100644 index 0000000..455ada5 --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/ApiClient.java @@ -0,0 +1,91 @@ +package org.example.desm.client; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.example.desm.common.model.AverageEmission; +import org.example.desm.common.model.ErrorResponse; +import org.example.desm.common.model.PowerPlant; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.example.desm.common.Constant.API_BASE_URL; +import static org.example.desm.common.Constant.ENDPOINT_PLANTS; +import static org.example.desm.common.Constant.ENDPOINT_POLLUTION; + +/** + * ApiClient is a utility class for making HTTP requests to the Administration Server. + * It provides methods to fetch all power plants and to get average emissions + * within a specified time range. + */ +public class ApiClient { + private static final Gson gson = new Gson(); + private static final HttpClient client = HttpClient.newHttpClient(); + + /** + * Fetches all power plants. + * + * @return a list of PowerPlant objects. + * @throws IOException if an I/O error occurs during the request. + * @throws InterruptedException if the request is interrupted. + * @throws ResponseErrorExceptions if the response indicates an error. + */ + public static List getAllPlants() + throws IOException, InterruptedException, ResponseErrorExceptions { + String uri = API_BASE_URL + ENDPOINT_PLANTS; + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .GET() + .build(); + HttpResponse response = client.send(req, HttpResponse.BodyHandlers.ofString()); + validateResponse(response); + + Type listType = new TypeToken>() {}.getType(); + return gson.fromJson(response.body(), listType); + } + + /** + * Fetches the average emissions for a specified time range. + * + * @param t1 the start time in milliseconds since epoch. + * @param t2 the end time in milliseconds since epoch. + * @return an AverageEmission object containing the average emission's data. + * @throws IOException if an I/O error occurs during the request. + * @throws InterruptedException if the request is interrupted. + * @throws ResponseErrorExceptions if the response indicates an error. + */ + public static AverageEmission getAverageEmission(long t1, long t2) + throws IOException, InterruptedException, ResponseErrorExceptions { + String uri = API_BASE_URL + ENDPOINT_POLLUTION + "?t1=" + t1 + "&t2=" + t2; + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .GET() + .build(); + + HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); + validateResponse(resp); + + return gson.fromJson(resp.body(), AverageEmission.class); + } + + /** + * Validates the HTTP response status code. + * + * @param response the HttpResponse to validate. + * @throws ResponseErrorExceptions if the response status code indicates an error. + */ + private static void validateResponse(HttpResponse response) throws ResponseErrorExceptions { + int status = response.statusCode(); + if (status < 200 || status >= 300) { + ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class); + throw new ResponseErrorExceptions(error); + } + } +} diff --git a/desm-client/src/main/java/org/example/desm/client/Client.java b/desm-client/src/main/java/org/example/desm/client/Client.java new file mode 100644 index 0000000..f61d5df --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/Client.java @@ -0,0 +1,87 @@ +package org.example.desm.client; + +import org.example.desm.client.command.AvailablePowerPlant; +import org.example.desm.client.command.Command; +import org.example.desm.client.command.QueryAverageEmission; + +import java.util.Map; +import java.util.Scanner; +import java.util.TreeMap; + +public class Client { + public final Scanner scanner; + private final Map commands; + + /** + * Constructs a Client with default commands. + */ + public Client() { + this.scanner = new Scanner(System.in); + this.commands = new TreeMap<>(); + initDefaultCommands(); + } + + /** + * Initializes the default set of commands. + */ + private void initDefaultCommands() { + int i = 0; + registerCommand(new AvailablePowerPlant( + ++i, "View all power plants", scanner)); + registerCommand(new QueryAverageEmission( + ++i, "Query average emission of power plants", scanner)); + } + + /** + * Register a command in the command map. + * + * @param command Command to register + */ + private void registerCommand(Command command) { + commands.put(command.getId(), command); + } + + /** + * Starts the client, displaying the menu and processing user input. + */ + public void start() { + while (true) { + printMenu(); + System.out.print("Choose: "); + String input = scanner.nextLine().trim(); + + try { + int choice = Integer.parseInt(input); + if (choice == 0) { + System.out.println("Exiting..."); + break; + } + + Command command = commands.get(choice); + if (command != null) { + command.execute(); + } else { + System.out.println("Invalid option."); + } + } catch (NumberFormatException e) { + System.out.println("Please enter a valid number."); + } + } + scanner.close(); + } + + /** + * Prints the menu with available commands. + */ + private void printMenu() { + System.out.println("\n===== Administration Client ====="); + for (Command command : commands.values()) { + System.out.println(command.getId() + ". " + command.getName()); + } + System.out.println("0. Exit"); + } + + public static void main(String[] args) { + new Client().start(); + } +} diff --git a/desm-client/src/main/java/org/example/desm/client/ResponseErrorExceptions.java b/desm-client/src/main/java/org/example/desm/client/ResponseErrorExceptions.java new file mode 100644 index 0000000..800b1c1 --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/ResponseErrorExceptions.java @@ -0,0 +1,12 @@ +package org.example.desm.client; + +import org.example.desm.common.model.ErrorResponse; + +/** + * Custom {@link Exception} which is thrown when there is an error response from the server. + */ +public class ResponseErrorExceptions extends Exception { + public ResponseErrorExceptions(ErrorResponse error) { + super(error.toString()); + } +} diff --git a/desm-client/src/main/java/org/example/desm/client/command/AvailablePowerPlant.java b/desm-client/src/main/java/org/example/desm/client/command/AvailablePowerPlant.java new file mode 100644 index 0000000..08c76c6 --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/command/AvailablePowerPlant.java @@ -0,0 +1,36 @@ +package org.example.desm.client.command; + +import org.example.desm.client.ApiClient; +import org.example.desm.client.ResponseErrorExceptions; +import org.example.desm.common.model.PowerPlant; + +import java.util.List; +import java.util.Scanner; + +/** + * Custom {@link Command} to retrieve and display all available power plants. + * This command fetches the list of power plants from the API and prints them. + */ +public class AvailablePowerPlant extends Command { + public AvailablePowerPlant(int id, String name, Scanner scanner) { + super(id, name, scanner); + } + + @Override + public void execute() { + try { + List plants = ApiClient.getAllPlants(); + if (plants.isEmpty()) { + System.out.println("No power plants registered."); + } else { + for (PowerPlant plant : plants) { + System.out.println(plant); + } + } + } catch (ResponseErrorExceptions e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + System.out.println("Unable to retrieve plants!"); + } + } +} diff --git a/desm-client/src/main/java/org/example/desm/client/command/Command.java b/desm-client/src/main/java/org/example/desm/client/command/Command.java new file mode 100644 index 0000000..6fddebc --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/command/Command.java @@ -0,0 +1,27 @@ +package org.example.desm.client.command; + +import lombok.Getter; + +import java.util.Scanner; + +/** + * Abstract class that represents a command in the Administration Client. + * Each command has an ID, a name, and a scanner for input. + */ +@Getter +public abstract class Command { + private final int id; + private final String name; + protected final Scanner scanner; + + public Command(int id, String name, Scanner scanner) { + this.id = id; + this.name = name; + this.scanner = scanner; + } + + /** + * Executes the command. + */ + public abstract void execute(); +} diff --git a/desm-client/src/main/java/org/example/desm/client/command/QueryAverageEmission.java b/desm-client/src/main/java/org/example/desm/client/command/QueryAverageEmission.java new file mode 100644 index 0000000..e3a30a9 --- /dev/null +++ b/desm-client/src/main/java/org/example/desm/client/command/QueryAverageEmission.java @@ -0,0 +1,38 @@ +package org.example.desm.client.command; + +import org.example.desm.client.ApiClient; +import org.example.desm.client.ResponseErrorExceptions; +import org.example.desm.common.model.AverageEmission; + +import java.util.Scanner; + +/** + * Custom {@link Command} to query the average emissions between two timestamps. + * This command prompts the user for start and end timestamps, retrieves the average emissions from the API, + * and prints the result. + */ +public class QueryAverageEmission extends Command { + public QueryAverageEmission(int id, String name, Scanner scanner) { + super(id, name, scanner); + } + + @Override + public void execute() { + try { + System.out.print("Enter start timestamp: "); + long t1 = Long.parseLong(scanner.nextLine().trim()); + + System.out.print("Enter end timestamp: "); + long t2 = Long.parseLong(scanner.nextLine().trim()); + + AverageEmission result = ApiClient.getAverageEmission(t1, t2); + System.out.println(result.toString()); + } catch (NumberFormatException e) { + System.out.println("Timestamps must be numeric."); + } catch (ResponseErrorExceptions e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + System.out.println("Failed to query emissions!"); + } + } +} diff --git a/desm-client/src/test/java/org/example/desm/client/ClientTest.java b/desm-client/src/test/java/org/example/desm/client/ClientTest.java new file mode 100644 index 0000000..438cc40 --- /dev/null +++ b/desm-client/src/test/java/org/example/desm/client/ClientTest.java @@ -0,0 +1,65 @@ +package org.example.desm.client; + +import org.example.desm.common.model.AverageEmission; +import org.example.desm.common.model.PowerPlant; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mockStatic; + +@ExtendWith(MockitoExtension.class) +class ClientTest { + @Test + void testAvailablePowerPlantCommand() { + List mockPlants = List.of( + new PowerPlant( "plant-1", "localhost", 8080), + new PowerPlant( "plant-2", "localhost", 8081) + ); + + // Simulate command 1 and then 0 to exit + String commandInput = "1\n0\n"; + System.setIn(new ByteArrayInputStream(commandInput.getBytes())); + + try (MockedStatic apiClientMockedStatic = mockStatic(ApiClient.class)) { + apiClientMockedStatic.when(ApiClient::getAllPlants) + .thenReturn(mockPlants); + + new Client().start(); + + apiClientMockedStatic.verify(ApiClient::getAllPlants); + } + } + + @Test + void testQueryAverageEmissionCommand() { + long t1 = 1000L; + long t2 = 2000L; + AverageEmission mockEmission = new AverageEmission(t1, t2, 10.0); + + // Simulate command 2 and then 0 to exit + String commandInput = String.format("2\n%d\n%d\n0\n", t1, t2); + System.setIn(new ByteArrayInputStream(commandInput.getBytes())); + + try (MockedStatic apiClientMockedStatic = mockStatic(ApiClient.class)) { + apiClientMockedStatic.when(() -> ApiClient.getAverageEmission(t1, t2)) + .thenReturn(mockEmission); + + new Client().start(); + + apiClientMockedStatic.verify(() -> ApiClient.getAverageEmission(t1, t2)); + } + } + + @Test + void testInvalidCommandInput() { + String commandInput = "x\n0\n"; + System.setIn(new ByteArrayInputStream(commandInput.getBytes())); + assertDoesNotThrow(() -> new Client().start()); + } +} \ No newline at end of file diff --git a/desm-core/src/main/java/org/example/desm/common/Constant.java b/desm-core/src/main/java/org/example/desm/common/Constant.java index a6ee589..de36454 100644 --- a/desm-core/src/main/java/org/example/desm/common/Constant.java +++ b/desm-core/src/main/java/org/example/desm/common/Constant.java @@ -2,6 +2,7 @@ public class Constant { public static final String MQTT_BROKER_ADDRESS = "tcp://localhost:1883"; + public static final String API_BASE_URL = "http://localhost:8080"; // MQTT topics public static final String TOPIC_ENERGY_REQUEST = "energy/request"; diff --git a/desm-core/src/main/java/org/example/desm/common/model/AverageEmission.java b/desm-core/src/main/java/org/example/desm/common/model/AverageEmission.java index b0e8733..ce7bf52 100644 --- a/desm-core/src/main/java/org/example/desm/common/model/AverageEmission.java +++ b/desm-core/src/main/java/org/example/desm/common/model/AverageEmission.java @@ -11,4 +11,10 @@ public class AverageEmission { private long startTimestamp; private long endTimestamp; private double average_co2; + + @Override + public String toString() { + return "[from: " + startTimestamp + ", to: " + endTimestamp + "] " + + "avg CO2: " + average_co2 + "g"; + } } diff --git a/desm-core/src/main/java/org/example/desm/common/model/ErrorResponse.java b/desm-core/src/main/java/org/example/desm/common/model/ErrorResponse.java index 6d9a706..d252805 100644 --- a/desm-core/src/main/java/org/example/desm/common/model/ErrorResponse.java +++ b/desm-core/src/main/java/org/example/desm/common/model/ErrorResponse.java @@ -11,4 +11,9 @@ public class ErrorResponse { private int status; private String error; private String message; + + @Override + public String toString() { + return "[" + status + "] " + error + ": " + message; + } } diff --git a/desm-core/src/main/java/org/example/desm/common/model/MeasurementsWrapper.java b/desm-core/src/main/java/org/example/desm/common/model/MeasurementsWrapper.java deleted file mode 100644 index 2a45b3a..0000000 --- a/desm-core/src/main/java/org/example/desm/common/model/MeasurementsWrapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.desm.common.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import org.example.desm.common.simulator.Measurement; - -import java.util.List; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public class MeasurementsWrapper { - private List measurements; -} diff --git a/desm-core/src/main/java/org/example/desm/common/model/PowerPlant.java b/desm-core/src/main/java/org/example/desm/common/model/PowerPlant.java index bba5b56..5def2ff 100644 --- a/desm-core/src/main/java/org/example/desm/common/model/PowerPlant.java +++ b/desm-core/src/main/java/org/example/desm/common/model/PowerPlant.java @@ -11,4 +11,9 @@ public class PowerPlant { private String id; private String address; private int port; + + @Override + public String toString() { + return "id: " + id + " — " + address + ":" + port; + } } diff --git a/desm-core/src/main/java/org/example/desm/common/model/PowerRequest.java b/desm-core/src/main/java/org/example/desm/common/model/PowerRequest.java index da5267e..2ab6a55 100644 --- a/desm-core/src/main/java/org/example/desm/common/model/PowerRequest.java +++ b/desm-core/src/main/java/org/example/desm/common/model/PowerRequest.java @@ -8,6 +8,6 @@ @AllArgsConstructor @Data public class PowerRequest { - private int power; + private int kwhPower; private long timestamp; } diff --git a/desm-provider/src/main/java/org/example/desm/provider/PeriodicPowerRequestSimulator.java b/desm-provider/src/main/java/org/example/desm/provider/PeriodicPowerRequestSimulator.java index 5a93ff9..dfa47df 100644 --- a/desm-provider/src/main/java/org/example/desm/provider/PeriodicPowerRequestSimulator.java +++ b/desm-provider/src/main/java/org/example/desm/provider/PeriodicPowerRequestSimulator.java @@ -13,8 +13,8 @@ * to a specified MQTT topic at regular intervals. */ public class PeriodicPowerRequestSimulator extends Thread { - private static final int DEFAULT_INTERVAL_MS = 10000; - private static final int DEFAULT_QOS = 2; + private static final int INTERVAL_MS = 10000; + private static final int PUB_QOS = 2; private static final int MIN_POWER = 5000; private static final int MAX_POWER = 15000; @@ -53,7 +53,7 @@ public PeriodicPowerRequestSimulator( * @param topic the topic to publish energy requests to. */ public PeriodicPowerRequestSimulator(IMqttClient client, String topic) { - this(client, topic, DEFAULT_QOS, DEFAULT_INTERVAL_MS); + this(client, topic, PUB_QOS, INTERVAL_MS); } /** diff --git a/desm-provider/src/main/java/org/example/desm/provider/RenewableEnergyProvider.java b/desm-provider/src/main/java/org/example/desm/provider/RenewableEnergyProvider.java index 761e25a..b9dda24 100644 --- a/desm-provider/src/main/java/org/example/desm/provider/RenewableEnergyProvider.java +++ b/desm-provider/src/main/java/org/example/desm/provider/RenewableEnergyProvider.java @@ -86,8 +86,7 @@ public void start() { public static void main(String[] args) { try { - RenewableEnergyProvider provider = new RenewableEnergyProvider(); - provider.start(); + new RenewableEnergyProvider().start(); } catch (MqttException e) { System.out.println("Initialization error in RenewableEnergyProvider."); System.out.printf("Message: %s%n", e.getMessage()); diff --git a/desm-provider/src/test/java/org/example/desm/provider/PeriodicPowerRequestSimulatorTest.java b/desm-provider/src/test/java/org/example/desm/provider/PeriodicPowerRequestSimulatorTest.java index f8db2fc..8797bce 100644 --- a/desm-provider/src/test/java/org/example/desm/provider/PeriodicPowerRequestSimulatorTest.java +++ b/desm-provider/src/test/java/org/example/desm/provider/PeriodicPowerRequestSimulatorTest.java @@ -58,7 +58,7 @@ void testRunPublishesMessage() throws MqttException, InterruptedException { PowerRequest request = new Gson().fromJson(payload, PowerRequest.class); // Check power is within the defined range - assertTrue(request.getPower() >= 5000 && request.getPower() <= 15000); + assertTrue(request.getKwhPower() >= 5000 && request.getKwhPower() <= 15000); // Check timestamp recent long now = System.currentTimeMillis(); diff --git a/desm-server/src/main/java/org/example/desm/server/GlobalControllerAdvice.java b/desm-server/src/main/java/org/example/desm/server/GlobalControllerAdvice.java index 9ee61e1..f1742ae 100644 --- a/desm-server/src/main/java/org/example/desm/server/GlobalControllerAdvice.java +++ b/desm-server/src/main/java/org/example/desm/server/GlobalControllerAdvice.java @@ -1,7 +1,7 @@ package org.example.desm.server; -import org.example.desm.server.exception.ApiExceptions; import org.example.desm.common.model.ErrorResponse; +import org.example.desm.server.exception.ApiExceptions; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; diff --git a/desm-server/src/main/java/org/example/desm/server/component/PollutionSubscriber.java b/desm-server/src/main/java/org/example/desm/server/component/PollutionSubscriber.java index 25aa5c9..5b8d79e 100644 --- a/desm-server/src/main/java/org/example/desm/server/component/PollutionSubscriber.java +++ b/desm-server/src/main/java/org/example/desm/server/component/PollutionSubscriber.java @@ -1,5 +1,7 @@ package org.example.desm.server.component; +import com.google.gson.reflect.TypeToken; +import org.example.desm.common.simulator.Measurement; import org.example.desm.server.service.PollutionService; import com.google.gson.Gson; import jakarta.annotation.PostConstruct; @@ -11,10 +13,12 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.example.desm.common.model.MeasurementsWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.lang.reflect.Type; +import java.util.List; + import static org.example.desm.common.Constant.TOPIC_CO2_MEASUREMENT; import static org.example.desm.common.Constant.MQTT_BROKER_ADDRESS; @@ -26,7 +30,7 @@ */ @Component public class PollutionSubscriber implements MqttCallback { - private static final int DEFAULT_QOS = 2; + private static final int SUB_QOS = 2; private final Gson gson = new Gson(); @@ -101,7 +105,7 @@ private void connect() throws MqttException { private void subscribe() throws MqttException { // Subscribe to the CO2 measurement topic System.out.printf("[%s] Subscribing to topic %s...%n", client.getClientId(), TOPIC_CO2_MEASUREMENT); - client.subscribe(TOPIC_CO2_MEASUREMENT, DEFAULT_QOS); + client.subscribe(TOPIC_CO2_MEASUREMENT, SUB_QOS); System.out.printf("[%s] Subscribed successfully.%n", client.getClientId()); } @@ -114,8 +118,9 @@ public void connectionLost(Throwable cause) { @Override public void messageArrived(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - MeasurementsWrapper measurements = gson.fromJson(payload, MeasurementsWrapper.class); - pollutionService.registerMeasurements(measurements.getMeasurements()); + Type listType = new TypeToken>() {}.getType(); + List measurements = gson.fromJson(payload, listType); + pollutionService.registerMeasurements(measurements); } @Override diff --git a/desm-server/src/main/java/org/example/desm/server/controller/PollutionController.java b/desm-server/src/main/java/org/example/desm/server/controller/PollutionController.java index d61d015..ed638d7 100644 --- a/desm-server/src/main/java/org/example/desm/server/controller/PollutionController.java +++ b/desm-server/src/main/java/org/example/desm/server/controller/PollutionController.java @@ -11,7 +11,7 @@ import static org.example.desm.common.Constant.ENDPOINT_POLLUTION; /** - * Controller for handling pollution related requests. + * {@link RestController} for handling pollution related requests. */ @RestController @RequestMapping(ENDPOINT_POLLUTION) diff --git a/desm-server/src/main/java/org/example/desm/server/controller/PowerPlantController.java b/desm-server/src/main/java/org/example/desm/server/controller/PowerPlantController.java index fc73b41..8038694 100644 --- a/desm-server/src/main/java/org/example/desm/server/controller/PowerPlantController.java +++ b/desm-server/src/main/java/org/example/desm/server/controller/PowerPlantController.java @@ -15,16 +15,14 @@ import static org.example.desm.common.Constant.ENDPOINT_REGISTER_PLANT; /** - * Controller for handling power plant related requests. + * {@link RestController} for handling power plant related requests. */ @RestController @RequestMapping(ENDPOINT_PLANTS) public class PowerPlantController { private final PowerPlantService powerPlantService; - public PowerPlantController( - PowerPlantService powerPlantService - ) { + public PowerPlantController(PowerPlantService powerPlantService) { this.powerPlantService = powerPlantService; } @@ -50,6 +48,7 @@ public ResponseEntity> registerPlant( @RequestBody PowerPlant plant ) { powerPlantService.registerPlant(plant); - return ResponseEntity.ok(powerPlantService.getAllPlants()); + List plants = powerPlantService.getAllPlants(); + return ResponseEntity.ok(plants); } } diff --git a/desm-server/src/main/java/org/example/desm/server/exception/ApiExceptions.java b/desm-server/src/main/java/org/example/desm/server/exception/ApiExceptions.java index a1cbe2a..5d06cb5 100644 --- a/desm-server/src/main/java/org/example/desm/server/exception/ApiExceptions.java +++ b/desm-server/src/main/java/org/example/desm/server/exception/ApiExceptions.java @@ -1,16 +1,23 @@ package org.example.desm.server.exception; +import lombok.Getter; import org.example.desm.common.model.ErrorResponse; import org.springframework.http.HttpStatus; /** * Base class for all API exceptions. + *

* This class extends {@link RuntimeException} and provides a method to build an {@link ErrorResponse} object. - * It also defines abstract methods to get the HTTP status code and error message. */ public abstract class ApiExceptions extends RuntimeException { - public ApiExceptions(String message) { + @Getter + private final HttpStatus statusCode; + private final String error; + + public ApiExceptions(HttpStatus statusCode, String error, String message) { super(message); + this.statusCode = statusCode; + this.error = error; } /** @@ -21,23 +28,9 @@ public ApiExceptions(String message) { */ public ErrorResponse buildResponse() { return new ErrorResponse( - getStatusCode().value(), - getError(), + statusCode.value(), + error, getMessage() ); } - - /** - * Method to get the HTTP status code. - * - * @return the HTTP status code. - */ - public abstract HttpStatus getStatusCode(); - - /** - * Method to get the error message. - * - * @return the error message. - */ - public abstract String getError(); } diff --git a/desm-server/src/main/java/org/example/desm/server/exception/InvalidPowerPlantParameterException.java b/desm-server/src/main/java/org/example/desm/server/exception/InvalidPowerPlantParameterException.java new file mode 100644 index 0000000..40f2438 --- /dev/null +++ b/desm-server/src/main/java/org/example/desm/server/exception/InvalidPowerPlantParameterException.java @@ -0,0 +1,12 @@ +package org.example.desm.server.exception; + +import org.springframework.http.HttpStatus; + +/** + * Custom {@link ApiExceptions} for handling invalid power plant parameters. + */ +public class InvalidPowerPlantParameterException extends ApiExceptions { + public InvalidPowerPlantParameterException(String message) { + super(HttpStatus.BAD_REQUEST, "Invalid power plant parameter", message); + } +} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/InvalidTimeIntervalException.java b/desm-server/src/main/java/org/example/desm/server/exception/InvalidTimeIntervalException.java new file mode 100644 index 0000000..662758d --- /dev/null +++ b/desm-server/src/main/java/org/example/desm/server/exception/InvalidTimeIntervalException.java @@ -0,0 +1,16 @@ +package org.example.desm.server.exception; + +import org.springframework.http.HttpStatus; + +/** + * Custom {@link ApiExceptions} for handling invalid time intervals. + */ +public class InvalidTimeIntervalException extends ApiExceptions { + public InvalidTimeIntervalException() { + super( + HttpStatus.BAD_REQUEST, + "Invalid time interval", + "Invalid time interval, start time must be less than or equal to end time." + ); + } +} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/PollutionReadOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/PollutionReadOperationException.java deleted file mode 100644 index 71aecf1..0000000 --- a/desm-server/src/main/java/org/example/desm/server/exception/PollutionReadOperationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.desm.server.exception; - -/** - * Custom {@link ReadOperationException} for handling errors related to pollution read operations. - */ -public class PollutionReadOperationException extends ReadOperationException { - public PollutionReadOperationException() { - super("Unable to retrieve pollution data at this time."); - } -} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/PollutionWriteOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/PollutionWriteOperationException.java deleted file mode 100644 index a29cfbf..0000000 --- a/desm-server/src/main/java/org/example/desm/server/exception/PollutionWriteOperationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.desm.server.exception; - -/** - * Custom {@link WriteOperationException} for handling errors related to pollution write operations. - */ -public class PollutionWriteOperationException extends WriteOperationException { - public PollutionWriteOperationException() { - super("Unable to save the pollution data at this time."); - } -} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantAlreadyExistsException.java b/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantAlreadyExistsException.java index 7d96844..d75e0cf 100644 --- a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantAlreadyExistsException.java +++ b/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantAlreadyExistsException.java @@ -7,16 +7,10 @@ */ public class PowerPlantAlreadyExistsException extends ApiExceptions { public PowerPlantAlreadyExistsException(String id) { - super("Power plant with ID " + id + " already exists."); - } - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.CONFLICT; - } - - @Override - public String getError() { - return "Plant Already Exists"; + super( + HttpStatus.CONFLICT, + "Plant already exists", + "Power plant with ID " + id + " already exists." + ); } } \ No newline at end of file diff --git a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantReadOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantReadOperationException.java deleted file mode 100644 index a0ead9c..0000000 --- a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantReadOperationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.desm.server.exception; - -/** - * Custom {@link ReadOperationException} for handling errors related to power plant read operations. - */ -public class PowerPlantReadOperationException extends ReadOperationException { - public PowerPlantReadOperationException() { - super("Unable to retrieve power plant data at this time."); - } -} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantWriteOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantWriteOperationException.java deleted file mode 100644 index 1e81d6a..0000000 --- a/desm-server/src/main/java/org/example/desm/server/exception/PowerPlantWriteOperationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.desm.server.exception; - -/** - * Custom {@link WriteOperationException} for handling errors related to power plant write operations. - */ -public class PowerPlantWriteOperationException extends WriteOperationException { - public PowerPlantWriteOperationException() { - super("Unable to register the power plant at this time."); - } -} diff --git a/desm-server/src/main/java/org/example/desm/server/exception/ReadOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/ReadOperationException.java index f9c0c8b..1a68e8a 100644 --- a/desm-server/src/main/java/org/example/desm/server/exception/ReadOperationException.java +++ b/desm-server/src/main/java/org/example/desm/server/exception/ReadOperationException.java @@ -3,20 +3,10 @@ import org.springframework.http.HttpStatus; /** - * Abstract custom {@link ApiExceptions} for handling errors related to read operations. + * Custom {@link ApiExceptions} for handling errors related to read operations. */ -public abstract class ReadOperationException extends ApiExceptions { +public class ReadOperationException extends ApiExceptions { public ReadOperationException(String message) { - super(message); - } - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.SERVICE_UNAVAILABLE; - } - - @Override - public String getError() { - return "Read Error"; + super(HttpStatus.SERVICE_UNAVAILABLE, "Read error", message); } } diff --git a/desm-server/src/main/java/org/example/desm/server/exception/WriteOperationException.java b/desm-server/src/main/java/org/example/desm/server/exception/WriteOperationException.java index 5770b4a..b6fa823 100644 --- a/desm-server/src/main/java/org/example/desm/server/exception/WriteOperationException.java +++ b/desm-server/src/main/java/org/example/desm/server/exception/WriteOperationException.java @@ -3,20 +3,10 @@ import org.springframework.http.HttpStatus; /** - * Abstract custom {@link ApiExceptions} for handling errors related to write operations. + * Custom {@link ApiExceptions} for handling errors related to write operations. */ -public abstract class WriteOperationException extends ApiExceptions { +public class WriteOperationException extends ApiExceptions { public WriteOperationException(String message) { - super(message); - } - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.SERVICE_UNAVAILABLE; - } - - @Override - public String getError() { - return "Write Error"; + super(HttpStatus.SERVICE_UNAVAILABLE, "Write error", message); } } diff --git a/desm-server/src/main/java/org/example/desm/server/service/PollutionService.java b/desm-server/src/main/java/org/example/desm/server/service/PollutionService.java index 7af7567..2eff39e 100644 --- a/desm-server/src/main/java/org/example/desm/server/service/PollutionService.java +++ b/desm-server/src/main/java/org/example/desm/server/service/PollutionService.java @@ -1,9 +1,10 @@ package org.example.desm.server.service; import org.example.desm.common.MyReadWriteLock; -import org.example.desm.server.exception.PollutionReadOperationException; -import org.example.desm.server.exception.PollutionWriteOperationException; +import org.example.desm.server.exception.InvalidTimeIntervalException; import org.example.desm.common.simulator.Measurement; +import org.example.desm.server.exception.ReadOperationException; +import org.example.desm.server.exception.WriteOperationException; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -48,9 +49,14 @@ public class PollutionService { * @param t1 start of the time interval (inclusive) * @param t2 end of the time interval (inclusive) * @return the average emission level in the specified time interval - * @throws PollutionReadOperationException if the reading thread is interrupted. + * @throws InvalidTimeIntervalException if {@code t1} is greater than {@code t2}. + * @throws ReadOperationException if the reading thread is interrupted. */ public double getAvgEmissionLevel(long t1, long t2) { + if (t1 > t2) { + throw new InvalidTimeIntervalException(); + } + try { lock.lockRead(); // Either t1 or t2 is considered inclusive @@ -67,7 +73,7 @@ public double getAvgEmissionLevel(long t1, long t2) { return count == 0 ? 0 : sum / count; } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new PollutionReadOperationException(); + throw new ReadOperationException("Unable to retrieve pollution data at this time."); } finally { lock.unlockRead(); } @@ -79,7 +85,7 @@ public double getAvgEmissionLevel(long t1, long t2) { * Uses a write lock to ensure exclusive access while modifying the internal list. * * @param newMeasurements the list of pollution measurements to register. - * @throws PollutionWriteOperationException if the writing thread is interrupted. + * @throws WriteOperationException if the writing thread is interrupted. */ public void registerMeasurements(List newMeasurements) { try { @@ -92,7 +98,7 @@ public void registerMeasurements(List newMeasurements) { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new PollutionWriteOperationException(); + throw new WriteOperationException("Unable to save the pollution data at this time."); } finally { lock.unlockWrite(); } diff --git a/desm-server/src/main/java/org/example/desm/server/service/PowerPlantService.java b/desm-server/src/main/java/org/example/desm/server/service/PowerPlantService.java index 3ef676a..7f6bf1d 100644 --- a/desm-server/src/main/java/org/example/desm/server/service/PowerPlantService.java +++ b/desm-server/src/main/java/org/example/desm/server/service/PowerPlantService.java @@ -1,11 +1,11 @@ package org.example.desm.server.service; +import org.example.desm.server.exception.InvalidPowerPlantParameterException; import org.example.desm.server.exception.PowerPlantAlreadyExistsException; -import org.example.desm.server.exception.PowerPlantReadOperationException; import org.example.desm.server.exception.ReadOperationException; -import org.example.desm.server.exception.PowerPlantWriteOperationException; import org.example.desm.common.model.PowerPlant; import org.example.desm.common.MyReadWriteLock; +import org.example.desm.server.exception.WriteOperationException; import org.springframework.stereotype.Service; import java.util.HashMap; @@ -55,7 +55,7 @@ public List getAllPlants() { return getAllPlantsUnsafe(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new PowerPlantReadOperationException(); + throw new ReadOperationException("Unable to retrieve power plant data at this time."); } finally { lock.unlockRead(); } @@ -67,12 +67,14 @@ public List getAllPlants() { * Uses a write lock to ensure exclusive access while modifying the internal map. * * @param plant the power plant to register. + * @throws InvalidPowerPlantParameterException if the plant's parameters are invalid. * @throws PowerPlantAlreadyExistsException if a plant with the same ID already exists. - * @throws PowerPlantWriteOperationException if the reading thread is interrupted. + * @throws WriteOperationException if the reading thread is interrupted. */ public void registerPlant(PowerPlant plant) { try { lock.lockWrite(); + validatePowerPlantParameters(plant); // Check if the plant already exists if (plants.containsKey(plant.getId())) { throw new PowerPlantAlreadyExistsException(plant.getId()); @@ -80,7 +82,7 @@ public void registerPlant(PowerPlant plant) { plants.put(plant.getId(), plant); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new PowerPlantWriteOperationException(); + throw new WriteOperationException("Unable to register the power plant at this time."); } finally { lock.unlockWrite(); } @@ -94,4 +96,26 @@ public void registerPlant(PowerPlant plant) { private List getAllPlantsUnsafe() { return List.copyOf(plants.values()); } + + /** + * Validates the parameters of a {@link PowerPlant}. + *

+ * Ensures that the plant's address is not null or blank and that the port number is valid. + * + * @param plant the power plant to validate. + * @throws InvalidPowerPlantParameterException if any parameter is invalid. + */ + private void validatePowerPlantParameters(PowerPlant plant) + throws InvalidPowerPlantParameterException { + // Validate plant's ID + if (plant.getAddress() == null || plant.getAddress().isBlank()) { + throw new InvalidPowerPlantParameterException("Address cannot be null or blank."); + } + + // Validate plant's port + if (plant.getPort() < 1 || plant.getPort() > 65535) { + throw new InvalidPowerPlantParameterException( + "Invalid port number: " + plant.getPort() + ". Must be between 1 and 65535."); + } + } } diff --git a/desm-server/src/test/java/org/example/desm/server/component/PollutionSubscriberTest.java b/desm-server/src/test/java/org/example/desm/server/component/PollutionSubscriberTest.java index 687ea2e..dc8f7b1 100644 --- a/desm-server/src/test/java/org/example/desm/server/component/PollutionSubscriberTest.java +++ b/desm-server/src/test/java/org/example/desm/server/component/PollutionSubscriberTest.java @@ -4,7 +4,6 @@ import org.eclipse.paho.client.mqttv3.IMqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.example.desm.common.model.MeasurementsWrapper; import org.example.desm.common.simulator.Measurement; import org.example.desm.server.service.PollutionService; import org.junit.jupiter.api.BeforeEach; @@ -60,11 +59,8 @@ void testMessageArrived() { new Measurement("id2", "co2", 200, 2000) ); - MeasurementsWrapper wrapper = new MeasurementsWrapper(); - wrapper.setMeasurements(measurements); - Gson gson = new Gson(); - String payload = gson.toJson(wrapper); + String payload = gson.toJson(measurements); MqttMessage mqttMessage = new MqttMessage(payload.getBytes()); diff --git a/desm-server/src/test/java/org/example/desm/server/controller/PowerPlantControllerTest.java b/desm-server/src/test/java/org/example/desm/server/controller/PowerPlantControllerTest.java index d2ccd0d..eb1b2c0 100644 --- a/desm-server/src/test/java/org/example/desm/server/controller/PowerPlantControllerTest.java +++ b/desm-server/src/test/java/org/example/desm/server/controller/PowerPlantControllerTest.java @@ -1,5 +1,6 @@ package org.example.desm.server.controller; +import org.example.desm.server.exception.InvalidPowerPlantParameterException; import org.example.desm.server.exception.PowerPlantAlreadyExistsException; import org.example.desm.server.service.PowerPlantService; import com.google.gson.Gson; @@ -38,8 +39,8 @@ class PowerPlantControllerTest { @Test void testGetAllPlants() throws Exception { List plants = List.of( - new PowerPlant("plant-1", "localhost", 0), - new PowerPlant("plant-2", "localhost", 1) + new PowerPlant("plant-1", "localhost", 8080), + new PowerPlant("plant-2", "localhost", 8081) ); when(powerPlantService.getAllPlants()).thenReturn(plants); @@ -54,8 +55,8 @@ void testGetAllPlants() throws Exception { @Test void testRegisterPlant() throws Exception { - PowerPlant plant = new PowerPlant("plant-1", "localhost", 0); - PowerPlant newPlant = new PowerPlant("plant-2", "localhost", 1); + PowerPlant plant = new PowerPlant("plant-1", "localhost", 8080); + PowerPlant newPlant = new PowerPlant("plant-2", "localhost", 8081); List updatedList = List.of(plant, newPlant); doNothing().when(powerPlantService).registerPlant(any(PowerPlant.class)); @@ -74,7 +75,7 @@ void testRegisterPlant() throws Exception { @Test void testRegisterPlantAlreadyExistsError() throws Exception { - PowerPlant duplicate = new PowerPlant("plant-1", "localhost", 0); + PowerPlant duplicate = new PowerPlant("plant-1", "localhost", 8080); PowerPlantAlreadyExistsException exception = new PowerPlantAlreadyExistsException(duplicate.getId()); @@ -89,4 +90,38 @@ void testRegisterPlantAlreadyExistsError() throws Exception { .andExpect(status().isConflict()) .andExpect(content().json(expectedResponse)); } + + @Test + void testRegisterPlantInvalidAddress() throws Exception { + PowerPlant invalidPlant = new PowerPlant("plant-invalid", "", 8080); + InvalidPowerPlantParameterException exception = new InvalidPowerPlantParameterException("Error"); + + doThrow(exception).when(powerPlantService).registerPlant(any(PowerPlant.class)); + + String requestBody = gson.toJson(invalidPlant); + String expectedResponse = gson.toJson(exception.buildResponse()); + + mockMvc.perform(post(ENDPOINT_PLANTS + ENDPOINT_REGISTER_PLANT) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isBadRequest()) + .andExpect(content().json(expectedResponse)); + } + + @Test + void testRegisterPlantInvalidPort() throws Exception { + PowerPlant invalidPlant = new PowerPlant("plant-invalid", "localhost", 0); + InvalidPowerPlantParameterException exception = new InvalidPowerPlantParameterException("Error"); + + doThrow(exception).when(powerPlantService).registerPlant(any(PowerPlant.class)); + + String requestBody = gson.toJson(invalidPlant); + String expectedResponse = gson.toJson(exception.buildResponse()); + + mockMvc.perform(post(ENDPOINT_PLANTS + ENDPOINT_REGISTER_PLANT) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isBadRequest()) + .andExpect(content().json(expectedResponse)); + } } \ No newline at end of file diff --git a/desm-server/src/test/java/org/example/desm/server/service/PollutionServiceTest.java b/desm-server/src/test/java/org/example/desm/server/service/PollutionServiceTest.java index eb1f35d..1397c3b 100644 --- a/desm-server/src/test/java/org/example/desm/server/service/PollutionServiceTest.java +++ b/desm-server/src/test/java/org/example/desm/server/service/PollutionServiceTest.java @@ -1,6 +1,7 @@ package org.example.desm.server.service; import org.example.desm.common.simulator.Measurement; +import org.example.desm.server.exception.InvalidTimeIntervalException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,6 +59,15 @@ public void testMeasurementOutsideInterval() { assertEquals(0.0, avg); } + @Test + public void testInvalidTimeInterval() { + long now = 100000; + + assertThrows(InvalidTimeIntervalException.class, () -> { + pollutionService.getAvgEmissionLevel(now + 1000, now - 1000); + }); + } + @Test void multipleGetAndRegistrationRandomArrivalTime() throws InterruptedException { int get = 10; diff --git a/desm-server/src/test/java/org/example/desm/server/service/PowerPlantServiceTest.java b/desm-server/src/test/java/org/example/desm/server/service/PowerPlantServiceTest.java index d02c7c9..a8f8365 100644 --- a/desm-server/src/test/java/org/example/desm/server/service/PowerPlantServiceTest.java +++ b/desm-server/src/test/java/org/example/desm/server/service/PowerPlantServiceTest.java @@ -1,5 +1,6 @@ package org.example.desm.server.service; +import org.example.desm.server.exception.InvalidPowerPlantParameterException; import org.example.desm.server.exception.PowerPlantAlreadyExistsException; import org.example.desm.common.model.PowerPlant; import org.junit.jupiter.api.BeforeEach; @@ -24,7 +25,7 @@ void setUp() { @Test void testRegisterPowerPlant() { - PowerPlant plant = new PowerPlant("plant-1", "localhost", 0); + PowerPlant plant = new PowerPlant("plant-1", "localhost", 8080); powerPlantService.registerPlant(plant); List result = powerPlantService.getAllPlants(); @@ -35,8 +36,8 @@ void testRegisterPowerPlant() { @Test void testRegisterPlantAlreadyExistsThrowsException() { - PowerPlant plant = new PowerPlant("plant-1", "localhost", 0); - PowerPlant duplicate = new PowerPlant("plant-1", "localhost", 1); + PowerPlant plant = new PowerPlant("plant-1", "localhost", 8080); + PowerPlant duplicate = new PowerPlant("plant-1", "localhost", 8081); powerPlantService.registerPlant(plant); @@ -44,12 +45,40 @@ void testRegisterPlantAlreadyExistsThrowsException() { powerPlantService.registerPlant(duplicate)); } + @Test + void testRegisterPlantInvalidAddressThrowsException() { + PowerPlant plant1 = new PowerPlant("plant-1", null, 8080); + PowerPlant plant2 = new PowerPlant("plant-1", "", 8081); + + assertThrows(InvalidPowerPlantParameterException.class, () -> + powerPlantService.registerPlant(plant1)); + + assertThrows(InvalidPowerPlantParameterException.class, () -> + powerPlantService.registerPlant(plant2)); + } + + @Test + void testRegisterPlantInvalidPortThrowsException() { + PowerPlant plant1 = new PowerPlant("plant-1", "localhost", 0); + PowerPlant plant2 = new PowerPlant("plant-1", "localhost", -1); + PowerPlant plant3 = new PowerPlant("plant-1", "localhost", 65536); + + assertThrows(InvalidPowerPlantParameterException.class, () -> + powerPlantService.registerPlant(plant1)); + + assertThrows(InvalidPowerPlantParameterException.class, () -> + powerPlantService.registerPlant(plant2)); + + assertThrows(InvalidPowerPlantParameterException.class, () -> + powerPlantService.registerPlant(plant3)); + } + @Test void testRegisterAndGetAllPlants() { List toRegister = List.of( - new PowerPlant("plant-1", "localhost", 0), - new PowerPlant("plant-2", "localhost", 1), - new PowerPlant("plant-3", "localhost", 2) + new PowerPlant("plant-1", "localhost", 8080), + new PowerPlant("plant-2", "localhost", 8081), + new PowerPlant("plant-3", "localhost", 8082) ); for (PowerPlant plant : toRegister) { @@ -88,7 +117,7 @@ void multipleGetAndRegistrationRandomArrivalTime() throws InterruptedException { // Random delay to simulate different arrival times of the threads. randomSleep(); String id = "plant-" + Thread.currentThread().getName(); - PowerPlant plant = new PowerPlant(id, "localhost", finalI); + PowerPlant plant = new PowerPlant(id, "localhost", 8080 + finalI); // Check if it does not throw an exception like ConcurrentModificationException. assertDoesNotThrow(() -> powerPlantService.registerPlant(plant)); registerLatch.countDown(); @@ -118,7 +147,7 @@ void multipleRegistrationWithSameId() throws InterruptedException { fail(e); } String id = "plant-1"; - PowerPlant plant = new PowerPlant(id, "localhost", finalI); + PowerPlant plant = new PowerPlant(id, "localhost", 8080 + finalI); try{ powerPlantService.registerPlant(plant); latchRegistration.countDown();