public class Client { public static final long POLLING_PERIOD_MILLIS = 1000L; public static final Logger CLIENT_LOGGER = new FileLogger("clientlog.txt"); private static final Logger logger = Log.create(Client.class); private List<String> localHistory = new ArrayList<String>(); private String host; private int port; private boolean connected = false; private Thread listenerThread; private Thread messageSendingThread; public Client(String host, Integer port) { this.host = host; this.port = port; } public void connect() { URL url = null; try { url = new URL(Constants.PROTOCOL, host, port, Constants.CONTEXT_PATH); url.openConnection(); // try connect to server connected = true; startListening(); startMessageSending(); } catch (MalformedURLException e) { logger.error("Could not build URL to server", e); throw new RuntimeException(e); } catch (IOException e) { logger.error(String.format("Could not connect to server on %s", url.toString()), e); } } private HttpURLConnection prepareInputConnection(URL url) throws IOException { HttpURLConnection connection = HttpURLConnection.class.cast(url.openConnection()); connection.setDoInput(true); return connection; } private HttpURLConnection prepareOutputConnection() throws IOException { URL url = new URL(Constants.PROTOCOL, host, port, Constants.CONTEXT_PATH); HttpURLConnection connection = HttpURLConnection.class.cast(url.openConnection()); connection.setRequestMethod(Constants.REQUEST_METHOD_POST); connection.setDoOutput(true); return connection; } public void disconnect() { // Thread#stop method is deprecated. The listeners threads stops when // finish it's runnable action. connected = false; } private void startListening() { // if listener thread is alive and listening now, no need to recreate. Just reuse it. if (listenerThread != null && listenerThread.isAlive()) { return; } Runnable listeningAction = () -> { boolean running = true; while (connected && running) { List<String> list = getMessages(); localHistory.addAll(list); try { Thread.sleep(POLLING_PERIOD_MILLIS); } catch (InterruptedException e) { logger.error("The message listening thread was interrupted", e); running = false; } } }; listenerThread = new Thread(listeningAction); listenerThread.start(); } private void startMessageSending() { // if listener thread is alive and listening now, no need to recreate. Just reuse it. if (messageSendingThread != null && messageSendingThread.isAlive()) { return; } Runnable messageSendingAction = () -> { Scanner scanner = new Scanner(System.in); while (connected) { String message = scanner.nextLine(); sendMessage(message); } }; messageSendingThread = new Thread(messageSendingAction); messageSendingThread.start(); } private void checkConnected() { if (!connected) { RuntimeException notConnectedError = new RuntimeException("No connection to server"); logger.error("No connection to server", notConnectedError); throw notConnectedError; } } public List<String> getMessages() { CLIENT_LOGGER.info("start receiving messages"); checkConnected(); List<String> list = new ArrayList<>(); HttpURLConnection incomeConnection = null; try { CLIENT_LOGGER.info("send request for receive messages"); String query = String.format( "%s?%s=%s", Constants.CONTEXT_PATH, Constants.REQUEST_PARAM_TOKEN, MessageHelper.buildToken(localHistory.size())); URL url = new URL(Constants.PROTOCOL, host, port, query); incomeConnection = prepareInputConnection(url); CLIENT_LOGGER.info("response is received"); String response = MessageHelper.inputStreamToString(incomeConnection.getInputStream()); JSONObject jsonObject = MessageHelper.stringToJsonObject(response); JSONArray jsonArray = (JSONArray) jsonObject.get("messages"); CLIENT_LOGGER.info("received " + jsonArray.size() + " messages"); for (Object o : jsonArray) { logger.info(String.format("Message from server: %s", o)); CLIENT_LOGGER.info("message from server: " + o); list.add(o.toString()); } /** Here is an example how for cycle can be replaced with Java 8 Stream API */ // jsonArray.forEach(System.out::println); // list = (List<String>) // jsonArray.stream().map(Object::toString).collect(Collectors.toList()); } catch (ParseException e) { logger.error("Could not parse message", e); CLIENT_LOGGER.error("could not parse message", e); } catch (ConnectException e) { logger.error("Connection error. Disconnecting...", e); CLIENT_LOGGER.error("connection error", e); disconnect(); } catch (IOException e) { logger.error("IOException occured while reading input message", e); CLIENT_LOGGER.error("IOException occured while reading input message", e); } finally { if (incomeConnection != null) { incomeConnection.disconnect(); } } CLIENT_LOGGER.info("stop receiving messages"); return list; } public void sendMessage(String message) { checkConnected(); HttpURLConnection outcomeConnection = null; try { CLIENT_LOGGER.info("start sending message \"" + message + "\""); outcomeConnection = prepareOutputConnection(); byte[] buffer = MessageHelper.buildSendMessageRequestBody(message).getBytes(); OutputStream outputStream = outcomeConnection.getOutputStream(); outputStream.write(buffer, 0, buffer.length); outputStream.close(); outcomeConnection.getInputStream(); // to send data to server CLIENT_LOGGER.info("message sent"); } catch (ConnectException e) { logger.error("Connection error. Disconnecting...", e); CLIENT_LOGGER.error("connection error", e); disconnect(); } catch (IOException e) { CLIENT_LOGGER.error("IOException", e); logger.error("IOException occurred while sending message", e); } finally { if (outcomeConnection != null) { outcomeConnection.disconnect(); } CLIENT_LOGGER.info("stop sending message \"" + message + "\""); } } }
public class ServerHandler implements HttpHandler { private static final Logger logger = Log.create(ServerHandler.class); private MessageStorage messageStorage = new InMemoryMessageStorage(); @Override public void handle(HttpExchange httpExchange) throws IOException { Response response; try { response = dispatch(httpExchange); } catch (Throwable e) { // WARNING! It's not a good practice to catch all exceptions via Throwable // or Exception classes. But if you want to handle and you know // how to handle them correctly, you may use such approach. // Useful when you use thread pool and don't want to corrupt a thread logger.error("An error occurred when dispatching request.", e); response = new Response( Constants.RESPONSE_CODE_INTERNAL_SERVER_ERROR, "Error while dispatching message"); } sendResponse(httpExchange, response); } private Response dispatch(HttpExchange httpExchange) { if (Constants.REQUEST_METHOD_GET.equals(httpExchange.getRequestMethod())) { return doGet(httpExchange); } else if (Constants.REQUEST_METHOD_POST.equals(httpExchange.getRequestMethod())) { return doPost(httpExchange); } else if (Constants.REQUEST_METHOD_PUT.equals(httpExchange.getRequestMethod())) { return doPut(httpExchange); } else if (Constants.REQUEST_METHOD_DELETE.equals(httpExchange.getRequestMethod())) { return doDelete(httpExchange); } else if (Constants.REQUEST_METHOD_OPTIONS.equals(httpExchange.getRequestMethod())) { return doOptions(httpExchange); } else { return new Response( Constants.RESPONSE_CODE_METHOD_NOT_ALLOWED, String.format("Unsupported http method %s", httpExchange.getRequestMethod())); } } private Response doGet(HttpExchange httpExchange) { String query = httpExchange.getRequestURI().getQuery(); if (query == null) { return Response.badRequest("Absent query in request"); } Map<String, String> map = queryToMap(query); String token = map.get(Constants.REQUEST_PARAM_TOKEN); if (StringUtils.isEmpty(token)) { return Response.badRequest("Token query parameter is required"); } try { int index = MessageHelper.parseToken(token); if (index > messageStorage.size()) { return Response.badRequest( String.format( "Incorrect token in request: %s. Server does not have so many messages", token)); } Portion portion = new Portion(index); List<Message> messages = messageStorage.getPortion(portion); String responseBody = MessageHelper.buildServerResponseBody(messages, messageStorage.size()); return Response.ok(responseBody); } catch (InvalidTokenException e) { return Response.badRequest(e.getMessage()); } } // +++++++ private Response doPost(HttpExchange httpExchange) { try { Message message = MessageHelper.getClientMessage(httpExchange.getRequestBody()); logger.info(String.format("Received new message from user: %s", message)); messageStorage.addMessage(message); return Response.ok(); } catch (ParseException e) { logger.error("Could not parse message.", e); return new Response(Constants.RESPONSE_CODE_BAD_REQUEST, "Incorrect request body"); } catch (MessageExistException e) { logger.error("Message with same id exist.", e); return new Response(Constants.RESPONSE_CODE_BAD_REQUEST, "Message with same id exist"); } } // ++++++ private Response doPut(HttpExchange httpExchange) { try { JSONObject jsonObject = stringToJsonObject(inputStreamToString(httpExchange.getRequestBody())); Message message = new Message(); message.setId(((String) jsonObject.get(Constants.Message.FIELD_ID))); message.setText(((String) jsonObject.get(Constants.Message.FIELD_TEXT))); // System.out.println(((long)jsonObject.get(Constants.Message.FIELD_TIMESTAMP_MODIFIED))); message.setTimestampModified( ((long) jsonObject.get(Constants.Message.FIELD_TIMESTAMP_MODIFIED))); logger.info(String.format("Received updated message from user: %s", message)); if (messageStorage.updateMessage(message)) { return Response.ok(); } return Response.badRequest("This message does not exist"); } catch (ParseException e) { logger.error("Could not parse message.", e); return new Response(Constants.RESPONSE_CODE_BAD_REQUEST, "Incorrect request body"); } } private Response doDelete(HttpExchange httpExchange) { String query = httpExchange.getRequestURI().getQuery(); if (query == null) { return Response.badRequest("Absent query in request"); } Map<String, String> map = queryToMap(query); String messageId = map.get(Constants.REQUEST_PARAM_MESSAGE_ID); if (StringUtils.isEmpty(messageId)) { return Response.badRequest("Message id query parameter is required"); } if (messageStorage.removeMessage(messageId)) { return Response.ok(); } return Response.badRequest("This message does not exist"); } private Response doOptions(HttpExchange httpExchange) { httpExchange .getResponseHeaders() .add(Constants.REQUEST_HEADER_ACCESS_CONTROL_METHODS, Constants.HEADER_VALUE_ALL_METHODS); return Response.ok(); } private void sendResponse(HttpExchange httpExchange, Response response) { try (OutputStream os = httpExchange.getResponseBody()) { byte[] bytes = response.getBody().getBytes(); Headers headers = httpExchange.getResponseHeaders(); headers.add(Constants.REQUEST_HEADER_ACCESS_CONTROL_ORIGIN, "*"); httpExchange.sendResponseHeaders(response.getStatusCode(), bytes.length); os.write(bytes); // there is no need to close stream manually // as try-catch with auto-closable is used /** * {@see http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html} */ } catch (IOException e) { logger.error("Could not send response", e); } } private Map<String, String> queryToMap(String query) { Map<String, String> result = new HashMap<>(); for (String queryParam : query.split(Constants.REQUEST_PARAMS_DELIMITER)) { String paramKeyValuePair[] = queryParam.split("="); if (paramKeyValuePair.length > 1) { result.put(paramKeyValuePair[0], paramKeyValuePair[1]); } else { result.put(paramKeyValuePair[0], ""); } } return result; } /** * This method does absolutely the same as {@link ServerHandler#queryToMap(String)} one, but uses * Java's 8 Stream API and lambda expressions * * <p>It's just as an example. Bu you can use it * * @param query the query to be parsed * @return the map, containing parsed key-value pairs from request */ private Map<String, String> queryToMap2(String query) { return Stream.of(query.split(Constants.REQUEST_PARAMS_DELIMITER)) .collect( Collectors.toMap( keyValuePair -> keyValuePair.split("=")[0], keyValuePair -> keyValuePair.split("=")[1])); } }
public class InMemoryMessageStorage implements MessageStorage { private static final String DEFAULT_PERSISTENCE_FILE = "messages.srg"; private static final Logger logger = Log.create(InMemoryMessageStorage.class); private List<Message> messages; public InMemoryMessageStorage() { messages = new ArrayList<>(); loadHistory(); } @Override public synchronized List<Message> getPortion(Portion portion) { int from = portion.getFromIndex(); if (from < 0) { throw new IllegalArgumentException( String.format("Portion from index %d can not be less then 0", from)); } int to = portion.getToIndex(); if (to != -1 && to < portion.getFromIndex()) { throw new IllegalArgumentException( String.format("Porting last index %d can not be less then start index %d", to, from)); } to = Math.max(to, messages.size()); return messages.subList(from, to); } @Override public void addMessage(Message message) { messages.add(message); rewriteHistory(); } @Override public boolean updateMessage(Message message) { for (int i = 0; i < messages.size(); i++) { if (messages.get(i).getId().compareTo(message.getId()) == 0) { Message newMessage = messages.get(i); if (newMessage.isDeleted()) return false; if (newMessage.isEdited()) { newMessage.setWasEdited(true); } newMessage.setText(message.getText()); newMessage.setEdited(true); messages.set(i, newMessage); rewriteHistory(); return true; } } return false; } @Override public synchronized boolean removeMessage(String messageId) { for (int i = 0; i < messages.size(); i++) { if (messages.get(i).getId().compareTo(Long.parseLong(messageId)) == 0) { Message newMessage = messages.get(i); newMessage.setText(""); newMessage.setDeleted(true); newMessage.setEdited(false); newMessage.setWasEdited(false); messages.set(i, newMessage); rewriteHistory(); return true; } } return false; } @Override public int size() { return messages.size(); } private boolean rewriteHistory() { try (Writer writer = new OutputStreamWriter(new FileOutputStream(DEFAULT_PERSISTENCE_FILE), "UTF-8")) { JSONArray array = MessageHelper.getJsonArrayOfMessages(messages); writer.write(array.toString()); return true; } catch (IOException e) { logger.error("Could not parse message.", e); return false; } } private void loadHistory() { StringBuilder jsonArrayString = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new FileReader(DEFAULT_PERSISTENCE_FILE))) { while (reader.ready()) { jsonArrayString.append(reader.readLine()); } } catch (IOException e) { logger.error("Could not parse message.", e); } JSONArray jsonArray = new JSONArray(); if (jsonArrayString.length() == 0) { return; } try { jsonArray = (JSONArray) MessageHelper.getJsonParser().parse(jsonArrayString.toString()); } catch (ParseException e) { logger.error("Could not parse message.", e); } for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = (JSONObject) jsonArray.get(i); Message message = MessageHelper.jsonObjectToMessage(jsonObject); messages.add(message); } } }
public class MessageHelper { public static final String MESSAGE_PART_ALL_MSG = "messages"; public static final String MESSAGE_PART_SINGLE_MSG = "message"; public static final String MESSAGE_PART_TOKEN = "token"; public static final String TOKEN_TEMPLATE = "TN%dEN"; public static final String TOKEN_FORMAT = "TN[0-9]{2,}EN"; private static final JSONParser jsonParser = new JSONParser(); private static final Logger logger = Log.create(MessageHelper.class); /** * Builds token based on amount of messages, which are already stored on server side or client * side. * * <p>E.g. Client has 5 messages. It does not want to retrieve messages it already has. So, client * passes 5 as argument to this method, and this method will return a token, which says to server: * Just give me all messages, but skip first 5. * * <p>On the other hand, server passes amount of messages it has (size of messages collection). * So, client can parse token and understand how many messages are on server side * * @param receivedMessagesCount amount of messages to skip. * @return generated token */ public static String buildToken(int receivedMessagesCount) { Integer stateCode = encodeIndex(receivedMessagesCount); return String.format(TOKEN_TEMPLATE, stateCode); } /** * Parses token and extract encoded amount of messages (typically - index) * * @param token the token to be parsed * @return decoded amount messages (index) */ public static int parseToken(String token) { if (!token.matches(TOKEN_FORMAT)) { throw new InvalidTokenException("Incorrect format of token"); } String encodedIndex = token.substring(2, token.length() - 2); try { int stateCode = Integer.valueOf(encodedIndex); return decodeIndex(stateCode); } catch (NumberFormatException e) { logger.error("Could not parse token", e); throw new InvalidTokenException("Invalid encoded value: " + encodedIndex); } } private static int encodeIndex(int receivedMessagesCount) { return receivedMessagesCount * 8 + 11; } private static int decodeIndex(int stateCode) { return (stateCode - 11) / 8; } @SuppressWarnings( "unchecked") // allows to suppress warning of unchecked parameter type for generics public static String buildServerResponseBody(List<Message> messages, int lastPosition) { JSONArray array = getJsonArrayOfMessages(messages); JSONObject jsonObject = new JSONObject(); jsonObject.put(MESSAGE_PART_ALL_MSG, array); jsonObject.put(MESSAGE_PART_TOKEN, buildToken(lastPosition)); return jsonObject.toJSONString(); } private static JSONArray getJsonArrayOfMessages(List<Message> messages) { // Java * approach /* List<JSONObject> jsonMessages = messages.stream() .map(MessageHelper::messageToJSONObject) .collect(Collectors.toList()); */ List<JSONObject> jsonMessages = new LinkedList<>(); for (Message message : messages) { jsonMessages.add(messageToJSONObject(message)); } JSONArray array = new JSONArray(); array.addAll(jsonMessages); return array; } @SuppressWarnings("unchecked") public static String buildSendMessageRequestBody(String message) { JSONObject jsonObject = new JSONObject(); jsonObject.put(MESSAGE_PART_SINGLE_MSG, message); return jsonObject.toJSONString(); } public static Message getClientMessage(InputStream inputStream) throws ParseException { JSONObject jsonObject = stringToJsonObject(inputStreamToString(inputStream)); String id = ((String) jsonObject.get(Constants.Message.FIELD_ID)); String author = ((String) jsonObject.get(Constants.Message.FIELD_AUTHOR)); long timestamp = ((long) jsonObject.get(Constants.Message.FIELD_TIMESTAMP)); String text = ((String) jsonObject.get(Constants.Message.FIELD_TEXT)); boolean deleted = jsonObject.get(Constants.Message.FIELD_DELETED).equals("true"); Message message = new Message(); message.setId(id); message.setAuthor(author); message.setTimestamp(timestamp); message.setText(text); message.setDeleted(deleted); return message; } public static JSONObject stringToJsonObject(String json) throws ParseException { // The same as (JSONObject) jsonParser.parse(json.trim()); return JSONObject.class.cast(jsonParser.parse(json.trim())); } public static String inputStreamToString(InputStream in) { byte[] buffer = new byte[1024]; int length = 0; try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { while ((length = in.read(buffer)) != -1) { outStream.write(buffer, 0, length); } return outStream.toString(); } catch (IOException e) { logger.error("An error occurred while reading input stream", e); throw new RuntimeException(e); } } private static JSONObject messageToJSONObject(Message message) { JSONObject jsonObject = new JSONObject(); jsonObject.put(Constants.Message.FIELD_ID, message.getId()); jsonObject.put(Constants.Message.FIELD_AUTHOR, message.getAuthor()); jsonObject.put(Constants.Message.FIELD_TIMESTAMP, message.getTimestamp()); jsonObject.put(Constants.Message.FIELD_TEXT, message.getText()); jsonObject.put(Constants.Message.FIELD_DELETED, message.isDeleted()); return jsonObject; } }