|
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style license that can be |
|
3 // found in the LICENSE file. |
|
4 |
|
5 package org.chromium.sdk.internal.transport; |
|
6 |
|
7 import java.io.BufferedReader; |
|
8 import java.io.IOException; |
|
9 import java.io.StringWriter; |
|
10 import java.io.Writer; |
|
11 import java.util.HashMap; |
|
12 import java.util.Map; |
|
13 import java.util.logging.Level; |
|
14 import java.util.logging.Logger; |
|
15 |
|
16 /** |
|
17 * A transport message encapsulating the data sent/received over the wire |
|
18 * (protocol headers and content). This class can serialize and deserialize |
|
19 * itself into a BufferedWriter according to the ChromeDevTools Protocol |
|
20 * specification. |
|
21 */ |
|
22 public class Message { |
|
23 |
|
24 /** |
|
25 * This exception is thrown during Message deserialization whenever the input |
|
26 * is malformed. |
|
27 */ |
|
28 public static class MalformedMessageException extends Exception { |
|
29 |
|
30 private static final long serialVersionUID = 1L; |
|
31 |
|
32 public MalformedMessageException() { |
|
33 super(); |
|
34 } |
|
35 |
|
36 public MalformedMessageException(String message) { |
|
37 super(message); |
|
38 } |
|
39 |
|
40 public MalformedMessageException(Throwable cause) { |
|
41 super(cause); |
|
42 } |
|
43 |
|
44 public MalformedMessageException(String message, Throwable cause) { |
|
45 super(message, cause); |
|
46 } |
|
47 |
|
48 } |
|
49 |
|
50 /** |
|
51 * Known ChromeDevTools Protocol headers (ToolHandler implementations |
|
52 * can add their own headers.) |
|
53 */ |
|
54 public enum Header { |
|
55 CONTENT_LENGTH("Content-Length"), |
|
56 TOOL("Tool"), |
|
57 DESTINATION("Destination"), ; |
|
58 |
|
59 public final String name; |
|
60 |
|
61 Header(String value) { |
|
62 this.name = value; |
|
63 } |
|
64 } |
|
65 |
|
66 /** |
|
67 * The class logger. |
|
68 */ |
|
69 private static final Logger LOGGER = Logger.getLogger(Message.class.getName()); |
|
70 |
|
71 /** |
|
72 * The end of protocol header line. |
|
73 */ |
|
74 private static final String HEADER_TERMINATOR = "\r\n"; |
|
75 |
|
76 private final HashMap<String, String> headers; |
|
77 |
|
78 private final String content; |
|
79 |
|
80 public Message(Map<String, String> headers, String content) { |
|
81 this.headers = new HashMap<String, String>(headers); |
|
82 this.content = content; |
|
83 this.headers.put(Header.CONTENT_LENGTH.name, String.valueOf(content == null |
|
84 ? 0 |
|
85 : content.length())); |
|
86 } |
|
87 |
|
88 /** |
|
89 * Sends a message through the specified writer. |
|
90 * |
|
91 * @param writer to send the message through |
|
92 * @throws IOException |
|
93 */ |
|
94 public void sendThrough(Writer writer) throws IOException { |
|
95 String content = maskNull(this.content); |
|
96 for (Map.Entry<String, String> entry : this.headers.entrySet()) { |
|
97 writeNonEmptyHeader(writer, entry.getKey(), entry.getValue()); |
|
98 } |
|
99 writer.write(HEADER_TERMINATOR); |
|
100 if (content.length() > 0) { |
|
101 writer.write(content); |
|
102 } |
|
103 writer.flush(); |
|
104 } |
|
105 |
|
106 /** |
|
107 * Reads a message from the specified reader. |
|
108 * |
|
109 * @param reader to read message from |
|
110 * @return a new message, or {@code null} if input is invalid (end-of-stream |
|
111 * or bad message format) |
|
112 * @throws IOException |
|
113 * @throws MalformedMessageException if the input does not represent a valid |
|
114 * message |
|
115 */ |
|
116 public static Message fromBufferedReader(BufferedReader reader) |
|
117 throws IOException, MalformedMessageException { |
|
118 Map<String, String> headers = new HashMap<String, String>(); |
|
119 synchronized (reader) { |
|
120 while (true) { // read headers |
|
121 String line = reader.readLine(); |
|
122 if (line == null) { |
|
123 LOGGER.fine("End of stream"); |
|
124 return null; |
|
125 } |
|
126 if (line.length() == 0) { |
|
127 break; // end of headers |
|
128 } |
|
129 String[] nameValue = line.split(":", 2); |
|
130 if (nameValue.length != 2) { |
|
131 LOGGER.log(Level.SEVERE, "Bad header line: {0}", line); |
|
132 return null; |
|
133 } else { |
|
134 String trimmedValue = nameValue[1].trim(); |
|
135 headers.put(nameValue[0], trimmedValue); |
|
136 } |
|
137 } |
|
138 |
|
139 // Read payload if applicable |
|
140 String contentLengthStr = getHeader(headers, Header.CONTENT_LENGTH.name, "0"); |
|
141 int contentLength = Integer.valueOf(contentLengthStr.trim()); |
|
142 char[] content = new char[contentLength]; |
|
143 int totalRead = 0; |
|
144 LOGGER.log(Level.FINER, "Reading payload: {0} bytes", contentLength); |
|
145 while (totalRead < contentLength) { |
|
146 int readBytes = reader.read(content, totalRead, contentLength - totalRead); |
|
147 if (readBytes == -1) { |
|
148 // End-of-stream (browser closed?) |
|
149 LOGGER.fine("End of stream while reading content"); |
|
150 return null; |
|
151 } |
|
152 totalRead += readBytes; |
|
153 } |
|
154 |
|
155 // Construct response message |
|
156 String contentString = new String(content); |
|
157 return new Message(headers, contentString); |
|
158 } |
|
159 } |
|
160 |
|
161 /** |
|
162 * @return the "Tool" header value |
|
163 */ |
|
164 public String getTool() { |
|
165 return getHeader(Header.TOOL.name, null); |
|
166 } |
|
167 |
|
168 /** |
|
169 * @return the "Destination" header value |
|
170 */ |
|
171 public String getDestination() { |
|
172 return getHeader(Header.DESTINATION.name, null); |
|
173 } |
|
174 |
|
175 /** |
|
176 * @return the message content. Never {@code null} (for no content, returns an |
|
177 * empty String) |
|
178 */ |
|
179 public String getContent() { |
|
180 return content; |
|
181 } |
|
182 |
|
183 /** |
|
184 * @param name of the header |
|
185 * @param defaultValue to return if the header is not found in the message |
|
186 * @return the {@code name} header value or {@code defaultValue} if the header |
|
187 * is not found in the message |
|
188 */ |
|
189 public String getHeader(String name, String defaultValue) { |
|
190 return getHeader(this.headers, name, defaultValue); |
|
191 } |
|
192 |
|
193 private static String getHeader(Map<? extends String, String> headers, String headerName, |
|
194 String defaultValue) { |
|
195 String value = headers.get(headerName); |
|
196 if (value == null) { |
|
197 value = defaultValue; |
|
198 } |
|
199 return value; |
|
200 } |
|
201 |
|
202 private static String maskNull(String string) { |
|
203 return string == null |
|
204 ? "" |
|
205 : string; |
|
206 } |
|
207 |
|
208 private static void writeNonEmptyHeader(Writer writer, String headerName, String headerValue) |
|
209 throws IOException { |
|
210 if (headerValue != null) { |
|
211 writeHeader(writer, headerName, headerValue); |
|
212 } |
|
213 } |
|
214 |
|
215 @Override |
|
216 public String toString() { |
|
217 StringWriter sw = new StringWriter(); |
|
218 try { |
|
219 this.sendThrough(sw); |
|
220 } catch (IOException e) { |
|
221 // never occurs |
|
222 } |
|
223 return sw.toString(); |
|
224 } |
|
225 |
|
226 private static void writeHeader(Writer writer, String name, String value) throws IOException { |
|
227 writer.append(name).append(':').append(value).append(HEADER_TERMINATOR); |
|
228 } |
|
229 } |