Skip to content

Commit

Permalink
Add Java NIO TODO List code snippet
Browse files Browse the repository at this point in the history
  • Loading branch information
100yo committed Jan 5, 2024
1 parent fa041dc commit 4ab5482
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static void main(String[] args) {
buffer.get(byteArray);
String reply = new String(byteArray, "UTF-8"); // buffer drain

// if buffer is a non-direct one, is has a wrapped array and we can get it
// 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 + ">");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package bg.sofia.uni.fmi.mjt.todo.command;

public record Command(String command, String[] arguments) {
}
Original file line number Diff line number Diff line change
@@ -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<String> getCommandArguments(String input) {
List<String> 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, lets add it to list
sb.delete(0, sb.length()); //and reset StringBuilder`s content
} else {
sb.append(c); //else add character to token
}
}
//lets 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<String> tokens = CommandCreator.getCommandArguments(clientInput);
String[] args = tokens.subList(1, tokens.size()).toArray(new String[0]);

return new Command(tokens.get(0), args);
}
}
Original file line number Diff line number Diff line change
@@ -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 + " <username> <todo_item>");
}

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 + " <username> <todo_item_id>");
}

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 + " <username>");
}
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<SelectionKey> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Map<Integer, String>> 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<Integer, String> list(String user) {
var toDos = userTodos.get(user);
if (toDos == null || toDos.isEmpty()) {
return Collections.emptyMap();
}

return Collections.unmodifiableMap(toDos);
}
}
Original file line number Diff line number Diff line change
@@ -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<Integer, String> list(String user);

}
Original file line number Diff line number Diff line change
@@ -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");
}

}
Loading

0 comments on commit 4ab5482

Please sign in to comment.