From 1005e92efc79a427f771b8efe122bb32afee1049 Mon Sep 17 00:00:00 2001 From: 100yo Date: Sat, 21 Dec 2024 00:08:24 +0200 Subject: [PATCH] Add Java network programming code snippets --- .../multithreaded/ClientRequestHandler.java | 43 ++++++ .../echo/net/multithreaded/EchoClient.java | 45 ++++++ .../echo/net/multithreaded/EchoServer.java | 49 ++++++ .../fmi/mjt/echo/net/simple/EchoClient.java | 44 ++++++ .../fmi/mjt/echo/net/simple/EchoServer.java | 35 +++++ .../uni/fmi/mjt/echo/nio/EchoClient.java | 46 ++++++ .../uni/fmi/mjt/echo/nio/EchoClientNio.java | 60 ++++++++ .../uni/fmi/mjt/echo/nio/EchoServer.java | 70 +++++++++ .../uni/fmi/mjt/todo/command/Command.java | 4 + .../fmi/mjt/todo/command/CommandCreator.java | 37 +++++ .../fmi/mjt/todo/command/CommandExecutor.java | 73 +++++++++ .../sofia/uni/fmi/mjt/todo/server/Server.java | 121 +++++++++++++++ .../fmi/mjt/todo/storage/InMemoryStorage.java | 44 ++++++ .../uni/fmi/mjt/todo/storage/Storage.java | 13 ++ .../mjt/todo/command/CommandCreatorTest.java | 42 ++++++ .../mjt/todo/command/CommandExecutorTest.java | 139 ++++++++++++++++++ .../mjt/todo/storage/InMemoryStorageTest.java | 74 ++++++++++ 17 files changed, 939 insertions(+) create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/ClientRequestHandler.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoClient.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoServer.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoClient.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoServer.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClient.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClientNio.java create mode 100644 11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoServer.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/Command.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandCreator.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutor.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/server/Server.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorage.java create mode 100644 11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/Storage.java create mode 100644 11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandCreatorTest.java create mode 100644 11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutorTest.java create mode 100644 11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorageTest.java diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/ClientRequestHandler.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/ClientRequestHandler.java new file mode 100644 index 00000000..beacd349 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/ClientRequestHandler.java @@ -0,0 +1,43 @@ +package bg.sofia.uni.fmi.mjt.echo.net.multithreaded; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; + +public class ClientRequestHandler implements Runnable { + + private Socket socket; + + public ClientRequestHandler(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + + Thread.currentThread().setName("Client Request Handler for " + socket.getRemoteSocketAddress()); + + try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true); // autoflush on + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { + + String inputLine; + while ((inputLine = in.readLine()) != null) { // read the message from the client + System.out.println("Message received from client: " + inputLine); + out.println("Echo " + inputLine); // send response back to the client + } + + } catch (IOException e) { + System.out.println(e.getMessage()); + } finally { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoClient.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoClient.java new file mode 100644 index 00000000..8b559fa1 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoClient.java @@ -0,0 +1,45 @@ +package bg.sofia.uni.fmi.mjt.echo.net.multithreaded; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Scanner; + +public class EchoClient { + + private static final int SERVER_PORT = 4444; + + public static void main(String[] args) { + + try (Socket socket = new Socket("localhost", SERVER_PORT); + PrintWriter writer = new PrintWriter(socket.getOutputStream(), true); // autoflush on + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + Scanner scanner = new Scanner(System.in)) { + + Thread.currentThread().setName("Echo client thread " + socket.getLocalPort()); + + System.out.println("Connected to the server."); + + while (true) { + System.out.print("Enter message: "); + String message = scanner.nextLine(); // read a line from the console + + if ("quit".equals(message)) { + break; + } + + System.out.println("Sending message <" + message + "> to the server..."); + + writer.println(message); // send the message to the server + + String reply = reader.readLine(); // read the response from the server + System.out.println("The server replied <" + reply + ">"); + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the network communication", e); + } + } +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoServer.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoServer.java new file mode 100644 index 00000000..019ce033 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/multithreaded/EchoServer.java @@ -0,0 +1,49 @@ +package bg.sofia.uni.fmi.mjt.echo.net.multithreaded; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class EchoServer { + + private static final int SERVER_PORT = 4444; + private static final int MAX_EXECUTOR_THREADS = 10; + + public static void main(String[] args) { + + ExecutorService executor = Executors.newFixedThreadPool(MAX_EXECUTOR_THREADS); + + Thread.currentThread().setName("Echo Server Thread"); + + try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) { + + System.out.println("Server started and listening for connect requests"); + + Socket clientSocket; + + while (true) { + + // Calling accept() blocks and waits for connection request by a client + // When a request comes, accept() returns a socket to communicate with this + // client + clientSocket = serverSocket.accept(); + + System.out.println("Accepted connection request from client " + clientSocket.getInetAddress()); + + // We want each client to be processed in a separate thread + // to keep the current thread free to accept() requests from new clients + ClientRequestHandler clientHandler = new ClientRequestHandler(clientSocket); + + // uncomment the line below to launch a thread manually + // new Thread(clientHandler).start(); + executor.execute(clientHandler); // use a thread pool to launch a thread + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the server socket", e); + } + } + +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoClient.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoClient.java new file mode 100644 index 00000000..30fa710f --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoClient.java @@ -0,0 +1,44 @@ +package bg.sofia.uni.fmi.mjt.echo.net.simple; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Scanner; + +public class EchoClient { + + private static final int SERVER_PORT = 6666; + + public static void main(String[] args) { + + try (Socket socket = new Socket("localhost", SERVER_PORT); + PrintWriter writer = new PrintWriter(socket.getOutputStream(), true); // autoflush on + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + Scanner scanner = new Scanner(System.in)) { + + System.out.println("Connected to the server."); + + while (true) { + System.out.print("Enter message: "); + String message = scanner.nextLine(); // read a line from the console + + if ("quit".equals(message)) { + break; + } + + System.out.println("Sending message <" + message + "> to the server..."); + + writer.println(message); // send the message to the server + + String reply = reader.readLine(); // read the response from the server + System.out.println("The server replied <" + reply + ">"); + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the network communication", e); + } + + } +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoServer.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoServer.java new file mode 100644 index 00000000..ea785a54 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/net/simple/EchoServer.java @@ -0,0 +1,35 @@ +package bg.sofia.uni.fmi.mjt.echo.net.simple; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; + +public class EchoServer { + + private static final int SERVER_PORT = 6666; + + public static void main(String[] args) { + + try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) { + System.out.println("Server started and listening for connect request"); + + try (Socket clientSocket = serverSocket.accept(); + BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { // autoflush on + + String inputLine; + while ((inputLine = br.readLine()) != null) { + System.out.println("Message received from client: " + inputLine); + out.println("Echo " + inputLine); + } + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the server socket", e); + } + } + +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClient.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClient.java new file mode 100644 index 00000000..7524a8e4 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClient.java @@ -0,0 +1,46 @@ +package bg.sofia.uni.fmi.mjt.echo.nio; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.util.Scanner; + +// NIO specifics wrapped & hidden +public class EchoClient { + + private static final int SERVER_PORT = 7777; + + public static void main(String[] args) { + + try (SocketChannel socketChannel = SocketChannel.open(); + BufferedReader reader = new BufferedReader(Channels.newReader(socketChannel, "UTF-8")); + PrintWriter writer = new PrintWriter(Channels.newWriter(socketChannel, "UTF-8"), true); + Scanner scanner = new Scanner(System.in)) { + + socketChannel.connect(new InetSocketAddress("localhost", SERVER_PORT)); + + System.out.println("Connected to the server."); + + while (true) { + System.out.print("Enter message: "); + String message = scanner.nextLine(); // read a line from the console + + if ("quit".equals(message)) { + break; + } + + System.out.println("Sending message <" + message + "> to the server..."); + + writer.println(message); + + String reply = reader.readLine(); // read the response from the server + System.out.println("The server replied <" + reply + ">"); + } + } catch (IOException e) { + throw new RuntimeException("There is a problem with the network communication", e); + } + } +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClientNio.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClientNio.java new file mode 100644 index 00000000..bc42370d --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoClientNio.java @@ -0,0 +1,60 @@ +package bg.sofia.uni.fmi.mjt.echo.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Scanner; + +// NIO, blocking +public class EchoClientNio { + + private static final int SERVER_PORT = 7777; + private static final String SERVER_HOST = "localhost"; + private static final int BUFFER_SIZE = 512; + + private static ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + + public static void main(String[] args) { + + try (SocketChannel socketChannel = SocketChannel.open(); + Scanner scanner = new Scanner(System.in)) { + + socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT)); + + System.out.println("Connected to the server."); + + while (true) { + System.out.print("Enter message: "); + String message = scanner.nextLine(); // read a line from the console + + if ("quit".equals(message)) { + break; + } + + System.out.println("Sending message <" + message + "> to the server..."); + + buffer.clear(); // switch to writing mode + buffer.put(message.getBytes()); // buffer fill + buffer.flip(); // switch to reading mode + socketChannel.write(buffer); // buffer drain + + buffer.clear(); // switch to writing mode + socketChannel.read(buffer); // buffer fill + buffer.flip(); // switch to reading mode + + byte[] byteArray = new byte[buffer.remaining()]; + buffer.get(byteArray); + String reply = new String(byteArray, "UTF-8"); // buffer drain + + // if the buffer is a non-direct one, it has a wrapped array and we can get it + //String reply = new String(buffer.array(), 0, buffer.position(), "UTF-8"); // buffer drain + + System.out.println("The server replied <" + reply + ">"); + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the network communication", e); + } + } +} diff --git a/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoServer.java b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoServer.java new file mode 100644 index 00000000..254406c8 --- /dev/null +++ b/11-network-ii/snippets/echoclientserver/src/bg/sofia/uni/fmi/mjt/echo/nio/EchoServer.java @@ -0,0 +1,70 @@ +package bg.sofia.uni.fmi.mjt.echo.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class EchoServer { + public static final int SERVER_PORT = 7777; + private static final String SERVER_HOST = "localhost"; + private static final int BUFFER_SIZE = 1024; + + public static void main(String[] args) { + try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { + + serverSocketChannel.bind(new InetSocketAddress(SERVER_HOST, SERVER_PORT)); + serverSocketChannel.configureBlocking(false); + + Selector selector = Selector.open(); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + + while (true) { + int readyChannels = selector.select(); + if (readyChannels == 0) { + // select() is blocking but may still return with 0, check javadoc + continue; + } + + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isReadable()) { + SocketChannel sc = (SocketChannel) key.channel(); + + buffer.clear(); + int r = sc.read(buffer); + if (r < 0) { + System.out.println("Client has closed the connection"); + sc.close(); + continue; + } + buffer.flip(); + sc.write(buffer); + + } else if (key.isAcceptable()) { + ServerSocketChannel sockChannel = (ServerSocketChannel) key.channel(); + SocketChannel accept = sockChannel.accept(); + accept.configureBlocking(false); + accept.register(selector, SelectionKey.OP_READ); + } + + keyIterator.remove(); + } + + } + + } catch (IOException e) { + throw new RuntimeException("There is a problem with the server socket", e); + } + } +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/Command.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/Command.java new file mode 100644 index 00000000..db567800 --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/Command.java @@ -0,0 +1,4 @@ +package bg.sofia.uni.fmi.mjt.todo.command; + +public record Command(String command, String[] arguments) { +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandCreator.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandCreator.java new file mode 100644 index 00000000..cd347676 --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandCreator.java @@ -0,0 +1,37 @@ +package bg.sofia.uni.fmi.mjt.todo.command; + +import java.util.ArrayList; +import java.util.List; + +public class CommandCreator { + // straight out of https://stackoverflow.com/a/14656159 with small enhancement + private static List getCommandArguments(String input) { + List tokens = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + + boolean insideQuote = false; + + for (char c : input.toCharArray()) { + if (c == '"') { + insideQuote = !insideQuote; + } + if (c == ' ' && !insideQuote) { // when space is not inside quote split + tokens.add(sb.toString().replace("\"", "")); // token is ready, let's add it to list + sb.delete(0, sb.length()); // and reset StringBuilder`s content + } else { + sb.append(c); //else add character to token + } + } + // let's not forget about last token that doesn't have space after it + tokens.add(sb.toString().replace("\"", "")); + + return tokens; + } + + public static Command newCommand(String clientInput) { + List tokens = CommandCreator.getCommandArguments(clientInput); + String[] args = tokens.subList(1, tokens.size()).toArray(new String[0]); + + return new Command(tokens.get(0), args); + } +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutor.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutor.java new file mode 100644 index 00000000..c855dc97 --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutor.java @@ -0,0 +1,73 @@ +package bg.sofia.uni.fmi.mjt.todo.command; + +import bg.sofia.uni.fmi.mjt.todo.storage.Storage; + +public class CommandExecutor { + private static final String INVALID_ARGS_COUNT_MESSAGE_FORMAT = + "Invalid count of arguments: \"%s\" expects %d arguments. Example: \"%s\""; + + private static final String ADD = "add-todo"; + private static final String COMPLETE = "complete-todo"; + private static final String LIST = "list"; + + private Storage storage; + + public CommandExecutor(Storage storage) { + this.storage = storage; + } + + public String execute(Command cmd) { + return switch (cmd.command()) { + case ADD -> addToDo(cmd.arguments()); + case COMPLETE -> complete(cmd.arguments()); + case LIST -> list(cmd.arguments()); + default -> "Unknown command"; + }; + } + + private String addToDo(String[] args) { + if (args.length != 2) { + return String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, ADD, 2, ADD + " "); + } + + String user = args[0]; + String todo = args[1]; + + int todoID = storage.add(user, todo); + return String.format("Added new To Do with ID %s for user %s", todoID, user); + } + + private String complete(String[] args) { + if (args.length != 2) { + return String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, COMPLETE, 2, + COMPLETE + " "); + } + + String user = args[0]; + int todoID; + try { + todoID = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + return "Invalid ID provided for command \"complete-todo\": only integer values are allowed"; + } + + storage.remove(user, todoID); + return String.format("Completed To Do with ID %s for user %s", todoID, user); + } + + private String list(String[] args) { + if (args.length != 1) { + return String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, LIST, 1, LIST + " "); + } + String user = args[0]; + var todos = storage.list(user); + if (todos.isEmpty()) { + return "No To-Do items found for user with name " + user; + } + + StringBuilder response = new StringBuilder(String.format("To-Do list of user %s:%n", user)); + todos.forEach((k, v) -> response.append(String.format("[%d] %s%n", k, v))); + + return response.toString(); + } +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/server/Server.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/server/Server.java new file mode 100644 index 00000000..2f6526e5 --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/server/Server.java @@ -0,0 +1,121 @@ +package bg.sofia.uni.fmi.mjt.todo.server; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +import bg.sofia.uni.fmi.mjt.todo.command.CommandCreator; +import bg.sofia.uni.fmi.mjt.todo.command.CommandExecutor; + +public class Server { + private static final int BUFFER_SIZE = 1024; + private static final String HOST = "localhost"; + + private final CommandExecutor commandExecutor; + + private final int port; + private boolean isServerWorking; + + private ByteBuffer buffer; + private Selector selector; + + public Server(int port, CommandExecutor commandExecutor) { + this.port = port; + this.commandExecutor = commandExecutor; + } + + public void start() { + try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { + selector = Selector.open(); + configureServerSocketChannel(serverSocketChannel, selector); + this.buffer = ByteBuffer.allocate(BUFFER_SIZE); + isServerWorking = true; + while (isServerWorking) { + try { + int readyChannels = selector.select(); + if (readyChannels == 0) { + continue; + } + + Iterator keyIterator = selector.selectedKeys().iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isReadable()) { + SocketChannel clientChannel = (SocketChannel) key.channel(); + String clientInput = getClientInput(clientChannel); + System.out.println(clientInput); + if (clientInput == null) { + continue; + } + + String output = commandExecutor.execute(CommandCreator.newCommand(clientInput)); + writeClientOutput(clientChannel, output); + + } else if (key.isAcceptable()) { + accept(selector, key); + } + + keyIterator.remove(); + } + } catch (IOException e) { + System.out.println("Error occurred while processing client request: " + e.getMessage()); + } + } + } catch (IOException e) { + throw new UncheckedIOException("failed to start server", e); + } + } + + public void stop() { + this.isServerWorking = false; + if (selector.isOpen()) { + selector.wakeup(); + } + } + + private void configureServerSocketChannel(ServerSocketChannel channel, Selector selector) throws IOException { + channel.bind(new InetSocketAddress(HOST, this.port)); + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_ACCEPT); + } + + private String getClientInput(SocketChannel clientChannel) throws IOException { + buffer.clear(); + + int readBytes = clientChannel.read(buffer); + if (readBytes < 0) { + clientChannel.close(); + return null; + } + + buffer.flip(); + + byte[] clientInputBytes = new byte[buffer.remaining()]; + buffer.get(clientInputBytes); + + return new String(clientInputBytes, StandardCharsets.UTF_8); + } + + private void writeClientOutput(SocketChannel clientChannel, String output) throws IOException { + buffer.clear(); + buffer.put(output.getBytes()); + buffer.flip(); + + clientChannel.write(buffer); + } + + private void accept(Selector selector, SelectionKey key) throws IOException { + ServerSocketChannel sockChannel = (ServerSocketChannel) key.channel(); + SocketChannel accept = sockChannel.accept(); + + accept.configureBlocking(false); + accept.register(selector, SelectionKey.OP_READ); + } +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorage.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorage.java new file mode 100644 index 00000000..18805cb4 --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorage.java @@ -0,0 +1,44 @@ +package bg.sofia.uni.fmi.mjt.todo.storage; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class InMemoryStorage implements Storage { + private Map> userTodos; + + public InMemoryStorage() { + this.userTodos = new HashMap<>(); + } + + public int add(String user, String todo) { + if (!userTodos.containsKey(user)) { + userTodos.put(user, new HashMap<>()); + } + + var toDos = userTodos.get(user); + int id = toDos.size(); + toDos.put(id, todo); + + return id; + } + + public void remove(String user, int todoID) { + var toDos = userTodos.get(user); + if (toDos == null || !toDos.containsKey(todoID)) { + return; + } + + toDos.remove(todoID); + } + + @Override + public Map list(String user) { + var toDos = userTodos.get(user); + if (toDos == null || toDos.isEmpty()) { + return Collections.emptyMap(); + } + + return Collections.unmodifiableMap(toDos); + } +} diff --git a/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/Storage.java b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/Storage.java new file mode 100644 index 00000000..dbd4496c --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/Storage.java @@ -0,0 +1,13 @@ +package bg.sofia.uni.fmi.mjt.todo.storage; + +import java.util.Map; + +public interface Storage { + + int add(String user, String todo); + + void remove(String user, int todoID); + + Map list(String user); + +} diff --git a/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandCreatorTest.java b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandCreatorTest.java new file mode 100644 index 00000000..4be28aad --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandCreatorTest.java @@ -0,0 +1,42 @@ +package bg.sofia.uni.fmi.mjt.todo.command; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CommandCreatorTest { + + @Test + public void testCommandCreationWithNoArguments() { + String command = "test"; + Command cmd = CommandCreator.newCommand(command); + + assertEquals(command, cmd.command(), "unexpected command returned for command 'test'"); + assertNotNull(cmd.arguments(), "command arguments should not be null"); + assertEquals(0, cmd.arguments().length, "unexpected command arguments count"); + } + + @Test + public void testCommandCreationWithOneArgument() { + String command = "test abcd"; + Command cmd = CommandCreator.newCommand(command); + + assertEquals(command.split(" ")[0], cmd.command(), "unexpected command returned for command 'test abcd'"); + assertNotNull(cmd.arguments(), "command arguments should not be null"); + assertEquals(1, cmd.arguments().length, "unexpected command arguments count"); + assertEquals(command.split(" ")[1], cmd.arguments()[0], "unexpected argument returned for command 'test abcd'"); + } + + @Test + public void testCommandCreationWithOneArgumentInQuotes() { + String command = "test \"abcd 1234\""; + Command cmd = CommandCreator.newCommand(command); + + assertEquals(command.split(" ")[0], cmd.command(), "unexpected command returned for command 'test \"abcd 1234\"'"); + assertNotNull(cmd.arguments(), "command arguments should not be null"); + assertEquals(1, cmd.arguments().length, "unexpected command arguments count"); + assertEquals("abcd 1234", cmd.arguments()[0], "multi-word argument is not respected"); + } + +} diff --git a/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutorTest.java b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutorTest.java new file mode 100644 index 00000000..fe89513e --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutorTest.java @@ -0,0 +1,139 @@ +package bg.sofia.uni.fmi.mjt.todo.command; + +import java.util.Collections; + +import bg.sofia.uni.fmi.mjt.todo.storage.Storage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CommandExecutorTest { + + private static final String INVALID_ARGS_COUNT_MESSAGE_FORMAT = "Invalid count of arguments: \"%s\" expects %d arguments. Example: \"%s\""; + private static final String ADD = "add-todo"; + private static final String COMPLETE = "complete-todo"; + private static final String LIST = "list"; + + private Storage storage; + private CommandExecutor cmdExecutor; + + private String testUser = "user"; + private String testTodo = "todo"; + private int testID = 123; + + private Command add = new Command("add-todo", new String[]{testUser, testTodo}); + private Command complete = new Command("complete-todo", new String[]{testUser, String.format("%d", testID)}); + private Command list = new Command("list", new String[]{testUser}); + + @BeforeEach + public void setUp() { + storage = mock(Storage.class); + cmdExecutor = new CommandExecutor(storage); + } + + @Test + public void testAddToDo() { + when(storage.add(testUser, testTodo)).thenReturn(testID); + String expected = String.format("Added new To Do with ID %s for user %s", testID, testUser); + String actual = cmdExecutor.execute(add); + + assertEquals(expected, actual, "unexpected output for 'add-todo'"); + } + + @Test + public void testAddToDoReturnsErrorWhenLessArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, ADD, 2, ADD + " "); + String actual = cmdExecutor.execute(new Command("add-todo", new String[]{testUser})); + + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the provided arguments are less than two"); + } + + @Test + public void testAddToDoReturnsErrorWhenMoreArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, ADD, 2, ADD + " "); + String actual = cmdExecutor.execute(new Command("add-todo", new String[]{testUser, testTodo, testTodo})); + + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the provided arguments are more than two"); + } + + @Test + public void testComplete() { + String expected = String.format("Completed To Do with ID %s for user %s", testID, testUser); + String actual = cmdExecutor.execute(complete); + + verify(storage, times(1)).remove(testUser, testID); + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the user and ID are present in the storage"); + } + + @Test + public void testCompleteReturnsErrorWhenLessArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, COMPLETE, 2, COMPLETE + " "); + String actual = cmdExecutor.execute(new Command(COMPLETE, new String[]{testUser})); + + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the provided arguments are less than two"); + } + + @Test + public void testCompleteReturnsErrorWhenMoreArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, COMPLETE, 2, COMPLETE + " "); + String actual = cmdExecutor.execute(new Command(COMPLETE, new String[]{testUser, String.format("%d", testID), testTodo})); + + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the provided arguments are more than two"); + } + + @Test + public void testCompleteReturnsErrorWhenIDIsNotNumerical() { + String expected = "Invalid ID provided for command \"complete-todo\": only integer values are allowed"; + String actual = cmdExecutor.execute(new Command(COMPLETE, new String[]{testUser, testUser})); + + assertEquals(expected, actual, "unexpected output for 'complete-todo' when the provided to-do ID is not a number"); + } + + @Test + public void testList() { + when(storage.list(testUser)).thenReturn(Collections.singletonMap(testID, testTodo)); + String expected = String.format("To-Do list of user %s:%n[%d] %s%n", testUser, testID, testTodo); + String actual = cmdExecutor.execute(list); + + assertEquals(expected, actual, "unexpected output for 'list' when to-do list for user has one entry"); + } + + @Test + public void testListWhenEmpty() { + when(storage.list(testUser)).thenReturn(Collections.emptyMap()); + String expected = "No To-Do items found for user with name " + testUser; + String actual = cmdExecutor.execute(list); + + assertEquals(expected, actual, "unexpected output for 'list' when to-do list for user is empty"); + } + + @Test + public void testListReturnsErrorWhenLessArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, LIST, 1, LIST + " "); + String actual = cmdExecutor.execute(new Command(LIST, new String[]{})); + + assertEquals(expected, actual, "unexpected output for 'list' with no arguments"); + } + + @Test + public void testListReturnsErrorWhenMoreArguments() { + String expected = String.format(INVALID_ARGS_COUNT_MESSAGE_FORMAT, LIST, 1, LIST + " "); + String actual = cmdExecutor.execute(new Command(LIST, new String[]{testUser, testUser})); + + assertEquals(expected, actual, "unexpected output for 'list' with no arguments"); + } + + @Test + public void testUnknownCommand() { + String expected = "Unknown command"; + String actual = cmdExecutor.execute(new Command("test", new String[]{})); + + assertEquals(expected, actual, "unexpected output for unknown command"); + } + +} diff --git a/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorageTest.java b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorageTest.java new file mode 100644 index 00000000..1b25b61d --- /dev/null +++ b/11-network-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorageTest.java @@ -0,0 +1,74 @@ +package bg.sofia.uni.fmi.mjt.todo.storage; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InMemoryStorageTest { + + private Storage storage; + + private final String testUser = "testUser"; + private final String testTodo = "testTodo"; + private int testTodoID; + + @BeforeEach + public void setUp() { + storage = new InMemoryStorage(); + testTodoID = storage.add(testUser, testTodo); + } + + @Test + public void testStorageListWhenUserIsUnknown() { + Map expected = Collections.emptyMap(); + Map actual = storage.list("unknown"); + + assertEquals(expected, actual, "expected empty map for unknown user"); + } + + @Test + public void testStorageList() { + Map expected = Collections.singletonMap(testTodoID, testTodo); + Map actual = storage.list(testUser); + + assertEquals(expected, actual, "unexpected map for known user"); + } + + @Test + public void testStorageRemove() { + Map expectedBefore = Collections.singletonMap(testTodoID, testTodo); + Map actualBefore = storage.list(testUser); + + assertEquals(expectedBefore, actualBefore, "test prerequisite failed: user todo list is not correct"); + + storage.remove(testUser, testTodoID); + + Map expectedAfter = Collections.emptyMap(); + Map actualAfter = storage.list(testUser); + + assertEquals(expectedAfter, actualAfter, "expected empty map for user with recently removed item"); + } + + @Test + public void testStorageRemoveDoesNotChangeWhenUnknownUserIsGiven() { + Map expected = Collections.singletonMap(testTodoID, testTodo); + storage.remove("unknown", 1); + Map actual = storage.list(testUser); + + assertEquals(expected, actual, "map shouldn't change when unknown user is given"); + } + + + @Test + public void testStorageRemoveDoesNotChangeWhenToDoIDDoesNotExist() { + Map expected = Collections.singletonMap(testTodoID, testTodo); + storage.remove(testUser, 123); + Map actual = storage.list(testUser); + + assertEquals(expected, actual, "map shouldn't change when unknown to-do ID is given"); + } +}