Exemple #1
0
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 + "\"");
    }
  }
}
Exemple #2
0
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;
  }
}