diff --git a/.gitignore b/.gitignore index c7cf644..0969247 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea -SharifEngine.iml +JGFramework.iml *.class @@ -7,9 +7,13 @@ SharifEngine.iml .mtj.tmp/ # Package Files # -*.jar *.war *.ear +.DS_Store # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Jar libraries # +libs/*.jar +out/ \ No newline at end of file diff --git a/JGFramework.iml b/JGFramework.iml new file mode 100644 index 0000000..d3e9e47 --- /dev/null +++ b/JGFramework.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal/Resources/ip-port.conf b/Terminal/Resources/ip-port.conf new file mode 100644 index 0000000..7f460c0 --- /dev/null +++ b/Terminal/Resources/ip-port.conf @@ -0,0 +1 @@ +{"ip":"127.0.0.1","port":3001} \ No newline at end of file diff --git a/Terminal/Terminal.iml b/Terminal/Terminal.iml new file mode 100644 index 0000000..2d0f670 --- /dev/null +++ b/Terminal/Terminal.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal/src/Terminal.java b/Terminal/src/Terminal.java new file mode 100644 index 0000000..4b51400 --- /dev/null +++ b/Terminal/src/Terminal.java @@ -0,0 +1,212 @@ +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import server.network.JsonSocket; +import server.network.data.Command; + +import java.io.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Scanner; +import java.util.StringTokenizer; + +/** + * Created by Saeed on 1/23/2015. + */ +public class Terminal { + private static final String FILE_PATH = "../JGFramework/Terminal/Resources/ip-port.conf"; + + private static final String CMD_TYPE_CONNECT = "connect"; + private static final String CMD_TYPE_DISCONNECT = "disconnect"; + private static final String CMD_TYPE_SETPORT = "set-port"; + private static final String CMD_TYPE_SETIP = "set-ip"; + + private static Terminal terminal = null; + private String ip; + private int port; + private JsonSocket jsonSocket; + + public Terminal() { + try { + setIpAndPortFromFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static Terminal getInstance() { + if (terminal == null) { + terminal = new Terminal(); + } + return terminal; + } + + public void writeIpAndPortToFile(String ip, int port) throws IOException { + Gson gson = new Gson(); + String json = gson.toJson(new TerminalAppConfig(ip,port)); + FileWriter writer = new FileWriter(FILE_PATH); + writer.write(json); + writer.close(); + + } + + public void setIpAndPortFromFile() throws IOException { + + + Gson gson = new Gson(); + + try { + BufferedReader br = new BufferedReader( new FileReader(FILE_PATH)); + TerminalAppConfig terminalAppConfig= gson.fromJson(br, TerminalAppConfig.class); + this.port = terminalAppConfig.getPort(); + this.ip = terminalAppConfig.getIp(); + } catch (JsonParseException e) { + throw new RuntimeException("Terminal App config file does not meet expected syntax"); + } + } + + public void handleCommand(String cmd) { + StringTokenizer tokenizer = new StringTokenizer(cmd, " "); + ArrayList command = new ArrayList<>(); + while (tokenizer.hasMoreTokens()) { + command.add(tokenizer.nextToken()); + } + + String cmdType = command.get(0); + + switch (cmdType) { + case CMD_TYPE_CONNECT: + try { + handleConnectCmd(); + } catch (IOException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + break; + + case CMD_TYPE_DISCONNECT: + try { + handleDisconnectCmd(); + } catch (IOException e) { + e.printStackTrace(); + } + break; + + case CMD_TYPE_SETIP: + try { + handleSetIpCmd(command); + } catch (IOException e) { + e.printStackTrace(); + } + break; + + case CMD_TYPE_SETPORT: + try { + handleSetPortCmd(command); + } catch (IOException e) { + e.printStackTrace(); + } + break; + + default: + Command externalCommand = new Command(); + externalCommand.cmdType = cmdType; + command.remove(0); + externalCommand.args = command.toArray(new String[command.size()]); + + try { + jsonSocket.send(externalCommand); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + private void handleConnectCmd() throws IOException, NoSuchAlgorithmException { + + System.out.print("Enter the password: "); + Scanner scanner = new Scanner(System.in); + String password = scanner.nextLine(); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(password.getBytes()); + byte[] bytes = md5.digest(); //This has bytes in decimal format; + //Convert it to hexadecimal format + StringBuilder sb = new StringBuilder(); + for (byte aByte : bytes) { + sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1)); + } + //Get complete hashed password in hex format + String token = sb.toString(); + + jsonSocket = new JsonSocket(ip, port); + jsonSocket.send(token); + + System.out.println("Successfully connected!"); + } + + private void handleDisconnectCmd() throws IOException { + if (jsonSocket != null) {// it's connected + jsonSocket.close(); + System.out.println("Successfully disconnected!"); + } + else System.out.println("You are not connected yet!"); + } + + private void handleSetIpCmd(ArrayList command) throws IOException { + if (jsonSocket != null) { + System.out.println("Cannot change IP or Port while connecting to server."); + return; + } + if (command.get(1).equals("-s")) { + this.ip = command.get(2); + writeIpAndPortToFile(this.ip, this.port); + } else { + this.ip = command.get(1); + } + + System.out.println("IP changed successfully."); + } + + private void handleSetPortCmd(ArrayList command) throws IOException { + if (jsonSocket != null) { + System.out.println("Cannot change IP or Port while connecting to server."); + return; + } + if (command.get(1).equals("-s")) { + String port = command.get(2); + System.out.println(port); + this.port = Integer.parseInt(port); + writeIpAndPortToFile(this.ip, this.port); + } else { + String port = command.get(1); + this.port = Integer.parseInt(port); + } + + System.out.println("Port changed successfully."); + } + + private static class TerminalAppConfig { + private String ip; + private int port; + + public TerminalAppConfig(String ip, int port) { + this.ip = ip; + this.port = port; + } + + public String getIp() { + return ip; + } + + public int getPort() { + if (port > 0 && port <= 65535) { + return port; + } else { + throw new RuntimeException("Invalid ui port number in config file"); + } + } + } + +} \ No newline at end of file diff --git a/Terminal/src/TerminalApp.java b/Terminal/src/TerminalApp.java new file mode 100644 index 0000000..4c77f26 --- /dev/null +++ b/Terminal/src/TerminalApp.java @@ -0,0 +1,18 @@ +import java.util.Scanner; + +/** + * Created by Saeed on 1/23/2015. + */ +public class TerminalApp { + public static void main(String[] args) { + Terminal terminal = Terminal.getInstance(); + + Scanner scanner = new Scanner(System.in); + System.out.println("<< JGFramework's Terminal >>"); + + while (true) { + String command = scanner.nextLine(); + terminal.handleCommand(command); + } + } +} diff --git a/libs/gson-2.3.1.jar b/libs/gson-2.3.1.jar new file mode 100644 index 0000000..250132c Binary files /dev/null and b/libs/gson-2.3.1.jar differ diff --git a/libs/junit-4.12.jar b/libs/junit-4.12.jar new file mode 100644 index 0000000..3a7fc26 Binary files /dev/null and b/libs/junit-4.12.jar differ diff --git a/resources/client/connection.conf b/resources/client/connection.conf new file mode 100644 index 0000000..04b0f14 --- /dev/null +++ b/resources/client/connection.conf @@ -0,0 +1,5 @@ +{ + "token" : "00000000000000000000000000000000", + "port" : 7099, + "ip" : "127.0.0.1" +} \ No newline at end of file diff --git a/resources/game_handler/output_handler.conf b/resources/game_handler/output_handler.conf new file mode 100644 index 0000000..a9be674 --- /dev/null +++ b/resources/game_handler/output_handler.conf @@ -0,0 +1,7 @@ +{ + "sendToUI" : true, + "timeInterval" : 500, + "sendToFile" : true, + "filePath" : "/log.log", + "bufferSize" : 100 +} \ No newline at end of file diff --git a/resources/game_handler/turn_timeout.conf b/resources/game_handler/turn_timeout.conf new file mode 100644 index 0000000..a80c324 --- /dev/null +++ b/resources/game_handler/turn_timeout.conf @@ -0,0 +1,4 @@ +{ + "clientResponseTime" : 500, + "simulateTimeout" : 10000 +} \ No newline at end of file diff --git a/resources/network/client.conf b/resources/network/client.conf new file mode 100644 index 0000000..2259cad --- /dev/null +++ b/resources/network/client.conf @@ -0,0 +1,3 @@ +{ + "port" : 3002 +} \ No newline at end of file diff --git a/resources/network/terminal.conf b/resources/network/terminal.conf new file mode 100644 index 0000000..66eff6f --- /dev/null +++ b/resources/network/terminal.conf @@ -0,0 +1,4 @@ +{ + "token" : "abcdefghijklmnopqrstuvwxyz012345", + "port" : 3001 +} diff --git a/resources/network/ui.conf b/resources/network/ui.conf new file mode 100644 index 0000000..5198add --- /dev/null +++ b/resources/network/ui.conf @@ -0,0 +1,4 @@ +{ + "token" : "abcdefghijklmnopqrstuvwxyz012346", + "port" : 7000 +} \ No newline at end of file diff --git a/src/client/AI.java b/src/client/AI.java new file mode 100644 index 0000000..255b2c2 --- /dev/null +++ b/src/client/AI.java @@ -0,0 +1,32 @@ +package client; + +/** + * AI class. + * You should fill body of the method {@link #doTurn}. + * Do not change name or modifiers of the methods or fields. + * You can add as many methods or fields as you want! + * You could use model to get information about current state of + * the game. + */ +public class AI { + + /** + * Game model. Describes current state of the game. + */ + public Model model; + + /** + * Constructor. + * + * @param model model of the game + */ + public AI(Model model) { + this.model = model; + } + + public void doTurn() { + // you should fill this method + // an example is included here + } + +} diff --git a/src/client/Controller.java b/src/client/Controller.java new file mode 100644 index 0000000..5ea698e --- /dev/null +++ b/src/client/Controller.java @@ -0,0 +1,174 @@ +package client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import server.network.data.Message; +import util.Log; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Main controller. Controls execution of the program, e.g. checks time limit of + * the client, handles incoming messages, controls network operations, etc. + */ +public class Controller { + + /** + * Logging tag. + */ + private static final String TAG = "Controller"; + + /** + * Connection details' file encoding. + */ + private static String detailsEnc = "UTF-8"; + + /** + * Connection details. + */ + private int port; + private String host; + private String token; + + /** + * Player of the game. + */ + private AI client; + + /** + * Model of the game. + */ + private Model model; + + /** + * Client side network. + */ + private Network network; + + /** + * Terminator. Controller waits for this object to be notified. Then it will + * be terminated. + */ + private final Object terminator; + + /** + * Name of the file which contains connection details. + */ + private String settingsFile; + + /** + * Timer used to limit execution of {@link AI#doTurn}. + */ + private Timer timer; + + + /** + * Constructor. + * + * @param settingsFile name of the file which contains connection details + */ + public Controller(String settingsFile) { + timer = new Timer(); + terminator = new Object(); + this.settingsFile = settingsFile; + } + + /** + * Starts a client by connecting to the server and sending a token. + */ + public void start() { + try { + readClientData(); + model = new Model(); + client = new AI(model); + network = new Network(this::handleMessage); + network.setConnectionData(host, port, token); + while (!network.isConnected()) + network.connect(); + synchronized (terminator) { + terminator.wait(); + } + network.terminate(); + } catch (Exception e) { + Log.i(TAG, "Error while starting client.", e); + } + } + + /** + * Handles incoming message. This method will be called from + * {@link client.Network} when a new message is received. + * + * @param msg incoming message + */ + private void handleMessage(Message msg) { + if (msg.name.equals("turn")) + handleTurnMessage(msg); + else if (msg.name.equals("init")) + handleInitMessage(msg); + } + + /** + * Handles init message. + * + * @param msg init message + */ + private void handleInitMessage(Message msg) { + model.handleInitMessage(msg); + } + + /** + * Handles turn message. Gives the message to the model and then executes + * client's code to do next turn. + * + * @param msg turn message + */ + private void handleTurnMessage(Message msg) { + model.handleTurnMessage(msg); + doTurn(); + } + + /** + * Starts {@link AI#doTurn} with turn timeout. + */ + private void doTurn() { + Thread turn = new Thread() { + @Override + public void run() { + // set timeout + timer.schedule(new TimerTask() { + @Override + public void run() { + // interrupt turn thread + interrupt(); + // send result of this turn + network.send(model.getClientTurn()); + } + }, model.getTurnTimeout()); + + // do client's turn + client.doTurn(); + } + }; + turn.start(); + } + + /** + * Reads data of client from the file {@link #settingsFile}. + * + * @throws IOException if an I/O exception occurs during read of the file. + */ + private void readClientData() throws IOException { + byte fileData[] = Files.readAllBytes(Paths.get(settingsFile)); + String json = new String(fileData, detailsEnc); + Gson gson = new Gson(); + JsonObject details = gson.fromJson(json, JsonObject.class); + host = details.getAsJsonPrimitive("ip").getAsString(); + port = details.getAsJsonPrimitive("port").getAsInt(); + token = details.getAsJsonPrimitive("token").getAsString(); + } + +} diff --git a/src/client/Main.java b/src/client/Main.java new file mode 100644 index 0000000..21a29df --- /dev/null +++ b/src/client/Main.java @@ -0,0 +1,12 @@ +package client; + +/** + * Initial point of execution. + */ +public class Main { + + public static void main(String[] args) { + new Controller("resources/client/connection.conf").start(); + } + +} diff --git a/src/client/Model.java b/src/client/Model.java new file mode 100644 index 0000000..930c4b9 --- /dev/null +++ b/src/client/Model.java @@ -0,0 +1,39 @@ +package client; + +import server.network.data.Message; + +/** + * Model contains data which describes current state of the game. + */ +public class Model { + + private long turnTimeout; + private long turnStartTime; + + public void handleInitMessage(Message msg) { + // game developers' todo store data + } + + public void handleTurnMessage(Message msg) { + turnStartTime = System.currentTimeMillis(); + // game developers' todo store data + } + + public long getTurnTimeout() { + return turnTimeout; + } + + public long getTurnTimePassed() { + return System.currentTimeMillis() - turnStartTime; + } + + public long getTurnRemainingTime() { + return turnTimeout - getTurnTimePassed(); + } + + public Message getClientTurn() { + // game developers' todo collect client's events as a single message + return null; + } + +} diff --git a/src/client/Network.java b/src/client/Network.java new file mode 100644 index 0000000..f526e5b --- /dev/null +++ b/src/client/Network.java @@ -0,0 +1,160 @@ +package client; + +import server.network.JsonSocket; +import server.network.data.Message; +import util.Log; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +/** + * {@link Network} is responsible for connecting to server, and + * sending/receiving messages to/from server. + */ +public class Network { + + /** + * Logging tag. + */ + private static final String TAG = "Network"; + + /** + * Handles incoming messages. + */ + private Consumer messageHandler; + + /** + * Connection details. + */ + private int port; + private String host; + private String token; + + /** + * Socket of the client. + */ + private JsonSocket client; + + /** + * Connection flag. + */ + private boolean isConnected; + + /** + * Executor used to receive messages. + */ + private ExecutorService executor; + + /** + * Termination flag. + */ + private boolean terminateFlag; + + + /** + * Constructor. + * + * @param messageHandler handles incoming messages + */ + public Network(Consumer messageHandler) { + this.messageHandler = messageHandler; + executor = Executors.newSingleThreadExecutor(); + } + + /** + * Setter for connection details. + * + * @param host server's host (ip) + * @param port server's port + * @param token client's token + */ + public void setConnectionData(String host, int port, String token) { + this.host = host; + this.port = port; + this.token = token; + } + + /** + * Tries to connect and send token to the server. + */ + public void connect() { + isConnected = false; + JsonSocket client; + Message init; + try { + client = new JsonSocket(host, port); + client.send(new Message("token", new Object[]{token})); + init = client.get(Message.class); + if (!init.name.equals("init")) { + client.close(); + throw new Exception("First message of the server was not init message."); + } + } catch (Exception e) { + Log.i(TAG, "Error while connection to server.", e); + return; + } + isConnected = true; + this.client = client; + messageHandler.accept(init); + startReceiving(); + } + + /** + * Starts listening for the server messages. + */ + private void startReceiving() { + executor.submit(() -> { + while (!terminateFlag) + doReceive(); + executor.shutdownNow(); + executor = null; + }); + } + + /** + * Listens for a single message of the server. + */ + private void doReceive() { + try { + messageHandler.accept(client.get(Message.class)); + } catch (IOException e) { + Log.i(TAG, "Error receiving the server's message.", e); + } + } + + /** + * Sends a message to the server. + * + * @param msg message to send + */ + public void send(Message msg) { + try { + client.send(msg); + } catch (IOException e) { + Log.i(TAG, "Error while sending client's message.", e); + } + } + + /** + * Terminates operations of the network. + */ + public void terminate() { + terminateFlag = true; + try { + client.close(); + } catch (IOException e) { + Log.i(TAG, "Error closing the client.", e); + } + } + + public boolean isConnected() { + return isConnected; + } + + public boolean isTerminated() { + return terminateFlag; + } + +} diff --git a/src/server/Server.java b/src/server/Server.java index 3640617..b4db9d3 100644 --- a/src/server/Server.java +++ b/src/server/Server.java @@ -1,12 +1,290 @@ package server; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import server.core.CommandHandler; +import server.core.Factory; import server.core.GameHandler; +import server.core.GameLogic; +import server.core.model.ClientInfo; +import server.network.ClientNetwork; import server.network.TerminalNetwork; +import server.network.UINetwork; +import server.network.data.Message; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; /** - * Created by Razi on 12/6/2014. + * Class is the main Server of the game with networks and game logic within. + *

