Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions desm-client/src/main/java/org/example/desm/client/ApiClient.java
Original file line number Diff line number Diff line change
@@ -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<PowerPlant> getAllPlants()
throws IOException, InterruptedException, ResponseErrorExceptions {
String uri = API_BASE_URL + ENDPOINT_PLANTS;

HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
HttpResponse<String> response = client.send(req, HttpResponse.BodyHandlers.ofString());
validateResponse(response);

Type listType = new TypeToken<List<PowerPlant>>() {}.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<String> 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<String> response) throws ResponseErrorExceptions {
int status = response.statusCode();
if (status < 200 || status >= 300) {
ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class);
throw new ResponseErrorExceptions(error);
}
}
}
87 changes: 87 additions & 0 deletions desm-client/src/main/java/org/example/desm/client/Client.java
Original file line number Diff line number Diff line change
@@ -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<Integer, Command> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<PowerPlant> 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!");
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
}
65 changes: 65 additions & 0 deletions desm-client/src/test/java/org/example/desm/client/ClientTest.java
Original file line number Diff line number Diff line change
@@ -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<PowerPlant> 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<ApiClient> 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<ApiClient> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public class ErrorResponse {
private int status;
private String error;
private String message;

@Override
public String toString() {
return "[" + status + "] " + error + ": " + message;
}
}
Loading