org.chromium.sdk/src/org/chromium/sdk/internal/transport/Message.java
changeset 2 e4420d2515f1
child 52 f577ea64429e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.chromium.sdk/src/org/chromium/sdk/internal/transport/Message.java	Wed Dec 23 17:13:18 2009 -0800
@@ -0,0 +1,229 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.sdk.internal.transport;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A transport message encapsulating the data sent/received over the wire
+ * (protocol headers and content). This class can serialize and deserialize
+ * itself into a BufferedWriter according to the ChromeDevTools Protocol
+ * specification.
+ */
+public class Message {
+
+  /**
+   * This exception is thrown during Message deserialization whenever the input
+   * is malformed.
+   */
+  public static class MalformedMessageException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public MalformedMessageException() {
+      super();
+    }
+
+    public MalformedMessageException(String message) {
+      super(message);
+    }
+
+    public MalformedMessageException(Throwable cause) {
+      super(cause);
+    }
+
+    public MalformedMessageException(String message, Throwable cause) {
+      super(message, cause);
+    }
+
+  }
+
+  /**
+   * Known ChromeDevTools Protocol headers (ToolHandler implementations
+   * can add their own headers.)
+   */
+  public enum Header {
+    CONTENT_LENGTH("Content-Length"),
+    TOOL("Tool"),
+    DESTINATION("Destination"), ;
+
+    public final String name;
+
+    Header(String value) {
+      this.name = value;
+    }
+  }
+
+  /**
+   * The class logger.
+   */
+  private static final Logger LOGGER = Logger.getLogger(Message.class.getName());
+
+  /**
+   * The end of protocol header line.
+   */
+  private static final String HEADER_TERMINATOR = "\r\n";
+
+  private final HashMap<String, String> headers;
+
+  private final String content;
+
+  public Message(Map<String, String> headers, String content) {
+    this.headers = new HashMap<String, String>(headers);
+    this.content = content;
+    this.headers.put(Header.CONTENT_LENGTH.name, String.valueOf(content == null
+        ? 0
+        : content.length()));
+  }
+
+  /**
+   * Sends a message through the specified writer.
+   *
+   * @param writer to send the message through
+   * @throws IOException
+   */
+  public void sendThrough(Writer writer) throws IOException {
+    String content = maskNull(this.content);
+    for (Map.Entry<String, String> entry : this.headers.entrySet()) {
+      writeNonEmptyHeader(writer, entry.getKey(), entry.getValue());
+    }
+    writer.write(HEADER_TERMINATOR);
+    if (content.length() > 0) {
+      writer.write(content);
+    }
+    writer.flush();
+  }
+
+  /**
+   * Reads a message from the specified reader.
+   *
+   * @param reader to read message from
+   * @return a new message, or {@code null} if input is invalid (end-of-stream
+   *         or bad message format)
+   * @throws IOException
+   * @throws MalformedMessageException if the input does not represent a valid
+   *         message
+   */
+  public static Message fromBufferedReader(BufferedReader reader)
+      throws IOException, MalformedMessageException {
+    Map<String, String> headers = new HashMap<String, String>();
+    synchronized (reader) {
+      while (true) { // read headers
+        String line = reader.readLine();
+        if (line == null) {
+          LOGGER.fine("End of stream");
+          return null;
+        }
+        if (line.length() == 0) {
+          break; // end of headers
+        }
+        String[] nameValue = line.split(":", 2);
+        if (nameValue.length != 2) {
+          LOGGER.log(Level.SEVERE, "Bad header line: {0}", line);
+          return null;
+        } else {
+          String trimmedValue = nameValue[1].trim();
+          headers.put(nameValue[0], trimmedValue);
+        }
+      }
+
+      // Read payload if applicable
+      String contentLengthStr = getHeader(headers, Header.CONTENT_LENGTH.name, "0");
+      int contentLength = Integer.valueOf(contentLengthStr.trim());
+      char[] content = new char[contentLength];
+      int totalRead = 0;
+      LOGGER.log(Level.FINER, "Reading payload: {0} bytes", contentLength);
+      while (totalRead < contentLength) {
+        int readBytes = reader.read(content, totalRead, contentLength - totalRead);
+        if (readBytes == -1) {
+          // End-of-stream (browser closed?)
+          LOGGER.fine("End of stream while reading content");
+          return null;
+        }
+        totalRead += readBytes;
+      }
+
+      // Construct response message
+      String contentString = new String(content);
+      return new Message(headers, contentString);
+    }
+  }
+
+  /**
+   * @return the "Tool" header value
+   */
+  public String getTool() {
+    return getHeader(Header.TOOL.name, null);
+  }
+
+  /**
+   * @return the "Destination" header value
+   */
+  public String getDestination() {
+    return getHeader(Header.DESTINATION.name, null);
+  }
+
+  /**
+   * @return the message content. Never {@code null} (for no content, returns an
+   *         empty String)
+   */
+  public String getContent() {
+    return content;
+  }
+
+  /**
+   * @param name of the header
+   * @param defaultValue to return if the header is not found in the message
+   * @return the {@code name} header value or {@code defaultValue} if the header
+   *         is not found in the message
+   */
+  public String getHeader(String name, String defaultValue) {
+    return getHeader(this.headers, name, defaultValue);
+  }
+
+  private static String getHeader(Map<? extends String, String> headers, String headerName,
+      String defaultValue) {
+    String value = headers.get(headerName);
+    if (value == null) {
+      value = defaultValue;
+    }
+    return value;
+  }
+
+  private static String maskNull(String string) {
+    return string == null
+        ? ""
+        : string;
+  }
+
+  private static void writeNonEmptyHeader(Writer writer, String headerName, String headerValue)
+      throws IOException {
+    if (headerValue != null) {
+      writeHeader(writer, headerName, headerValue);
+    }
+  }
+
+  @Override
+  public String toString() {
+    StringWriter sw = new StringWriter();
+    try {
+      this.sendThrough(sw);
+    } catch (IOException e) {
+      // never occurs
+    }
+    return sw.toString();
+  }
+
+  private static void writeHeader(Writer writer, String name, String value) throws IOException {
+    writer.append(name).append(':').append(value).append(HEADER_TERMINATOR);
+  }
+}