+ * {@link server.Server Server} class is the main runner of the framework activities. + * Creation of the server class will assign the proper objects with the preferred configurations based on + * files within the config files. + * The class creates a terminal with connection through a terminal network. + * Also sets the {@link server.core.GameLogic GameLogic} subclass instance to the + * {@link server.core.GameHandler GameHandler} of the framework with the utility + * {@link server.core.Factory Factory} class. + * Any mistake in the config files causes rising a runtime exception. + *

*/ public class Server { - private TerminalNetwork terminalNetwork; - public GameHandler gameHandler; + + private static final String RESOURCE_PATH_TERMINAL = "resources/network/terminal.conf"; + private static final String RESOURCE_PATH_UI = "resources/network/ui.conf"; + private static final String RESOURCE_PATH_CLIENT = "resources/network/client.conf"; + + private Factory mFactory; + private TerminalNetwork mTerminalNetwork; + private GameHandler mGameHandler; + TerminalConfig mTerminalConfig; + private UIConfig mUIConfig; + private ClientConfig mClientConfig; + + ClientInfo[] mClientsInfo; + + /** + * Constructor of main server of the framework, which creates and connects server components to the object. + *

+ * This class factory accepts a {@link server.core.Factory Factory} instance in order to set the + * user created subclass of {@link server.core.GameLogic GameLogic} class to the + * {@link server.core.GameHandler GameHandler} of the server. + * The configuration of {@link server.network.TerminalNetwork TerminalNetwork} and + * {@link server.network.UINetwork UINetwork} are inside a file in resources folder. + * ({@see https://github.com/JavaChallenge/JGFramework/wiki wiki}) + * Occurring any error during parsing config json files, causes a runtime exception to be thrown. + *

+ * @param factory The factory class, implemented by user. + */ + public Server(Factory factory) { + mFactory = factory; + + Gson gson = new Gson(); + + File file; + BufferedReader bufferedReader; + + try { + file = new File(RESOURCE_PATH_TERMINAL); + bufferedReader = new BufferedReader(new FileReader(file)); + mTerminalConfig = gson.fromJson(bufferedReader, TerminalConfig.class); + } catch (FileNotFoundException notFound) { + throw new RuntimeException("Terminal config file not found"); + } catch (JsonParseException parse) { + throw new RuntimeException("Terminal config file does not meet expected syntax"); + } + mTerminalNetwork = new TerminalNetwork(mTerminalConfig.getTerminalToken()); + + try { + file = new File(RESOURCE_PATH_UI); + bufferedReader = new BufferedReader(new FileReader(file)); + mUIConfig = gson.fromJson(bufferedReader, UIConfig.class); + } catch (FileNotFoundException notFound) { + throw new RuntimeException("UI config file not found"); + } catch (JsonParseException parse) { + throw new RuntimeException("UI config file does not meet the expected syntax"); + } + mGameHandler = new GameHandler(new ClientNetwork(), new UINetwork(mUIConfig.getUIToken())); + mGameHandler.init(); + + try { + file = new File(RESOURCE_PATH_CLIENT); + bufferedReader = new BufferedReader(new FileReader(file)); + mClientConfig = gson.fromJson(bufferedReader, ClientConfig.class); + } catch (FileNotFoundException notFound) { + throw new RuntimeException("Client config file not found"); + } catch (JsonParseException parse) { + throw new RuntimeException("Client config file does not meet the expected syntax"); + } + + setCommandHandler(new CommandHandler()); + } + + public void setCommandHandler(CommandHandler commandHandler) { + mTerminalNetwork.setHandler(commandHandler); + commandHandler.setServer(this); + } + + public Factory getFactory() { + return mFactory; + } + + public TerminalNetwork getTerminalNetwork() { + return mTerminalNetwork; + } + + public GameHandler getGameHandler() { + return mGameHandler; + } + + /** + * Starts the server by make it listening and responding to the Terminal. + */ + public void start() { + mTerminalNetwork.listen(mTerminalConfig.getTerminalPort()); + } + + public void newGame(String[] options, long uiTimeout, long clientTimeout) { + GameLogic gameLogic = mFactory.getGameLogic(options); + gameLogic.init(); + mGameHandler.setGameLogic(gameLogic); + mClientsInfo = gameLogic.getClientInfo(); + mGameHandler.setClientsInfo(mClientsInfo); + for (int i = 0 ; i < mClientsInfo.length; ++i) { + int id = mGameHandler.getClientNetwork().defineClient(mClientsInfo[i].getToken()); + if (id != i) { + throw new RuntimeException("Client ID and client order does not match"); + } + mClientsInfo[i].setID(id); + } + + mGameHandler.getUINetwork().listen(mUIConfig.getUIPort()); + mGameHandler.getClientNetwork().listen(mClientConfig.getClientPort()); + + try { + mGameHandler.getUINetwork().waitForClient(uiTimeout); + } catch (InterruptedException e) { + throw new RuntimeException("Waiting for ui clients interrupted"); + } + + try { + mGameHandler.getClientNetwork().waitForAllClients(clientTimeout); + } catch (InterruptedException e) { + throw new RuntimeException("Waiting for clients interrupted"); + } + + Message initialMessage = gameLogic.getUIInitialMessage(); + mGameHandler.getUINetwork().sendBlocking(initialMessage); + + Message[] initialMessages = gameLogic.getClientInitialMessages(); + for (int i = 0; i + * This class will be created from the terminal config json file using the + * google-gson object. + * The token is the token of the {@link server.network.TerminalNetwork TerminalNetwork} + * and the port is the port to connect to the {@link server.network.TerminalNetwork TerminalNetwork}. + *

+ */ + private static class TerminalConfig { + private String token; + private int port; + + public TerminalConfig(String token, int port) { + this.token = token; + this.port = port; + } + + /** + * Getter for the terminal token. + *

+ * This method will check the token for a valid 32-character token and returns it as an + * {@link java.lang.String String}. + * In the case of an invalid token, raises a runtime exception. + *

+ * @return 32-character token + */ + public String getTerminalToken() { + if (token.matches("(.){32}") && token.length() == 32) { + return token; + } + throw new RuntimeException("Invalid terminal token in config file"); + } + + /** + * Getter for the terminal port. + *

+ * This method checks the port for a valid number and returns it as an integer. + * In the case of an invalid port number (out of range numbers), raises a runtime exception. + *

+ * @return Integer port number + */ + public int getTerminalPort() { + if (port > 0 && port <= 65535) { + return port; + } else { + throw new RuntimeException("Invalid terminal port number in config file"); + } + } + } + + /** + * + */ + private static class UIConfig { + private String token; + private int port; + + public UIConfig(String token, int port) { + this.token = token; + this.port = port; + } + + /** + * Getter for the user interface token. + *

+ * This method will check the token for a valid 32-character token and returns it as an + * {@link java.lang.String String}. + * In the case of an invalid token, raises a runtime exception. + *

+ * @return 32-character token + */ + public String getUIToken() { + if (token.matches("(.){32}") && token.length() == 32) { + return token; + } + throw new RuntimeException("Invalid UI token in config file"); + } + + /** + * Getter for the ui port. + *

+ * This method checks the port for a valid number and returns it as an integer. + * In the case of an invalid port number (out of range numbers), raises a runtime exception. + *

+ * @return Integer port number + */ + public int getUIPort() { + if (port > 0 && port <= 65535) { + return port; + } else { + throw new RuntimeException("Invalid ui port number in config file"); + } + } + } + + /** + * + */ + private static class ClientConfig { + private int port; + + public ClientConfig(int port) { + this.port = port; + } + + /** + * Getter for the client server port. + *

+ * This method checks the port for a valid number and returns it as an integer. + * In the case of an invalid port number (out of range numbers), raises a runtime exception. + *

+ * @return Integer port number + */ + public int getClientPort() { + if (port > 0 && port <= 65535) { + return port; + } else { + throw new RuntimeException("Invalid ui port number in config file"); + } + } + } } diff --git a/src/server/core/CommandHandler.java b/src/server/core/CommandHandler.java new file mode 100644 index 0000000..d474a81 --- /dev/null +++ b/src/server/core/CommandHandler.java @@ -0,0 +1,84 @@ +package server.core; + +import server.Server; +import server.core.model.Event; +import server.network.TerminalNetwork; +import server.network.data.Message; + +import java.util.HashMap; +import java.util.function.UnaryOperator; + +/** + * Created by alilotfi on 2/2/15. + */ +public class CommandHandler implements TerminalNetwork.TerminalInterface { + + public static final String REPORT_NAME = "report"; + + protected Server mServer; + private HashMap> mHandlers = new HashMap<>(); + + public CommandHandler() { + defineCommand("status", this::cmdStatus); + defineCommand("newGame", this::cmdNewGame); +// defineCommand("loadGame", this::cmdLoadGame); + defineCommand("startGame", this::cmdStartGame); +// defineCommand("pauseGame", this::cmdPauseGame); +// defineCommand("resumeGame", this::cmdResumeGame); +// defineCommand("endGame", this::cmdEndGame); +// defineCommand("exit", this::cmdExit); + } + + public void defineCommand(String command, UnaryOperator handler) { + mHandlers.put(command, handler); + } + + public void setServer(Server server) { + mServer = server; + } + + @Override + public Message runCommand(Message cmd) { + if (cmd == null || cmd.name == null) + return null; + UnaryOperator handler = mHandlers.get(cmd.name); + if (handler != null) { + return handler.apply(cmd); + } else { + return new Message(REPORT_NAME, new Object[]{ + "This command is not defined." + }); + } + } + + public Message cmdStatus(Message cmd) { + return new Message(REPORT_NAME, new Object[] { + "Number of connected clients: " + mServer.getGameHandler().getClientNetwork().getNumberOfConnected() + }); + } + + public Message cmdNewGame(Message cmd) { + String args[] = null; + if (cmd.args != null) { + args = new String[cmd.args.length]; + for (int i = 0; i < args.length; i++) + args[i] = (String) cmd.args[i]; + } + mServer.newGame(args, 5*60*1000, 5*60*1000); // todo: change static timeouts + return new Message(REPORT_NAME, null); + } + + public Message cmdStartGame(Message cmd) { + mServer.getGameHandler().start(); + return new Message(REPORT_NAME, new Object[] { + "Game started successfully!" + }); + } + + + @Override + public void putEvent(Event event) { + mServer.getGameHandler().queueEvent(event); + } + +} diff --git a/src/server/core/Factory.java b/src/server/core/Factory.java new file mode 100644 index 0000000..5b9ec51 --- /dev/null +++ b/src/server/core/Factory.java @@ -0,0 +1,19 @@ +package server.core; + +/** + * Factory class is based on factory design pattern, and returns preferred subclass of the + * {@link server.core.GameLogic GameLogic} class which represents the main game logic engine of the game. + *

+ * The user must fill in this method to return the preferred subclass of the + * {@link server.core.GameLogic GameLogic} in order to be used in the + * {@link server.core.GameHandler GameHandler} class + *

+ */ +public interface Factory { + + /** + * The user fills in this abstract method to return the preferred subclass of the game logic. + * @return The preferred child of the {@link server.core.GameLogic GameLogic} class + */ + public GameLogic getGameLogic(String[] options); +} diff --git a/src/server/core/GameHandler.java b/src/server/core/GameHandler.java index 3a475da..0ebc20a 100644 --- a/src/server/core/GameHandler.java +++ b/src/server/core/GameHandler.java @@ -1,13 +1,344 @@ package server.core; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import server.core.model.ClientInfo; +import server.core.model.Event; import server.network.ClientNetwork; import server.network.UINetwork; +import server.network.data.Message; +import util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.concurrent.*; /** - * Created by Razi on 12/6/2014. + * Core controller of the framework, controls the {@link server.core.GameLogic GameLogic}, main loop of the game and + * does the output controlling operations. + *

+ * This class runs the main running thread of the framework. Class interacts with the clients, UI, and the + * GameLogic itself. + * Threads in this class, will gather the clients' events + * (See also {@link server.network.ClientNetwork ClientNetwork}), send them to the main Game + * (See also {@link server.core.GameLogic GameLogic}) + * The output will be manipulated and sent to the appropriate controller within a inner module of the class + * (OutputController). + * The sequence of the creation and running the operations of this class will be through the call of the following + * methods. + * {@link server.core.GameHandler#init() init()}, {@link server.core.GameHandler#start() start()} and then at the + * moment the external terminal user wants to shut down the games loop (except than waiting for the + * {@link server.core.GameLogic GameLogic} to flag the end of the game), the + * {@link server.core.GameHandler#shutdown() shutdown()} method would be called. + * Note that shutting down the {@link server.core.GameHandler GameHandler} will not immediately stop the threads, + * actually it will set a shut down request flag in the class, which will closes the thread in the aspect of + * accepting more inputs, and the terminate the threads as soon as the operation queue got empty. + *

*/ public class GameHandler { - private ClientNetwork clientNetwork; - private UINetwork uiNetwork; - private GameLogic gameLogic; + + private static final String RESOURCE_PATH_OUTPUT_HANDLER = "resources/game_handler/output_handler.conf"; + private static final String RESOURCE_PATH_TURN_TIMEOUT = "resources/game_handler/turn_timeout.conf"; + private final long GAME_LOGIC_SIMULATE_TIMEOUT; + private final long GAME_LOGIC_TURN_TIMEOUT = 1000; + private final long CLIENT_RESPONSE_TIME; + + private ClientNetwork mClientNetwork; + private UINetwork mUINetwork; + private GameLogic mGameLogic; + private OutputController mOutputController; + private ClientInfo[] mClientsInfo; + + private Loop mLoop; + BlockingQueue terminalEventsQueue; + + /** + * Constructor of the {@link server.core.GameHandler GameHandler}, connects the handler to the Clients through + * {@link server.network.ClientNetwork ClientNetwork} and to the UI through + * {@link server.network.UINetwork UINetwork}. + *

+ * The constructor accepts the instances of {@link server.core.GameHandler GameHandler} and + * {@link server.network.ClientNetwork ClientNetwork} classes. Then sets some configurations of the loops + * within the "turn_timeout.conf" file ({@see https://github.com/JavaChallenge/JGFramework/wiki wiki}). + *

+ * @param clientNetwork Network which the game will contact to the clients through + * @param uiNetwork Network which the game will contact to the UI through + */ + public GameHandler(ClientNetwork clientNetwork, UINetwork uiNetwork) { + mClientNetwork = clientNetwork; + mUINetwork = uiNetwork; + terminalEventsQueue = new LinkedBlockingQueue<>(); + + Gson gson = new Gson(); + try { + File file = new File(RESOURCE_PATH_TURN_TIMEOUT); + BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); + TimeConfig timeConfig = gson.fromJson(bufferedReader, TimeConfig.class); + GAME_LOGIC_SIMULATE_TIMEOUT = timeConfig.getClientResponseTime(); + CLIENT_RESPONSE_TIME = timeConfig.getUIResponseTime(); + } catch (FileNotFoundException notFound) { + throw new RuntimeException("turn_timeout config file not found"); + } catch (JsonParseException e) { + throw new RuntimeException("turn_time config file does not meet expected syntax"); + } + } + + /** + * Initializes the game handler module based on the given game. + *

+ * This method creates needed instance of module {@link server.core.OutputController OutputController} based on + * configurations located in the "output_handler.conf" file + * ({@see https://github.com/JavaChallenge/JGFramework/wiki wiki}). + * Any problem in loading or parsing the configuration JSON files, will result in rising a runtime exception. + *

+ */ + public void init() { + OutputHandlerConfig outputHandlerConfig; + Gson gson = new Gson(); + try { + File file = new File(RESOURCE_PATH_OUTPUT_HANDLER); + BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); + outputHandlerConfig = gson.fromJson(bufferedReader, OutputHandlerConfig.class); + } catch (FileNotFoundException notFound) { + throw new RuntimeException("Output handler config file not found"); + } catch (JsonParseException e) { + throw new RuntimeException("Output handler config file does not meet expected syntax"); + } + mOutputController = new OutputController(outputHandlerConfig.sendToUI, + mUINetwork, + outputHandlerConfig.timeInterval, + outputHandlerConfig.sendToFile, + outputHandlerConfig.getFile(), + outputHandlerConfig.bufferSize); + } + + /** + * Setter of the game logic, will set the main game engine of the {@link server.core.GameHandler GameHandler} to + * the given instance of {@link server.core.GameLogic GameLogic}. + * @param gameLogic The given instance of the {@link server.core.GameLogic GameLogic} to set in the object. + */ + + public void setGameLogic(GameLogic gameLogic) { + mGameLogic = gameLogic; + } + + /** + * Getter of the {@link server.network.ClientNetwork ClientNetwork} class which is used to contact to the clients + * through it. + * @return The {@link server.network.ClientNetwork ClientNetwork} instance used to connect to clients + */ + public ClientNetwork getClientNetwork() { + return mClientNetwork; + } + + /** + * Getter of the {@link server.network.UINetwork UINetwork} class instance, which is used in order to connect to + * the UI. + * @return Instance of {@link server.network.UINetwork UINetwork} class, used to send messages to user interface + */ + public UINetwork getUINetwork() { + return mUINetwork; + } + + /** + * Setter of the {@link server.core.model.ClientInfo ClientInfo} instance stored in the class in order to recognize + * the clients from each other + * @param clientsInfo Array of information of the clients held in the class + */ + public void setClientsInfo(ClientInfo[] clientsInfo) { + mClientsInfo = clientsInfo; + } + + /** + * Starts the main game ({@link server.core.GameLogic GameLogic}) loop and the + * {@link server.core.OutputController OutputController} operations in two new {@link java.lang.Thread Thread}. + */ + public void start() { + mLoop = new Loop(); + new Thread(mLoop).start(); + new Thread(mOutputController).start(); + } + + /** + * Registers a shutdown request into the main loop and {@link server.core.OutputController OutputController} class + *

+ * Note that the shutdown requests, will be responded as soon as the current queue of operations got freed. + *

+ */ + public void shutdown() { + mLoop.shutdown(); + mOutputController.shutdown(); + } + + /** + * Queues an event to be simulated in the next turn of the loop. + * + * @param event terminal event + */ + public void queueEvent(Event event) { + synchronized (mLoop.terminalEventsQueue) { + mLoop.terminalEventsQueue.add(event); + } + } + + /** + * In order to give the loop a thread to be ran beside of the main loop. + *

+ * This inner class has a {@link java.util.concurrent.Callable Callable} part, which is wrote down as a + * runnable code template. This template is composed by the multiple steps in every turn of the game. + *

+ */ + private class Loop implements Runnable { + + private boolean shutdownRequest = false; + + private Event[] environmentEvents; + private Event[] terminalEvents; + private Event[][] clientEvents; + private final ArrayList terminalEventsQueue = new ArrayList<>(); + + /** + * The run method of the {@link java.lang.Runnable Runnable} interface which will create a + * {@link java.util.concurrent.Callable Callable} instance and call it in a while until the finish flag if the + * game had been raised or the shutdown request sent to the class (through + * {@link server.core.GameHandler.Loop#shutdown() shutdown()} method) + */ + @Override + public void run() { + Callable simulate = () -> { + mGameLogic.simulateEvents(terminalEvents, environmentEvents, clientEvents); + mGameLogic.generateOutputs(); + if (mGameLogic.isGameFinished()) { + mLoop.shutdown(); + mOutputController.shutdown(); + } + + mOutputController.putMessage(mGameLogic.getUIMessage()); + + Message[] output = mGameLogic.getClientMessages(); + for (int i = 0 ; i < output.length; ++i) { + mClientNetwork.queue(i, output[i]); + } + mClientNetwork.sendAllBlocking(); + + mClientNetwork.startReceivingAll(); + long elapsedTime = System.currentTimeMillis(); + environmentEvents = mGameLogic.makeEnvironmentEvents(); + elapsedTime = System.currentTimeMillis() - elapsedTime; + if (CLIENT_RESPONSE_TIME - elapsedTime > 0) { + try { + wait(CLIENT_RESPONSE_TIME - elapsedTime); + } catch (InterruptedException e) { + throw new RuntimeException("Waiting for clients interrupted"); + } + } + mClientNetwork.stopReceivingAll(); + + clientEvents = new Event[mClientsInfo.length][]; + for (int i = 0; i < mClientsInfo.length; ++i) { + if (mClientNetwork.getReceivedEvent(i) != null) { + clientEvents[i] = mClientNetwork.getReceivedEvent(i); + } + } + //FIXME: Put a blocking queue for terminal + BlockingQueue terminalEventsQueue = new LinkedBlockingQueue<>(); + + synchronized (terminalEventsQueue) { + terminalEvents = terminalEventsQueue.toArray(new Event[terminalEventsQueue.size()]); + terminalEventsQueue.clear(); + } + + return null; + }; +// RunnableFuture runnableSimulate = new FutureTask<>(simulate); +// ExecutorService service = Executors.newSingleThreadExecutor(); + + while (!shutdownRequest) { + long start = System.currentTimeMillis(); + try { + simulate.call(); + } catch (Exception e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + long remaining = GAME_LOGIC_TURN_TIMEOUT - (end - start); + if (remaining <= 0) { + Log.i("GameHandler", "Simulation timeout passed!"); + } else { + try { + Thread.sleep(remaining); + } catch (InterruptedException e) { + Log.i("GameHandler", "Loop interrupted!"); + break; + } + } + } + } + + /** + * Will set the shutdown request flag in order to finish the main {@link server.core.GameHandler.Loop Loop} at + * the first possible turn + */ + public void shutdown() { + this.shutdownRequest = true; + } + } + + /** + * The template of the "output_handler.conf" JSON file. + *

+ * This class could be filled by a {@link com.google.gson.Gson Gson} object. The properties of class are + * normally accessed through {@link server.core.GameHandler#init() init()} method. + *

+ */ + private class OutputHandlerConfig { + private boolean sendToUI; + private int timeInterval; + private boolean sendToFile; + private String filePath; + private int bufferSize; + + public OutputHandlerConfig(boolean sendToUI, + int timeInterval, + boolean sendToFile, + String filePath, + int bufferSize) { + this.sendToUI = sendToUI; + this.timeInterval = timeInterval; + this.sendToFile = sendToFile; + this.filePath = filePath; + this.bufferSize = bufferSize; + } + private File getFile() { + return new File(filePath); + } + } + + /** + * The template of the "turn_timeout.conf" JSON file. + *

+ * This class could be filled by a {@link com.google.gson.Gson Gson} object. The properties of class are + * normally accessed through {@link server.core.GameHandler#GameHandler GameHandler Constructor} method. + *

+ */ + private static class TimeConfig { + private long mClientResponseTime; + private long mSimulateTimeout; + + public TimeConfig(long clientResponseTime,long uiResponseTime) { + mClientResponseTime = clientResponseTime; + mSimulateTimeout = uiResponseTime; + } + + public long getClientResponseTime() { + return mClientResponseTime; + } + + public long getUIResponseTime() { + return mSimulateTimeout; + } + } } diff --git a/src/server/core/GameLogic.java b/src/server/core/GameLogic.java index f0fe94f..58ccab4 100644 --- a/src/server/core/GameLogic.java +++ b/src/server/core/GameLogic.java @@ -1,7 +1,74 @@ package server.core; +import server.core.model.ClientInfo; +import server.core.model.Event; +import server.network.data.Message; + +import java.util.HashMap; +import java.util.UUID; + +import java.util.HashMap; + /** - * Created by Razi on 12/6/2014. + * The abstract class representing the main game logic of the user's game. + *

+ * This class will be the simulator engine of the game. + *

*/ -public abstract class GameLogic { +public interface GameLogic { + + //TODO: Saeed Rajab, change these... common baby... + /** + * This method must send initial and necessary values to UI and clients. + * @return A hashmap that has Token as key and a Message as value. + * Token is used for specifying if the Message is for UI or Client. + */ + public void init(); + + /** + * + * @return + */ + public Message getUIInitialMessage(); + + /** + * + * @return + */ + public Message[] getClientInitialMessages(); + + /** + * + * @return + */ + public ClientInfo[] getClientInfo(); + + /** + * Simulate events based on the current turn event and calculate the changes in game. + * @param terminalEvent Events that user enters in terminal. + * @param environmentEvent Events that is related to environment. Suppose we want to develop a strategic game. + * Increasing/Decreasing a specific resource in map is an environment event. + * @param clientsEvent Events that is related to client e.g. moving the player. + */ + public void simulateEvents(Event[] terminalEvent, Event[] environmentEvent, Event[][] clientsEvent); + + /** + * This method generates the output based on the changes that were calculated in + * {@link #simulateEvents(server.core.model.Event[], server.core.model.Event[], java.util.HashMap)}. + * @return A hashmap that has Token as key and a Message as value. + * Token is used for specifying if the Message is for UI or Client. + */ + public void generateOutputs(); + + public Message getUIMessage(); + + public Message[] getClientMessages(); + + /** + * This method is used for making the environment events. + * @return An array that is environment events. + */ + public Event[] makeEnvironmentEvents(); + + public boolean isGameFinished(); } diff --git a/src/server/core/OutputController.java b/src/server/core/OutputController.java new file mode 100644 index 0000000..307f1ec --- /dev/null +++ b/src/server/core/OutputController.java @@ -0,0 +1,380 @@ +package server.core; + +import server.exceptions.OutputControllerQueueOverflowException; +import server.network.UINetwork; +import server.network.data.Message; + +import java.io.*; +import java.util.LinkedList; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; + +/** + * Class created as a part of {@link GameHandler GameHandler} class for controlling the output. + *

+ * This class gathers all output taken from "GameLogic" into a queue, and the process them as the user + * wishes. Currently passing to {@link server.network.UINetwork UINetwork} and saving in a + * local file is supported. + *

+ */ +public class OutputController implements Runnable { + + private static final int QUEUE_DEFAULT_SIZE = 100000; + + private boolean sendToUI = false; + private UINetwork uiNetwork; + private int timeInterval; + private Timer timer; + private boolean sendToFile = false; + private File outputFile; + private int bufferSize; + private FileWriter fileWriter; + private LinkedList messagesQueue; + + /** + * Constructor with the properties about how outputs must be handled. + *

+ * This constructor creates an instance of OutputController class with the given parameters. + * The two booleans indicate that how the class will handle the given outputs. + * In the case that sendToUI flag is set: + *

+ * Output handling will be assumed as a "Pre Runner" in order to a fast forward run based + * on the game logic, and storing data on a file for later use. + * Data would be held on memory until reaches the specified buffer size, and then will be + * written to the specified file (Using {@link server.core.OutputController.FileWriter FileWriter} + * runnable class). + *

+ * In the case that sendToFile flag is set: + *

+ * This type of output processing, will store outputs in the object, and sends it in regular + * timer ticks. This time is specified in milliseconds as timeInterval + *

+ * If the sendToUI or sendToFile flags were set, then the two other values would be assigned to the + * appropriate instance values. + * Note that this will raise a runtime exception, in the case of invalid arguments. + *

+ * @param sendToUI Indicates that data will be sent to given {@link server.network.UINetwork UINetwork} instance + * or not + * @param uiNetwork The given instance of {@link server.network.UINetwork UINetwork} class to send data to + * @param timeInterval The indicated timer tick which triggers + * {@link server.core.OutputController.UINetworkSender#sendToUINetwork(server.network.data.Message) + * sendToUINetwork(Message)} method + * @param sendToFile Indicates that a log of output will be saved in the given {@link java.io.File java.io.File} or + * not + * @param outputFile The given {@link java.io.File java.io.File} to save data within + * @param bufferSize The preferred number of message elements to be held on memory before writing to file + */ + public OutputController(boolean sendToUI, UINetwork uiNetwork, int timeInterval + , boolean sendToFile, File outputFile, int bufferSize) { + messagesQueue = new LinkedList<>(); + this.sendToUI = sendToUI; + if (sendToUI) { + if (uiNetwork != null) { + this.uiNetwork = uiNetwork; + } else { + throw new RuntimeException("The given parameter as uiNetwork is null"); + } + + if (timeInterval > 0) { + this.timeInterval = timeInterval; + } else { + throw new RuntimeException("The given parameter as timeInterval is invalid"); + } + } + this.sendToFile = sendToFile; + if (sendToFile) { + this.outputFile = outputFile; + try { + this.outputFile.createNewFile(); + } catch (IOException e) { + throw new RuntimeException("Could not create log file"); + } + + if (bufferSize > 0 && bufferSize <= QUEUE_DEFAULT_SIZE) { + this.bufferSize = bufferSize; + } else if (bufferSize > 0) { + throw new RuntimeException("The given parameter as bufferSize is greater than max queue size"); + } else { + throw new RuntimeException("The given parameter as bufferSize is not valid"); + } + } + } + + /** + * Run method implemented from {@link Runnable java.lang.Runnable} class, which will set and run the two type + * of output handlers. + *

+ * In this method, if the sendToUI mode got activated in the class, then a {@link java.util.Timer Timer} will + * be scheduled in the given period to send data to {@link server.network.UINetwork UINetwork}. + * And also if the sendToFile mode got activated, then a thread of + * {@link server.core.OutputController.FileWriter FileWriter} class will be run. + *

+ */ + @Override + public void run() { + if (sendToUI) { + timer = new Timer(); + timer.scheduleAtFixedRate(new UINetworkSender(), 0, timeInterval); + } + + if (sendToFile) { + fileWriter = new FileWriter(outputFile); + } + new Thread(fileWriter).start(); + } + + /** + * Accepts an instance of {@link server.network.data.Message Message} class as an argument, and places it + * on the queue. + *

+ * In this method, the given message will be putted in the message queue, only if there's a place on the + * queue. Otherwise the cleaning and caching processes will be done (through the + * {@link #hanldeOverflow() hanldeOverflow} method). + * Also if the buffer size condition is met, then the file writer method will be called with an + * alternative thread, to save the contents on the file. + *

+ * @param message The given message (as output) to put in the message queue. + */ + public synchronized void putMessage(Message message) { + if (messagesQueue.size() <= QUEUE_DEFAULT_SIZE) { + messagesQueue.addLast(message); + if (messagesQueue.size() == bufferSize && sendToFile) { + fileWriter.putMessages(messagesQueue); + messagesQueue = new LinkedList<>(); + } + } else { + if (hanldeOverflow()) { + messagesQueue.addLast(message); + } else { + throw new OutputControllerQueueOverflowException("Could not handle message queue overflow"); + } + } + synchronized (messagesQueue) { + messagesQueue.notifyAll(); + } + } + + /** + * Method created to handle the possible overflows occurance in the message queue. + *

+ * TODO: INCOMPLETE - Must be implemented to cache the queue to file. + *

+ * @return True if the queue unblocked, false if any error occur during this operation + */ + private boolean hanldeOverflow() { + messagesQueue.clear(); + return true; + } + + /** + * Tries to shutdown all the threads ran in this class so far. + *

+ * This method calls the close on the {@link server.core.OutputController.FileWriter FileWriter} instance of + * the object, causing it to interrupt as soon as all files wrote down on the file. + * Also cancels the timer to stop automatically invocation of + * {@link server.core.OutputController.UINetworkSender UINetworkSender}. + * Other threads would be closed automatically ({@link server.core.OutputController.UINetworkSender + * UINetworkSenders} after the timeout). + *

+ */ + public void shutdown() { + fileWriter.close(); + timer.cancel(); + } + + /** + * This inner class is used to do processes needed during the file saving operations, as an alternative + * thread. + *

+ * This class will be run by the "OutputController" class, as a file save operation is needed. + * This Runnable implemented class, could do the long term file saving operations, as an alternative + * thread. + * This class uses a {@link java.util.concurrent.BlockingQueue BlockingQueue>} + * implementation in order to save files without any block on the way of main thread. + *

+ */ + private class FileWriter implements Runnable { + + private boolean open = false; + private File file; + private BlockingQueue> messagesQueue; + + /** + * Constructor of the class which accepts a File and sets it as the output of writing operations. + * @param file Given File to store message data in + */ + public FileWriter(File file) { + this.file = file; + this.messagesQueue = new ArrayBlockingQueue<>(QUEUE_DEFAULT_SIZE); + } + + + /** + * Puts the given message in the {@link java.util.concurrent.BlockingQueue BlockingQueue} in order to + * store to file as soon as possible. + *

+ * This method makes the class enable to have a local copy of message queue, during the file + * processing operations (The outer copy could be destroyed). + *

+ * @param messages The given message linked list, in order to save as a local copy in + * {@link java.util.concurrent.BlockingQueue BlockingQueue} + */ + public void putMessages(LinkedList messages) { + try { + this.messagesQueue.put(messages); + synchronized (messagesQueue) { + messagesQueue.notifyAll(); + } + } catch (InterruptedException e) { + throw new RuntimeException("Put messages in FileWriter blocking queue interrupted"); + } + } + + /** + * The implemented run method of the {@link Runnable java.lang.Runnable} class which will + * starts the thread listening for any input in order to save to file. + *

+ * This method uses a while loop to save the whole contents of + * {@link java.util.concurrent.BlockingQueue BlockingQueue} of + * {@link server.network.data.Message Messages} to the given file. + * This will stop working only if the close order were sent and there's no more + * {@link server.network.data.Message Messages} on the queue. + *

+ */ + @Override + public void run() { + open = true; + while (open || messagesQueue.size() > 0) { + writeToFile(); + } + } + + /** + * Stores the contents of the message queue as an appendix to the FileWriter file. + *

+ * This method will write the whole contents of the saved message queue in the file writer object + * on the given file object by object. As this operation is completed, the file writer tries to write the + * next file in the {@link java.util.concurrent.BlockingQueue BlockingQueue} + *

+ */ + private void writeToFile() { + try { + try { + OutputStream outputStream = new FileOutputStream(file); + OutputStream buffer = new BufferedOutputStream(outputStream); + ObjectOutput outputFile = new ObjectOutputStream(buffer); + while (!messagesQueue.isEmpty()) { + for (Message message : messagesQueue.take()) { + outputFile.writeObject(message); + } + } + } catch (FileNotFoundException fileNotFound) { + throw new RuntimeException("Could not find the log file"); + } catch (IOException ioException) { + throw new RuntimeException("Could not write on the log file"); + } + synchronized (messagesQueue) { + notifyAll(); + } + } catch (InterruptedException e) { + throw new RuntimeException("Taking from FileWriter message queue interrupted."); + } + } + + /** + * This will send to the class a request to close connection to the file. + *

+ * This method sets the open flag of class to false and so as soon as + * {@link java.util.concurrent.BlockingQueue BlockingQueue} is empty, the writing operation will be + * finished and the thread will be killed. + *

+ */ + public void close() { + this.open = false; + } + } + + /** + * Is responsible for sending the first {@link server.network.data.Message Message} in the queue to the + * {@link server.network.UINetwork UINetwork}. + *

+ * As the failure in connection and some other issues leaves the + * {@link server.network.UINetwork#sendBlocking(server.network.data.Message) UINetwork.send(Message)} method + * blocked, and so causes a thread block, this class runs as an alternative thread to send the messages + * in the queue, to the {@link server.network.UINetwork UINetwrok} instance without causing the main + * thread of OutputController to sleep. + *

+ */ + private class UINetworkSender extends TimerTask { + + private final static int CONNECTION_TIMEOUT = 1000; + + private boolean sent = false; + + /** + * Implemented run method from {@link java.lang.Runnable Runnable} class which sends the first message in the + * queue to the {@link server.network.UINetwork UINetwork}. + *

+ * This method will call the {@link #sendToUINetwork(server.network.data.Message) sendToUINetwork(Message)} + * method with the appropriate message instance. + * Every connection made in a separate thread and with a specified timeout. + * If the last connection could send the message within the timeout, then this method will take another + * message from the queue and send to the network, otherwise, the last message will be sent again. + * The caller timer will be canceled if the shutdown request was sent to the class. + *

+ */ + @Override + public void run() { + Message message; + synchronized (messagesQueue) { + while (messagesQueue.size() <= 0) { + try { + messagesQueue.wait(); + } catch (InterruptedException e) { + throw new RuntimeException("Wait on UINetworkSender interrupted"); + } + } + message = messagesQueue.getFirst(); + if (sent) { + messagesQueue.removeFirst(); + } + } + sent = false; + sendToUINetwork(message); + } + + /** + * This method serves the instance of {@link server.network.UINetwork UINetwork} class with messages. + *

+ * This method calls {@link server.network.UINetwork#sendBlocking(server.network.data.Message) send(message)} + * method on {@link server.network.UINetwork UINetwork} instance in order to show up on UI. + * Calling this while the message queue is empty, will put it in the wait mode. + * Basically this method will be called by the timer scheduled according to user preferred + * time interval. + * As the time of sending message to the {@link server.network.UINetwork UINetworks} exceeds the + * timeout limit, then the execution will be cancelled. + *

+ */ + private void sendToUINetwork(Message message) { + Callable run = () -> { + uiNetwork.sendBlocking(message); + return null; + }; + RunnableFuture runnableFuture = new FutureTask<>(run); + ExecutorService service = Executors.newSingleThreadExecutor(); + service.execute(runnableFuture); + try { + runnableFuture.get(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS); + sent = true; + } catch (ExecutionException execution) { + throw new RuntimeException("Connection to the UI failed in execution"); + } catch (TimeoutException timeOut) { + runnableFuture.cancel(true); + } catch (InterruptedException interrupted) { + throw new RuntimeException("Connection to the UI interrupted"); + } + service.shutdown(); + } + } +} diff --git a/src/server/core/model/ClientInfo.java b/src/server/core/model/ClientInfo.java new file mode 100644 index 0000000..6a17ce8 --- /dev/null +++ b/src/server/core/model/ClientInfo.java @@ -0,0 +1,50 @@ +package server.core.model; + +/** + * Is a template for information required for creation and assigned to a client of the game. + *

+ * Parts of this class members are required for creating and connecting a client to the + * {@link server.network.ClientNetwork ClientNetwork}, these parts have to be wrote down to the config file in + * "Resources" folder ({@see https://github.com/JavaChallenge/JGFramework/wiki wiki}). + *

+ */ +public class ClientInfo { + + private String mToken; + private String mName; + private int mID; + + /** + * Setter for the ID of the client. + *

+ * ID of every client would be generated inside the {@link server.network.ClientNetwork ClientNetwor} class. + * Then the assigned unique ID will be set to the client through this method. + *

+ * @param ID Assigned ID by the {@link server.network.ClientNetwork ClientNetwork} class. + */ + public void setID(int ID) { + mID = ID; + } + + /** + * Getter of the Token filed of the object. + *

+ * Returns the 32-character token assigned to the client (normally by user) as an String + *

+ * @return String token of the client + */ + public String getToken() { + return mToken; + } + + + //TODO DOC + public void setmName(String mName) { + this.mName = mName; + } + + //TODO DOC + public void setmToken(String mToken) { + this.mToken = mToken; + } +} diff --git a/src/server/core/model/Event.java b/src/server/core/model/Event.java new file mode 100644 index 0000000..8b09db7 --- /dev/null +++ b/src/server/core/model/Event.java @@ -0,0 +1,9 @@ +package server.core.model; + +/** + * Created by Razi on 12/22/2014. + */ +public class Event { + public static final String EVENT = "event"; + +} diff --git a/src/server/exceptions/OutputControllerQueueOverflowException.java b/src/server/exceptions/OutputControllerQueueOverflowException.java new file mode 100644 index 0000000..1dfc891 --- /dev/null +++ b/src/server/exceptions/OutputControllerQueueOverflowException.java @@ -0,0 +1,14 @@ +package server.exceptions; + +/** + * Created by alilotfi on 1/23/15. + */ +public class OutputControllerQueueOverflowException extends RuntimeException { + public OutputControllerQueueOverflowException() { + super(); + } + + public OutputControllerQueueOverflowException(String s) { + super(s); + } +} diff --git a/src/server/network/ClientHandler.java b/src/server/network/ClientHandler.java new file mode 100644 index 0000000..ea23df1 --- /dev/null +++ b/src/server/network/ClientHandler.java @@ -0,0 +1,250 @@ +package server.network; + +import server.network.data.Message; +import util.Log; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.Supplier; + +/** + * {@link server.network.ClientHandler} handles a client, i.e. it is responsible + * for sending/receiving messages to/from the client. + *

+ * After that a client is verified by the server, server assigns a + * {@link server.network.ClientHandler} to that client. + *

+ * Messages should arrive in a predefined interval of time (which is determined + * by the server). Every message arrived out of this interval is discarded + * immediately. If two or more messages arrived in the valid time the last one + * is stored as the "last validated message". + */ +public class ClientHandler { + + /** + * Logging tag. + */ + private static String TAG = "ClientHandler"; + + /** + * Socket of the client. + */ + private JsonSocket client; + + /** + * Lock for {@link #client}. + */ + private final Object clientLock; + + /** + * Termination flag. + */ + private boolean terminateFlag; + + /** + * Last valid message which is arrived on time. + */ + private Message lastValidatedMessage; + + /** + * Last message received from client. + */ + private Message lastReceivedMessage; + + /** + * This object is notified when a message is received. + */ + private final Object messageNotifier; + + /** + * Message queue. These messages will be sent to the client asap. + */ + private final LinkedBlockingDeque messagesToSend; + + + /** + * Constructor. + */ + public ClientHandler() { + messagesToSend = new LinkedBlockingDeque<>(); + clientLock = new Object(); + messageNotifier = new Object(); + } + + /** + * Queues a message for the client. Message is not sent until {@link #send} + * is called. + * + * @param msg message to send. + */ + public void queue(Message msg) { + messagesToSend.add(msg); + } + + /** + * Sends last queued message. + */ + public void send() { + try { + client.send(messagesToSend.remove()); + } catch (NoSuchElementException e) { + Log.i(TAG, "no message is queued for this client", e); + } catch (Exception e) { + Log.i(TAG, "message sending failure", e); + } + } + + /** + * Removes last validated message. + */ + public void clearLastValidatedMessage() { + lastValidatedMessage = null; + } + + /** + * A message is valid if it is arrived in a valid time, which is determined + * by the server. + * + * @return last validated message. + * @see #getReceiver + */ + public Message getLastValidatedMessage() { + return lastValidatedMessage; + } + + /** + * Binds handler to a socket, i.e. this handler is responsible for + * sending/receiving messages to/from the socket. + * + * @param socket client + */ + public void bind(JsonSocket socket) { + try { + if (client != null) + client.close(); + } catch (IOException e) { + Log.i(TAG, "socket closing failure", e); + } finally { + synchronized (clientLock) { + client = socket; + clientLock.notifyAll(); + } + } + } + + /** + * The result of method is a {@link java.lang.Runnable} object. When this + * runnable is called it receives a new message from the client and if it + * arrives in a valid time (which is checked using timeValidator) + * stores it in {@link #lastValidatedMessage}. + * + * @param timeValidator get method of this object returns + * true if and only if it is called in a valid time. + * (valid time is the time when messages can be + * arrived from clients, e.g. half a second after + * each turn) + * @return a runnable which is used by server to receive new messages of client + */ + public Runnable getReceiver(Supplier timeValidator) { + return () -> { + while (!terminateFlag) { + try { + waitForClient(); + receive(); + if (timeValidator.get()) + lastValidatedMessage = lastReceivedMessage; + } catch (InterruptedException e) { + Log.i(TAG, "waiting for client interrupted", e); + } catch (IOException e) { + Log.i(TAG, "message receiving failure", e); + } + } + }; + } + + /** + * Receives a message from client. + * + * @throws IOException if an I/O error occurs. + */ + private void receive() throws IOException { + lastReceivedMessage = client.get(Message.class); + synchronized (messageNotifier) { + messageNotifier.notifyAll(); + } + } + + /** + * Returns true if any client is connected to this client handler. + * + * @return true if any client is connected to this client handler. + */ + public boolean isConnected() { + return client != null; + } + + /** + * Blocks caller method until the client send a message. + * + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForClientMessage() throws InterruptedException { + synchronized (messageNotifier) { + messageNotifier.wait(); + } + } + + /** + * Blocks caller method at most timeout milliseconds until + * the client send a message. + * + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForClientMessage(long timeout) throws InterruptedException { + synchronized (messageNotifier) { + messageNotifier.wait(timeout); + } + } + + /** + * Blocks caller method until a client is connected to this handler. + * + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForClient() throws InterruptedException { + if (client == null || client.isClosed()) + synchronized (clientLock) { + clientLock.wait(); + } + } + + /** + * Blocks caller method at most timeout milliseconds until a + * client is connected to this handler. + * + * @param timeout timeout in milliseconds + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForClient(long timeout) throws InterruptedException { + if (client == null || client.isClosed()) + synchronized (clientLock) { + clientLock.wait(timeout); + } + } + + /** + * Terminates operations of the handler. It actually closes the socket and + * changes a flag. + */ + public void terminate() { + try { + terminateFlag = true; + if (client != null) + client.close(); + } catch (IOException e) { + Log.i(TAG, "Socket closing failure.", e); + } + } + +} diff --git a/src/server/network/ClientNetwork.java b/src/server/network/ClientNetwork.java index 00b8687..f33e121 100644 --- a/src/server/network/ClientNetwork.java +++ b/src/server/network/ClientNetwork.java @@ -1,7 +1,377 @@ package server.network; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import server.core.model.Event; +import server.network.data.Message; +import util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.*; + /** - * Created by Razi on 12/6/2014. + * {@link server.network.ClientNetwork} is a server which is responsible for + * sending messages from server to players + * and receiving players' requests (messages). + *

+ * First of all clients (players) must be defined to the server via method + * {@link #defineClient}. Server assigns an ID to each client so further calls + * get ID of the client instead of its token. + *

+ * When a client (player) is connected to the server, it sends a token and waits + * for the initial message which contains necessary data of beginning of the + * game. + *

+ * Each turn server sends some data to the clients, these data are first queued + * by {@link #queue} and then sent by send method. Server does it best to send + * messages simultaneously to clients, i.e. sending procedure is fair. + *

+ * The communications are one sided, i.e. everything which is sent by client is + * ignored by the server. */ -public class ClientNetwork { +public class ClientNetwork extends NetServer { + + /** + * Logging tag. + */ + private static String TAG = "ClientNetwork"; + + /** + * Indicates that receive time is valid or not. + */ + private volatile boolean receiveTimeFlag; + + /** + * Tokens of clients. + */ + private HashMap mTokens; + + /** + * Client handlers. + */ + private ArrayList mClients; + + /** + * A thread pool used to send all messages. + */ + private ExecutorService sendExecutor; + + /** + * A thread pool used to receive messages from clients. + */ + private ExecutorService receiveExecutor; + + /** + * A thread pool used to accept and verify clients. + */ + private ExecutorService acceptExecutor; + + /** + * Gson used to extract event from a message. + */ + private Gson gson; + + /** + * Constructor. + */ + public ClientNetwork() { + gson = new Gson(); + mTokens = new HashMap<>(); + mClients = new ArrayList<>(); + sendExecutor = Executors.newCachedThreadPool(); + receiveExecutor = Executors.newCachedThreadPool(); + } + + /** + * Defines a client with a token. It actually assigns an ID and a handler to + * the client. + * + * @param token token of the client + * @return ID of the client + * @see {@link #omitAllClients} + */ + public int defineClient(String token) { + if (!isTerminated()) + throw new RuntimeException("ClientNetwork: " + + "server is not terminated when defineClient() is called."); + if (mTokens.containsKey(token)) + throw new RuntimeException("ClientNetwork: " + + "duplicate token. " + token); + int id = mClients.size(); + mTokens.put(token, id); + mClients.add(newClient()); + return id; + } + + /** + * Creates a new handler. + * + * @return new handler + */ + private ClientHandler newClient() { + ClientHandler client = new ClientHandler(); + Runnable receiver = client.getReceiver(() -> receiveTimeFlag); + receiveExecutor.submit(receiver); + return client; + } + + /** + * Remove defined clients and free memory allocated for them. + * + * @see {@link #defineClient} + */ + public void omitAllClients() { + if (!isTerminated()) + throw new RuntimeException("ClientNetwork: " + + "server is not terminated when omitAllClients() is called."); + mClients.forEach(server.network.ClientHandler::terminate); + mTokens.clear(); + mClients.clear(); + sendExecutor.shutdownNow(); + receiveExecutor.shutdownNow(); + System.gc(); + mTokens = new HashMap<>(); + mClients = new ArrayList<>(); + sendExecutor = Executors.newCachedThreadPool(); + receiveExecutor = Executors.newCachedThreadPool(); + } + + /** + * Queues a message for a client. + * + * @param clientID ID of the client + * @param msg message + * @see {@link #defineClient} + * @see {@link #sendAllBlocking} + */ + public void queue(int clientID, Message msg) { + mClients.get(clientID).queue(msg); + } + + /** + * Sends all queued messages. Method will not return until all messages + * sent. (or some failure occur) + * + * @see {@link #queue} + */ + public void sendAllBlocking() { + CyclicBarrier sendBarrier = new CyclicBarrier(mClients.size()+1); + for (ClientHandler client : mClients) { + sendExecutor.submit(() -> { + try { + sendBarrier.await(); + client.send(); + sendBarrier.await(); + } catch (InterruptedException | BrokenBarrierException e) { + Log.d(TAG, "waiting barrier interrupted.", e); + } + }); + } + try { + sendBarrier.await(); // start sending + sendBarrier.await(); // wait to send + } catch (Exception e) { + Log.d(TAG, "waiting barrier interrupted.", e); + } + } + + /** + * Stops receiving messages from clients. Any message which is arrived after + * a call of this method is discarded by the handler. + * + * @see {@link #startReceivingAll} + */ + public void stopReceivingAll() { + receiveTimeFlag = false; + } + + /** + * Starts receiving messages from clients. The last message which is arrived + * after a call of this method and before the corresponding call of + * {@link #stopReceivingAll} is stored as "last valid message". + * + * @see {@link #stopReceivingAll} + */ + public void startReceivingAll() { + mClients.forEach(server.network.ClientHandler::clearLastValidatedMessage); + receiveTimeFlag = true; + } + + /** + * Returns last valid message which is received from a client. + * + * @param clientID ID of the client + * @return last valid message or null if there is no valid msg + * @see {@link #defineClient} + */ + public Message getReceivedMessage(int clientID) { + return mClients.get(clientID).getLastValidatedMessage(); + } + + /** + * Returns last valid event which is received from a client. + * + * @param clientID ID of the client + * @return last valid event or null if there is no valid event + * @see {@link #getReceivedMessage} + */ + public Event[] getReceivedEvent(int clientID) { + Message msg = getReceivedMessage(clientID); + Event[] events = null; + try { + JsonArray eventArray = ((JsonElement)msg.args[0]).getAsJsonArray(); + events = new Event[eventArray.size()]; + for (int i = 0; i < events.length; i++) + events[i] = gson.fromJson(eventArray.get(i), Event.class); + } catch (Exception e) { + Log.i(TAG, "Error getting received messages.", e); + } + return events; + } + + @Override + protected void accept(JsonSocket client) { + acceptExecutor.submit(() -> { + try { + verifyClient(client); + } catch (Exception e) { + // if anything was wrong close the client! + Log.i(TAG, "client rejected", e); + try { + client.close(); + } catch (Exception ignored) {} + } + }); + } + + private void verifyClient(JsonSocket client) throws Exception { + // get the token, timeout is 10 seconds + Future futureMessage + = acceptExecutor.submit(() -> client.get(Message.class)); + Message token = futureMessage.get(10, TimeUnit.SECONDS); + // check the token + if (!"token".equals(token.name)) + throw new Exception("ClientNetwork: Client rejected, " + + "first message is not token."); + String clientToken = (String) token.args[0]; + // ID of client + int clientID = mTokens.get(clientToken); + // bind socket to corresponding client handler + mClients.get(clientID).bind(client); + } + + /** + * Blocks caller method until the specified client send a message. + * + * @param clientID ID of the client + * @throws InterruptedException if current thread is interrupted. + * @see {@link #defineClient} + */ + public void waitForClientMessage(int clientID) throws InterruptedException { + mClients.get(clientID).waitForClientMessage(); + } + + /** + * Blocks caller method at most timeout milliseconds until + * the specified client send a message. + * + * @param clientID ID of the client + * @throws InterruptedException if current thread is interrupted. + * @see {@link #defineClient} + */ + public void waitForClientMessage(int clientID, long timeout) throws InterruptedException { + mClients.get(clientID).waitForClientMessage(timeout); + } + + /** + * Blocks caller method until the specified client is connected to the server. + * + * @param clientID ID of the client + * @throws InterruptedException if current thread is interrupted. + * @see {@link #defineClient} + */ + public void waitForClient(int clientID) throws InterruptedException { + mClients.get(clientID).waitForClient(); + } + + /** + * Blocks caller method at most timeout milliseconds until + * the specified client is connected to the server. + * + * @param clientID ID of the client + * @param timeout timeout in milliseconds + * @throws InterruptedException if current thread is interrupted. + * @see {@link #defineClient} + */ + public void waitForClient(int clientID, long timeout) throws InterruptedException { + mClients.get(clientID).waitForClient(timeout); + } + + /** + * Blocks caller method until all clients are connected to the server. + * + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForAllClients() throws InterruptedException { + for (ClientHandler client : mClients) + client.waitForClient(); + } + + /** + * Blocks caller method at most timeout milliseconds until all + * clients are connected to the server. + * + * @param timeout timeout in milliseconds + * @throws InterruptedException if current thread is interrupted. + */ + public void waitForAllClients(long timeout) throws InterruptedException { + for (ClientHandler client : mClients) { + long start = System.currentTimeMillis(); + client.waitForClient(timeout); + long end = System.currentTimeMillis(); + timeout -= end - start; + if (timeout <= 0) + return; + } + } + + /** + * Returns number of connected clients. + * + * @return num of connected clients. + */ + public int getNumberOfConnected() { + int noc = 0; + for (ClientHandler client : mClients) + if (client.isConnected()) + noc++; + return noc; + } + + /** + * States that a client is connected or not. + * + * @param clientID ID of the client + * @return true if the client is connected + * @see {@link #defineClient} + */ + public boolean isConnected(int clientID) { + return mClients.get(clientID).isConnected(); + } + + @Override + public void listen(int port) { + super.listen(port); + acceptExecutor = Executors.newCachedThreadPool(); + } + + @Override + public void terminate() { + super.terminate(); + acceptExecutor.shutdownNow(); + } + } diff --git a/src/server/network/JsonSocket.java b/src/server/network/JsonSocket.java new file mode 100644 index 0000000..b946a05 --- /dev/null +++ b/src/server/network/JsonSocket.java @@ -0,0 +1,162 @@ +package server.network; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.io.*; +import java.net.Socket; + +/** + * This class is a wrapper for java's Socket that uses json strings + * to transmit objects. + *

+ * A JsonSocket holds a Gson object to convert + * objects to json string and vice-versa, and a Socket to + * send and receive json strings. + *

+ * To create a JsonSocket, one can pass an existing socket + * to the constructor of this class. + *

+ * After a JsonSocket was successfully created, one can send and + * receive objects with methods send and get. + *

+ * Note that the other endpoint of the communication must use a + * JsonSocket to send and receive objects correctly. + *

+ * This class is a member of {@link server.network}. + * + * @see com.google.gson.Gson + * @see java.net.Socket + * + */ +public class JsonSocket { + + /** + * The Gson object is used to convert between java and json. + */ + private Gson mGson; + /** + * The underlying Socket. + */ + private Socket mSocket; + /** + * Input stream of the socket. + */ + private InputStream mIn; + /** + * Output stream of the socket. + */ + private OutputStream mOut; + + /** + * Initiates new socket with specified host and port and uses + * that socket to transmit json strings. + * + * @param host the host name. + * @param port the port number. + * @throws IOException if an I/O error occurs when creating the socket or + * its input or output stream. + * @see java.net.Socket#Socket(java.lang.String, int) + * @see #JsonSocket(java.net.Socket) + */ + public JsonSocket(String host, int port) throws IOException { + this(new Socket(host, port)); + } + + /** + * Uses an existing socket to transmit json strings. + * + * @param socket the socket object. + * @throws IOException if an I/O error occurs when creating the + * input or output stream of the socket. + */ + public JsonSocket(Socket socket) throws IOException { + mGson = new Gson(); + mSocket = socket; + mIn = mSocket.getInputStream(); + mOut = mSocket.getOutputStream(); + } + + /** + * Closes the underlying socket. + * + * @throws IOException if an I/O error occurs when closing the socket. + * @see java.net.Socket#close + * @see #isClosed + */ + public void close() throws IOException { + mSocket.close(); + } + + /** + * Returns the closed state of the socket. + * + * @return true if the socket has been closed + * @see java.net.Socket#isClosed + * @see #close + */ + public boolean isClosed() { + return mSocket.isClosed(); + } + + /** + * Converts obj to json string and sends it via the socket's + * output stream. + * + * @param obj the object to send. + * @throws IOException if an I/O error occurs, e.g. when the socket is closed. + * @see com.google.gson.Gson#toJson(java.lang.Object) + */ + public void send(Object obj) throws IOException { + String json = mGson.toJson(obj); + byte buffer[] = json.getBytes(); + mOut.write(buffer, 0, buffer.length); + mOut.write('\0'); + } + + /** + * Reads a json string from socket's input stream, and converts it to + * a JsonObject. + * + * @return the received JsonObject + * @throws IOException if an I/O error occurs, e.g. when the socket is closed. + * @see com.google.gson.Gson#fromJson(String,java.lang.Class) + * @see #get(java.lang.Class) + */ + public JsonObject get() throws IOException { + return get(JsonObject.class); + } + + /** + * Reads a json string from socket's input stream, and converts it to + * a an object of the specified class. + * + * @param classOfInput object's class + * @param type + * @return the received object + * @throws IOException if an I/O error occurs, e.g. when the socket is closed. + * @see com.google.gson.Gson#fromJson(String,java.lang.Class) + * @see #get + */ + public T get(Class classOfInput) throws IOException { + int length = 1000, total = 0, current; + byte buffer[] = new byte[length]; + while (true) { + current = mIn.read(); + if (current == -1) + throw new IOException("EOF reached."); + if (current == '\0') + break; + if (total >= length) { + byte newBuffer[] = new byte[2*length]; + System.arraycopy(buffer, 0, newBuffer, 0, length); + buffer = newBuffer; + length *= 2; + } + buffer[total++] = (byte) current; + } + String json = new String(buffer, 0, total); + return mGson.fromJson(json, classOfInput); + } + +} diff --git a/src/server/network/NetServer.java b/src/server/network/NetServer.java new file mode 100644 index 0000000..8e9b41c --- /dev/null +++ b/src/server/network/NetServer.java @@ -0,0 +1,93 @@ +package server.network; + +/** + * This class implements a network server. A NetServer runs a + * server on the specified port and waits for the clients to be connected, + * and then based on the implementation of the accept method, + * performs some operation for each client, + * e.g. performs some operation based on the request, + * returns some information to the requester, + * or offers a terminal for the client. + *

+ * When the method {@link #listen} is called, a + * {@link server.network.NetServerThread} is assigned to listen on the specified + * port. When a client attempts to connect to that port, the {@link #accept} + * method of the class is called, with a reference to the + * JsonSocket which is connected to the client. This is the initial + * point of the connection and one can send initial messages to start + * communication with the client. + *

+ * It is better to do all operations for a client in a separated thread. So if + * the client is fake it doesn't affect the behavior of the server. + * + * @see server.network.NetServerThread + * @see server.network.TerminalNetwork + * @see server.network.ClientNetwork + * @see server.network.UINetwork + */ +public abstract class NetServer { + + /** + * The thread which is assigned to listen on the port. + */ + private NetServerThread listener; + + /** + * A thread is created and assigned to listen on the specified port. + * {@link #terminate} must be called between two consecutive calls of this + * function. + * + * @param port server port + * @see #terminate + * @see server.network.NetServerThread + */ + public synchronized void listen(int port) { + if (listener != null) + throw new IllegalStateException("NetServer is currently listening."); + listener = new NetServerThread(port, this::accept); + listener.start(); + } + + /** + * Terminates operations of the current listener. {@link #listen} must be + * called between two consecutive calls of this function. + * + * @see #listen + * @see server.network.NetServerThread + */ + public synchronized void terminate() { + if (listener == null) + throw new IllegalStateException("NetServer is not started or has been terminated."); + listener.terminate(); + listener = null; + } + + /** + * Returns the state of the server. + * + * @return true if listening is terminated. + */ + public boolean isTerminated() { + return listener == null; + } + + /** + * A method to accept new clients. When a client connects to the port of + * this NetServer, this method is called, with a reference to + * a {@link server.network.JsonSocket} which is connected to the client side + * socket. A new thread may be assigned to handle requests of the client. + *

+ * This method is abstract and so it is not implemented in this class. + * Each server has a different behavior on accepting new clients, e.g. + * it may take a token from client or send some information at initial point. + * To implement a server, one can extend this class and implement this method. + *

+ * Note that this method must take care of everything that could be happen + * during communications between client and server. + * + * @param client a {@link server.network.JsonSocket} which is connected + * to the client's socket. + */ + protected abstract void accept(JsonSocket client); + +} diff --git a/src/server/network/NetServerThread.java b/src/server/network/NetServerThread.java new file mode 100644 index 0000000..7e906f5 --- /dev/null +++ b/src/server/network/NetServerThread.java @@ -0,0 +1,114 @@ +package server.network; + +import util.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.function.Consumer; + +/** + * This thread is used by {@link server.network.NetServer} to listen on + * some port for the new clients. + *

+ * The actual work of the {@link server.network.NetServerThread} is done by a + * {@link java.net.ServerSocket}. When a NetServerThread is ran, + * it creates a new server socket to listen on that port. When a new client is + * connected to that port, it passes the client's + * {@link server.network.JsonSocket} to the specified consumer. + *

+ * The consumer should accept the client and perform necessary operations. + *

+ * In the {@link server.network.NetServer} the consumer is the abstract method + * {@link server.network.NetServer#accept}. + *

+ * When the server socket is disconnected from the port, e.g. when it is closed + * due to an unknown reason, a new server socket will be created. + * So it is guaranteed that server is always listening on the port. + * + * @see server.network.NetServer + * @see java.net.ServerSocket + */ +public class NetServerThread extends Thread { + + /** + * Logging tag. + */ + private static String TAG = "NetServerThread"; + + /** + * The port to listen on. + */ + private final int port; + /** + * Terminate flag! + */ + private boolean terminateFlag; + /** + * The underlying server socket which actually listens on the port. + */ + private ServerSocket serverSocket; + /** + * This consumer specifies the behavior of the server when a new client is + * connected. + * Actually, clientAcceptor.accept(JsonSocket) is called when + * a client is connected to the port. + */ + private Consumer clientAcceptor; + + /** + * Constructor. + * @param port the server port + * @param clientAcceptor specifies accept behavior of the server + */ + public NetServerThread(int port, Consumer clientAcceptor) { + this.port = port; + this.clientAcceptor = clientAcceptor; + } + + /** + * Runs the server until the terminateFlag is set to + * true. + * + * @see #runServer + * @see #terminate + */ + @Override + public void run() { + while (!terminateFlag) + try { + runServer(); + } catch (Exception e) { + Log.i(TAG, "Server socket closed", e); + } + } + + /** + * Runs the server and handles new clients. The method can not be completed + * without throwing an exception. + * + * @throws IOException if an I/O error occurs, + * e.g. when the server socket is closed. + * @see #terminate + */ + private void runServer() throws IOException { + if (serverSocket != null && !serverSocket.isClosed()) + serverSocket.close(); + serverSocket = new ServerSocket(port); + while (!terminateFlag) + clientAcceptor.accept(new JsonSocket(serverSocket.accept())); + } + + /** + * Terminates operations of the current thread. Sets the terminate flag + * to true and closes the server socket. + */ + public synchronized void terminate() { + terminateFlag = true; + try { + serverSocket.close(); + } catch (Exception e) { + Log.i(TAG, "Socket closing failure.", e); + } + } + +} diff --git a/src/server/network/TerminalNetwork.java b/src/server/network/TerminalNetwork.java index 015299d..1c78c91 100644 --- a/src/server/network/TerminalNetwork.java +++ b/src/server/network/TerminalNetwork.java @@ -1,7 +1,161 @@ package server.network; +import server.core.model.Event; +import server.network.data.Message; + +import java.io.IOException; +import java.util.ArrayList; + /** - * Created by Razi on 12/6/2014. + * This class is responsible for create and handling the communication between users and the Terminal + *

+ * A new object from this class will be created when a Terminal wants to start. + *

+ * All of the required efforts for connecting a client to that server is done by the parent class called {@link server.network.NetServer} + * so in this class we just need to handle the events that are occur after user successfully connects with + * + * @see server.network.TerminalNetwork.TerminalNetworkThread + * @see server.network.NetServer */ -public class TerminalNetwork { +public class TerminalNetwork extends NetServer { + + public static final int MAX_RECEIVE_EXCEPTIONS = 20; + + /** + * used to identify the users to connect to Terminal. + */ + private String token; + + /** + * This interface is used for transfer Commands and Events. + * This interface is implemented in {@link server.core.GameHandler}. + * TerminalInterface has two methods called putEvent and runCommand + * that will be used for sending essential data to {@link server.core.GameHandler}. + */ + public interface TerminalInterface { + void putEvent(Event event); + Message runCommand(Message command); + } + + /** + * An instance of {@link server.network.TerminalNetwork} used to + */ + private TerminalInterface handler; + + @Override + protected void accept(JsonSocket client) { + new TerminalNetworkThread(client).start(); + } + + public TerminalNetwork(String token) { + this.token = token; + } + + public void setHandler(TerminalInterface handler) { + this.handler = handler; + } + + /** + * This thread is used by {@link server.network.TerminalNetwork} + * to handle multiple clients that want to start a terminal. + *

+ * {@link server.network.TerminalNetwork.TerminalNetworkThread} uses a {@link server.network.JsonSocket} + * to get the inputs of a client. The first input is a Token. + * If user enter a correct Token then terminal actually starts. + *

+ * Then it converts every input to a {@link server.network.data.Message} class + * and if the input was a valid one it checks whether this input is command or event. + * Then it creates the appropriate object and by means of {@link server.network.TerminalNetwork.TerminalInterface} + * sends it to {@link server.core.model.Event}. + * + * @see server.network.NetServer + * @see server.network.JsonSocket + */ + class TerminalNetworkThread extends Thread { + + /** + * Use for get the user input. + */ + private JsonSocket client; + + /** + * For making an infinite loop. there's no need to make it false because when the terminal actually + * terminated, the thread will be stop. + */ + private boolean userHasCommand = true; + + /** + * Constructor. + * @param client use to get the input from user + */ + public TerminalNetworkThread (JsonSocket client) { + this.client = client; + } + + /** + * Check tokens and analyze the inputs of the user. + * + * @see #TerminalNetworkThread(JsonSocket) + * @see #checkInput() + */ + @Override + public void run() { + try { + Message msg = client.get(Message.class); + + if (!msg.name.equals("token") || msg.args == null || msg.args.length < 1 || !msg.args[0].equals(token)) { + client.close(); + return; + } + Object [] args = new Object[1]; + args[0] = new ArrayList(); + client.send(new Message("init", args)); + + int exceptions = 0; + + // now the user is a valid one + while (userHasCommand) { + try { + checkInput(); + } catch (Exception e) { + exceptions++; + if (exceptions > MAX_RECEIVE_EXCEPTIONS) + return; + e.printStackTrace(); + } + } + + } catch (Exception e) { + e.printStackTrace(); + try { + client.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + + /** + * If input was not a command or an event then this method throws an IOException + * @throws IOException if the input is invalid + */ + public void checkInput() throws IOException { + Message msg = client.get(Message.class); + switch (msg.name) { + case "command": + Message command = new Message((String)msg.args[0], ((ArrayList)msg.args[1]).toArray()); + Message report = handler.runCommand(command); + client.send(report); + break; + case Event.EVENT: + handler.putEvent((Event) msg.args[0]); + break; + default: + client.send("Your input is invalid!\nPlease don't hack me :)"); + break; + } + } + + } + } diff --git a/src/server/network/UINetwork.java b/src/server/network/UINetwork.java index f498950..01d9e99 100644 --- a/src/server/network/UINetwork.java +++ b/src/server/network/UINetwork.java @@ -1,7 +1,250 @@ package server.network; +import server.network.data.Message; +import util.Log; + +import java.io.IOException; +import java.util.concurrent.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + /** - * Created by Razi on 12/6/2014. + * {@link server.network.UINetwork} is a server which is responsible for sending + * UI data to the node.js client. + *

+ * When a client is connected to the server, it sends a token and waits for the + * initial message which contains necessary data of beginning of the game. + *

+ * Messages are sent using methods {@link #sendBlocking} or {@link #sendNonBlocking}. + *

+ * The communications are one sided, i.e. everything which is sent by client is + * ignored by the server. */ -public class UINetwork { -} +public final class UINetwork extends NetServer { + + /** + * Logging tag + */ + private static String TAG = "UINetwork"; + + /** + * token of the server + */ + private final String mToken; + + /** + * current client of the server + */ + private JsonSocket mClient; + + /** + * Lock for {@link #mClient} + */ + private final Lock mClientLock; + + /** + * Notifies waiters when a new client is connected. + */ + private final Object clientNotifier; + + /** + * thread executor which is used to accept clients + */ + private ExecutorService executor; + + /** + * Thread executor which is used to send messages. + */ + private ExecutorService sendExecutor; + + /** + * Initializes the class and starts sending messages to clients. + * If there is no client at the time of sending, the message will be + * thrown away. + * + * @param token token of the server + * @see #sendBlocking + * @see #sendNonBlocking + * @see #hasClient + * @see #waitForClient + * @see #waitForNewClient + */ + public UINetwork(String token) { + mToken = token; + clientNotifier = new Object(); + mClientLock = new ReentrantLock(true); + } + + /** + * Sends a message to the client. + * Caller method will be blocked until the message is sent. + * + * @param msg message to send + */ + public void sendBlocking(Message msg) { + try { + mClient.send(msg); + } catch (IOException e) { + Log.d(TAG, "Message sending failure.", e); + } + } + + /** + * Sends a message to the client. + * Caller method wont be blocked. + * + * @param msg message to send + * @see #sendBlocking + */ + public void sendNonBlocking(Message msg) { + sendExecutor.submit(() -> sendBlocking(msg)); + } + + /** + * Creates a new thread to verify the client by taking a token. + * + * @param client a {@link server.network.JsonSocket} which is connected + * @see server.network.NetServer#accept + */ + @Override + protected void accept(JsonSocket client) { + executor.submit(() -> { + try { + verifyClient(client); + } catch (Exception e) { + // if anything was wrong close the client! + Log.i(TAG, "Client rejected.", e); + try { + client.close(); + } catch (Exception ignored) {} + } + }); + } + + /** + * Verifies the client by taking a token. + * + * @param client client + * @throws Exception if verification is failed + * @see #accept + */ + private void verifyClient(JsonSocket client) throws Exception { + // get the token, timeout is 10 seconds + Future futureMessage + = executor.submit(() -> client.get(Message.class)); + Message token = futureMessage.get(10, TimeUnit.SECONDS); + // check the token + if (!"token".equals(token.name) || !mToken.equals(token.args[0])) + throw new Exception("UINetwork: Client rejected, " + + "token is not correct."); + // so the token is correct! + changeClient(client); + } + + /** + * Changes current client to the specified client. + * It actually closes the previous client (if exists) and then creates a + * thread for the new one. + * + * @param client new client + * @see #verifyClient + */ + private void changeClient(JsonSocket client) { + mClientLock.lock(); + try { + // close previous socket + if (mClient != null) + mClient.close(); + } catch (Exception e) { + Log.i(TAG, "Socket closing failure.", e); + } finally { + // change the client + mClient = client; + // notify waiting threads + synchronized (clientNotifier) { + clientNotifier.notifyAll(); + } + mClientLock.unlock(); + } + } + + @Override + public synchronized void listen(int port) { + executor = Executors.newCachedThreadPool(); + sendExecutor = Executors.newSingleThreadExecutor(); + super.listen(port); + } + + @Override + public synchronized void terminate() { + super.terminate(); + executor.shutdown(); + executor = null; + sendExecutor.shutdown(); + sendExecutor = null; + } + + /** + * Returns true if any client is connected and verified by the server. + * + * @return true if there is any clients + */ + public boolean hasClient() { + return mClient != null; + } + + /** + * Caller will be blocked until a client is connected. + * If currently a client is connected, returns without waiting. + * + * @throws InterruptedException if the current thread is interrupted. + */ + public void waitForClient() throws InterruptedException { + synchronized (clientNotifier) { + if (hasClient()) + return; + clientNotifier.wait(); + } + } + + /** + * Caller will be blocked until a client is connected or the + * timeout is reached. + * If currently a client is connected, returns without waiting. + * + * @param timeout timeout in seconds + * @throws InterruptedException if the current thread is interrupted. + */ + public void waitForClient(long timeout) throws InterruptedException { + synchronized (clientNotifier) { + if (hasClient()) + return; + clientNotifier.wait(timeout); + } + } + + /** + * Caller will be blocked until a new client is connected. + * + * @throws InterruptedException if the current thread is interrupted. + */ + public void waitForNewClient() throws InterruptedException { + synchronized (clientNotifier) { + clientNotifier.wait(); + } + } + + /** + * Caller will be blocked until a new client is connected or the + * timeout is reached. + * + * @param timeout timeout in seconds + * @throws InterruptedException if the current thread is interrupted. + */ + public void waitForNewClient(long timeout) throws InterruptedException { + synchronized (clientNotifier) { + clientNotifier.wait(timeout); + } + } + +} \ No newline at end of file diff --git a/src/server/network/data/Command.java b/src/server/network/data/Command.java new file mode 100644 index 0000000..9faec15 --- /dev/null +++ b/src/server/network/data/Command.java @@ -0,0 +1,26 @@ +package server.network.data; + +import com.google.gson.JsonObject; + +/** + * Created by rajabzz on 12/22/14. + */ +public class Command { + public static final String COMMAND = "command"; + + public static final int COMMAND_TYPE_HELP = 0; + public static final int COMMAND_TYPE_STATUS = 1; + public static final int COMMAND_TYPE_NEW_GAME = 2; + public static final int COMMAND_TYPE_LOAD_GAME = 3; + public static final int COMMAND_TYPE_PAUSE_GAME = 4; + public static final int COMMAND_TYPE_RESUME_GAME = 5; + public static final int COMMAND_TYPE_END_GAME = 6; + public static final int COMMAND_TYPE_CONNECT = 7; + public static final int COMMAND_TYPE_DISCONNECT = 8; + public static final int COMMAND_TYPE_EXIT = 9; + + + public String cmdType; + public String[] args; + +} diff --git a/src/server/network/data/CommandReport.java b/src/server/network/data/CommandReport.java new file mode 100644 index 0000000..6858801 --- /dev/null +++ b/src/server/network/data/CommandReport.java @@ -0,0 +1,10 @@ +package server.network.data; + +/** + * Created by rajabzz on 12/22/14. + */ +public class CommandReport { + public static final String REPORT = "report"; + + public Object[] args; +} diff --git a/src/server/network/data/Message.java b/src/server/network/data/Message.java new file mode 100644 index 0000000..552f02e --- /dev/null +++ b/src/server/network/data/Message.java @@ -0,0 +1,24 @@ +package server.network.data; + +/** + * Created by rajabzz on 12/27/14. + */ +public class Message { + public String name; + public Object[] args; + + public Message(){} + + public Message(String name, Object[] args) { + this.name = name; + this.args = args; + } + + public void setName(String name) { + this.name = name; + } + + public void setArgs(Object[] args) { + this.args = args; + } +} diff --git a/src/test/server/network/ClientNetworkTest.java b/src/test/server/network/ClientNetworkTest.java new file mode 100644 index 0000000..de51408 --- /dev/null +++ b/src/test/server/network/ClientNetworkTest.java @@ -0,0 +1,250 @@ +package test.server.network; + +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import server.network.ClientNetwork; +import server.network.JsonSocket; +import server.network.data.Message; + +import java.net.SocketException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class ClientNetworkTest { + + public static final int port = 12346; + public static final String host = "localhost"; + private ClientNetwork clientNetwork; + + @Before + public void createClientNetwork() throws Exception { + // create server before each test + clientNetwork = new ClientNetwork(); + } + + @After + public void terminateClientNetwork() throws Exception { + // terminate server after each test + clientNetwork.terminate(); + clientNetwork.omitAllClients(); + // we should wait for completion of the test (results may appear after a delay) + Thread.sleep(500); + } + + @Test(timeout = 5000) + public void test1_single_client_wrong_token() throws Exception { + String token = "123456"; + // define client + int id = clientNetwork.defineClient(token); + // run server + clientNetwork.listen(port); + // create client + JsonSocket client = new JsonSocket(host, port); + // send wrong token (token must be an instance of Message, not a string) + client.send(token); + // wait for the server to check the token + clientNetwork.waitForClient(id, 1000); + // client must be rejected by the server + assertEquals(clientNetwork.isConnected(id), false); + // client must be closed due to incorrect token + try { + client.send("nothing!"); + fail(); + } catch (SocketException ignored) { + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test(timeout = 1000) + public void test2_single_client_correct_token() throws Exception { + String token = "123456"; + // define client + int id = clientNetwork.defineClient(token); + // run server + clientNetwork.listen(port); + // create client + JsonSocket client = new JsonSocket(host, port); + // send correct token + client.send(new Message("token", new Object[]{token})); + // wait for the server to check the token + clientNetwork.waitForClient(id); + // token is correct, so client must be alive + client.send(new Message("nothing!", null)); + } + + @Test(timeout = 1000) + public void test3_send_message_100_clients() throws Exception { + int n = 100; + int ids[] = new int[n]; + String tokens[] = new String[n]; + JsonSocket clients[] = new JsonSocket[n]; + // define clients + for (int i = 0; i < n; i++) { + tokens[i] = "random double: " + Math.random(); + ids[i] = clientNetwork.defineClient(tokens[i]); + } + // run server + clientNetwork.listen(port); + // create clients and send tokens + for (int i = 0; i < n; i++) { + clients[i] = new JsonSocket(host, port); + clients[i].send(new Message("token", new Object[]{tokens[i]})); + } + // wait for the server to handle incoming messages + clientNetwork.waitForAllClients(); + + long queueTime = System.currentTimeMillis(); + // these random numbers will be sent to clients + double randoms[] = new double[n]; + for (int i = 0; i < n; i++) { + randoms[i] = Math.random(); + clientNetwork.queue(ids[i], new Message("test message", new Object[]{"arg0", randoms[i]})); + } + queueTime = System.currentTimeMillis() - queueTime; + long sendTime = System.currentTimeMillis(); + clientNetwork.sendAllBlocking(); + sendTime = System.currentTimeMillis() - sendTime; + // check received messages + for (int i = 0; i < n; i++) { + // get message + Message msg = clients[i].get(Message.class); + // check data of received message + assertEquals(msg.name, "test message"); + assertEquals(msg.args.length, 2); + assertEquals(msg.args[0], "arg0"); + assertEquals(msg.args[1], randoms[i]); + } + System.out.printf("queue time: %dms%n", queueTime); + System.out.printf("send time: %dms%n", sendTime); + } + + @Test(timeout = 2000) + public void test4_10_clients_1000_messages() throws Exception { + int n = 10; + int m = 1000; + + int ids[] = new int[n]; + String tokens[] = new String[n]; + JsonSocket clients[] = new JsonSocket[n]; + // define clients + for (int i = 0; i < n; i++) { + tokens[i] = "random double: " + Math.random(); + ids[i] = clientNetwork.defineClient(tokens[i]); + } + // run server + clientNetwork.listen(port); + // create clients and send tokens + for (int i = 0; i < n; i++) { + clients[i] = new JsonSocket(host, port); + clients[i].send(new Message("token", new Object[]{tokens[i]})); + } + // wait for the server to handle incoming messages + clientNetwork.waitForAllClients(); + + Message msg[] = generateMessages(n); + for (int j = 0; j < m; j++) { + for (int i = 0; i < n; i++) + clientNetwork.queue(ids[i], msg[i]); + // send to clients + clientNetwork.sendAllBlocking(); + // check received messages + for (int i = 0; i < n; i++) { + // get message + Message cm = clients[i].get(Message.class); + // check data of received message + assertEquals(cm.name, msg[i].name); + for (int k = 0; k < msg[i].args.length; k++) + assertEquals(cm.args[k], msg[i].args[k]); + } + } + } + + @Test(timeout = 1000) + public void test5_wait_for_clients() throws Exception { + // takes about 500 seconds! + long startTime = System.currentTimeMillis(); + int n = 100; + String tokens[] = new String[n]; + JsonSocket clients[] = new JsonSocket[n]; + // define clients + for (int i = 0; i < n; i++) { + tokens[i] = "random double: " + Math.random(); + clientNetwork.defineClient(tokens[i]); + } + // run server + clientNetwork.listen(port); + // create clients and send tokens + // wrong token + clients[0] = new JsonSocket(host, port); + clients[0].send(tokens[0]); + // correct token + for (int i = 1; i < n; i++) { + clients[i] = new JsonSocket(host, port); + clients[i].send(new Message("token", new Object[]{tokens[i]})); + } + long endTime = System.currentTimeMillis(); + clientNetwork.waitForAllClients(950-(endTime-startTime)); + assertEquals(clientNetwork.getNumberOfConnected(), n-1); + } + + @Test(timeout = 1000) + public void test6_simple_send_receive() throws Exception { + String token = "123456"; + // define client + int id = clientNetwork.defineClient(token); + // run server + clientNetwork.listen(port); + // create client + JsonSocket client = new JsonSocket(host, port); + // send correct token + client.send(new Message("token", new Object[]{token})); + // wait for the server to check the token + clientNetwork.waitForClient(id); + + Message msg[] = generateMessages(10); + client.send(msg[0]); + client.send(msg[1]); + clientNetwork.startReceivingAll(); + client.send(msg[2]); + // wait for the server to get the message! + Thread.sleep(10); + clientNetwork.stopReceivingAll(); + client.send(msg[3]); + client.send(msg[4]); + Message m = clientNetwork.getReceivedMessage(id); + assertEquals(m.name, msg[2].name); + assertEquals(m.args[0], msg[2].args[0]); + assertEquals(m.args[1], msg[2].args[1]); + } + + @Test + public void test7_long_test_garbage_collection() throws Exception { + int n = 1000; + // transfer 10000*n messages + for (int i = 0; i < n; i++) { + // use the same clientNetwork instance + test4_10_clients_1000_messages(); + clientNetwork.terminate(); + clientNetwork.omitAllClients(); + } + clientNetwork.listen(port); + } + + public static Message generateMessage() { + return new Message("msg#"+Math.random(), new Object[] {null, Math.random()}); + } + + public static Message[] generateMessages(int n) { + Message messages[] = new Message[n]; + for (int i = 0; i < n; i++) + messages[i] = generateMessage(); + return messages; + } + +} \ No newline at end of file diff --git a/src/test/server/network/TerminalNetworkTest.java b/src/test/server/network/TerminalNetworkTest.java new file mode 100644 index 0000000..302ce20 --- /dev/null +++ b/src/test/server/network/TerminalNetworkTest.java @@ -0,0 +1,85 @@ +package test.server.network; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import server.core.model.Event; +import server.network.JsonSocket; +import server.network.TerminalNetwork; +import server.network.data.Message; + +import java.io.IOException; + +import static org.junit.Assert.*; + +public class TerminalNetworkTest { + public static final int port = 12345; + public static final String host = "localhost"; + public static final String token = "123456"; + private TerminalNetwork terminal; + + @Before + public void createTerminalNetwork() { + terminal = new TerminalNetwork(token); + terminal.setHandler(new TerminalNetwork.TerminalInterface() { + @Override + public void putEvent(Event event) { + System.out.println("event received"); + } + + @Override + public Message runCommand(Message command) { + return new Message("report", null); + } + }); + terminal.listen(port); + } + + @After + public void terminateTerminalNetwork() { + terminal.terminate(); + } + + @Test(timeout = 500) + public void wrongToken() { + JsonSocket client = null; + try { + client = new JsonSocket(host, port); + client.send("123456"); // token must be of type Message, not an integer + } catch (IOException e) { + fail(e.getMessage()); + } + try { + Message init = client.get(Message.class); // socket must be closed by the server + fail(); + } catch (IOException ignored) {} + } + + @Test(timeout = 500) + public void initMessage() { + JsonSocket client; + try { + client = new JsonSocket(host, port); + client.send(new Message("token", new Object[] {token})); + Message init = client.get(Message.class); + assertEquals("init", init.name); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + @Test(timeout = 500) + public void sendCommand() { + JsonSocket client; + try { + client = new JsonSocket(host, port); + client.send(new Message("token", new Object[] {token})); + Message init = client.get(Message.class); + client.send(new Message("command", new Object[] {})); + Message report = client.get(Message.class); + assertEquals(report.name, "report"); + } catch (IOException e) { + fail(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/server/network/UINetworkTest.java b/src/test/server/network/UINetworkTest.java new file mode 100644 index 0000000..5d566bd --- /dev/null +++ b/src/test/server/network/UINetworkTest.java @@ -0,0 +1,157 @@ +package test.server.network; + +import org.junit.*; +import org.junit.runners.MethodSorters; +import server.network.JsonSocket; +import server.network.UINetwork; +import server.network.data.Message; + +import java.io.IOException; +import java.net.SocketException; + +import static org.junit.Assert.*; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class UINetworkTest { + public static final int port = 12346; + public static final String host = "localhost"; + public static final String token = "123456"; + private UINetwork uiNetwork; + + @Before + public void createUINetwork() throws InterruptedException { + // create server before each test + uiNetwork = new UINetwork(token); + uiNetwork.listen(port); + } + + @After + public void terminateUINetwork() throws InterruptedException { + // terminate server after each test + uiNetwork.terminate(); + // we should wait for completion of the test (results may appear after a delay) + Thread.sleep(500); + } + + @Test(timeout = 2000) + public void test1_wrong_token() throws IOException, InterruptedException { + // create client + JsonSocket client = new JsonSocket(host, port); + // send wrong token (token must be an instance of Message, not a string) + client.send(token); + // wait for the server to check the token + uiNetwork.waitForClient(1000); + // client must be rejected by the server + assertEquals(uiNetwork.hasClient(), false); + // client must be closed due to incorrect token + try { + client.send("nothing!"); + fail(); + } catch (SocketException ignored) { + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test(timeout = 1000) + public void test2_correct_token() throws IOException, InterruptedException { + // create client + JsonSocket client = new JsonSocket(host, port); + // send correct token + client.send(new Message("token", new Object[]{token})); + // wait for the server to check the token + uiNetwork.waitForClient(); + // token is correct, so client must be alive + client.send("nothing!"); + } + + @Test(timeout = 1000) + public void test3_send_message() throws IOException, InterruptedException { + // create client + JsonSocket client = new JsonSocket(host, port); + // send correct token + client.send(new Message("token", new Object[] {token})); + // wait for the server to check the token + uiNetwork.waitForClient(); + // send message from UINetwork to the client + uiNetwork.sendNonBlocking(new Message("name", new Object[]{"arg0", "arg1"})); + // get message + Message msg = client.get(Message.class); + // check data of received message + assertEquals(msg.name, "name"); + assertEquals(msg.args.length, 2); + assertEquals(msg.args[0], "arg0"); + assertEquals(msg.args[1], "arg1"); + } + + @Test(timeout = 1000) + public void test4_10000_messages() throws IOException, InterruptedException { + // create client + JsonSocket client = new JsonSocket(host, port); + // send correct token + client.send(new Message("token", new Object[] {token})); + // wait for the server to check the token + uiNetwork.waitForClient(); + // init data to send + int total = 10000; + double data[] = new double[total]; + for (int i = 0; i < total; i++) + data[i] = Math.random(); + // send messages from server + long start = System.currentTimeMillis(); + for (int i = 0; i < total; i++) { + uiNetwork.sendNonBlocking(new Message("msg", new Object[]{data[i]})); + } + long end = System.currentTimeMillis(); + System.out.printf("total sending time: %d ms/%d msg%n", end-start, total); + // get messages on client side + start = System.currentTimeMillis(); + for (int i = 0; i < total; i++) + assertEquals(client.get(Message.class).args[0], data[i]); + end = System.currentTimeMillis(); + System.out.printf("total receiving time: %d ms/%d msg%n", end-start, total); + } + + @Test(timeout = 2000) + public void test5_multiple_clients() throws IOException, InterruptedException { + // client1 + JsonSocket client1 = new JsonSocket(host, port); + client1.send(new Message("token", new Object[]{token})); + uiNetwork.waitForClient(); + uiNetwork.sendNonBlocking(new Message("to client1", null)); + // client2 + JsonSocket client2 = new JsonSocket(host, port); + client2.send(new Message("token", new Object[]{token})); + uiNetwork.waitForNewClient(); + uiNetwork.sendNonBlocking(new Message("to client2", null)); + // client3 (wrong token) + JsonSocket client3 = new JsonSocket(host, port); + client3.send(token); + uiNetwork.waitForNewClient(1000); + uiNetwork.sendNonBlocking(new Message("to client2 again", null)); + // get messages + Message msg1 = client1.get(Message.class); + Message msg2 = client2.get(Message.class); + Message msg3 = client2.get(Message.class); + assertEquals(msg1.name, "to client1"); + assertEquals(msg2.name, "to client2"); + assertEquals(msg3.name, "to client2 again"); + } + + @Test + public void test6_token_timeout() throws IOException, InterruptedException { + // create client + JsonSocket client = new JsonSocket(host, port); + // timeout (11 seconds) + Thread.sleep(11000); + // client must be closed due to timeout + try { + client.send(new Message("token", new Object[] {token})); + fail(); + } catch (SocketException ignored) { + } catch (Exception e) { + fail(e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/src/util/Log.java b/src/util/Log.java new file mode 100644 index 0000000..a4459f3 --- /dev/null +++ b/src/util/Log.java @@ -0,0 +1,77 @@ +package util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Logging API. + * + */ +public final class Log { + + public static final int VERBOSE = 2; + public static final int DEBUG = 3; + public static final int INFO = 4; + public static final int WARN = 5; + public static final int ERROR = 6; + + private Log() {} + + public static void v(String tag, String msg) { + log(VERBOSE, tag, msg); + } + + public static void v(String tag, String msg, Throwable tr) { + log(VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); + } + + public static void d(String tag, String msg) { + log(DEBUG, tag, msg); + } + + public static void d(String tag, String msg, Throwable tr) { + log(DEBUG, tag, msg + '\n' + getStackTraceString(tr)); + } + + public static void i(String tag, String msg) { + log(INFO, tag, msg); + } + + public static void i(String tag, String msg, Throwable tr) { + log(INFO, tag, msg + '\n' + getStackTraceString(tr)); + } + + public static void w(String tag, String msg) { + log(WARN, tag, msg); + } + + public static void w(String tag, String msg, Throwable tr) { + log(WARN, tag, msg + '\n' + getStackTraceString(tr)); + } + + public static void w(String tag, Throwable tr) { + log(WARN, tag, getStackTraceString(tr)); + } + + public static void e(String tag, String msg) { + log(ERROR, tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + log(ERROR, tag, msg + '\n' + getStackTraceString(tr)); + } + + public static String getStackTraceString(Throwable tr) { + if (tr == null) + return ""; + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + tr.printStackTrace(pw); + return sw.toString(); + } + + public static void log(int priority, String tag, String msg) { + System.out.printf("\tpriority=%d,%n\ttag=%s,%n\tmessage=%s%n", priority, tag, msg); + } + +} \ No newline at end of file