-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
548 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/Command.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
} |
37 changes: 37 additions & 0 deletions
37
...twork-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandCreator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
...work-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/command/CommandExecutor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/server/Server.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
...work-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/InMemoryStorage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
11-network-ii/snippets/todo-list-app/src/bg/sofia/uni/fmi/mjt/todo/storage/Storage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
} |
42 changes: 42 additions & 0 deletions
42
...-ii/snippets/todo-list-app/test/bg/sofia/uni/fmi/mjt/todo/command/CommandCreatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
} |
Oops, something went wrong.