PageRenderTime 56ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/Npr_Test/src/org/npr/android/test/HttpServer.java

http://npr-android-app.googlecode.com/
Java | 324 lines | 175 code | 36 blank | 113 comment | 17 complexity | 342211e9ee4a132632eb4621b66f097a MD5 | raw file
Possible License(s): Apache-2.0
  1. // Copyright 2010 Google Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package org.npr.android.test;
  15. import android.util.Log;
  16. import org.apache.http.HttpStatus;
  17. import org.apache.http.ProtocolVersion;
  18. import org.apache.http.message.BasicStatusLine;
  19. import java.io.BufferedReader;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.io.UnsupportedEncodingException;
  24. import java.net.InetAddress;
  25. import java.net.ServerSocket;
  26. import java.net.Socket;
  27. import java.net.SocketException;
  28. import java.net.SocketTimeoutException;
  29. import java.net.URLDecoder;
  30. import java.net.UnknownHostException;
  31. import java.util.StringTokenizer;
  32. // TODO: This is a test framework piece and therefore needs a unit-test.
  33. /**
  34. * An abstract HTTP server used for testing.
  35. *
  36. * Implementing classes must define the <code>getData</code> method.
  37. */
  38. public abstract class HttpServer implements Runnable {
  39. private static final String TAG = HttpServer.class.getName();
  40. private int port = 0;
  41. private boolean isRunning = true;
  42. private ServerSocket socket;
  43. private Thread thread;
  44. private boolean simulateStream = false;
  45. /**
  46. * Returns the port that the server is running on. The host is localhost
  47. * (127.0.0.1).
  48. *
  49. * @return A port number assigned by the OS.
  50. */
  51. public int getPort() {
  52. return port;
  53. }
  54. /**
  55. * Prepare the server to start.
  56. *
  57. * This only needs to be called once per instance. Once initialized, the
  58. * server can be started and stopped as needed.
  59. */
  60. public void init() {
  61. try {
  62. socket = new ServerSocket(port, 0, InetAddress.getByAddress(new byte[] {
  63. 127, 0, 0, 1 }));
  64. socket.setSoTimeout(5000);
  65. port = socket.getLocalPort();
  66. Log.d(TAG, "Server stated at " + socket.getInetAddress().getHostAddress()
  67. + ":" + port);
  68. } catch (UnknownHostException e) {
  69. Log.e(TAG, "Error initializing server", e);
  70. } catch (IOException e) {
  71. Log.e(TAG, "Error initializing server", e);
  72. }
  73. }
  74. /**
  75. * Start the server.
  76. */
  77. public void start() {
  78. thread = new Thread(this);
  79. thread.start();
  80. }
  81. /**
  82. * Stop the server.
  83. *
  84. * This stops the thread listening to the port. It may take up to five seconds
  85. * to close the service and this call blocks until that occurs.
  86. */
  87. public void stop() {
  88. isRunning = false;
  89. if (thread == null) {
  90. Log.w(TAG, "Server was stopped without being started.");
  91. return;
  92. }
  93. Log.d(TAG, "Stopping server.");
  94. thread.interrupt();
  95. try {
  96. thread.join(5000);
  97. } catch (InterruptedException e) {
  98. Log.w(TAG, "Server was interrupted while stopping", e);
  99. }
  100. }
  101. /**
  102. * Determines if the server is running (i.e. has been
  103. * <code>start</code>ed and has not been <code>stop</code>ed.
  104. *
  105. * @return <code>true</code> if the server is running, otherwise <code>false</code>
  106. */
  107. public boolean isRunning() {
  108. return isRunning;
  109. }
  110. /**
  111. * Sets a value that determines whether the server will simulate an
  112. * open-ended stream by looping the content of the DataSource. This
  113. * is false, by default.
  114. *
  115. * @param simulateStreaming <code>true</code> to loop content, else <code>false</code>
  116. */
  117. protected void setSimulateStream(boolean simulateStreaming) {
  118. simulateStream = simulateStreaming;
  119. }
  120. /**
  121. * Determines if the server is configured to loop content, simulating an
  122. * open-ended stream. This is false, by default.
  123. * @return <code>true</code> to loop content, else <code>false</code>
  124. */
  125. public boolean isSimulatingStream() {
  126. return simulateStream;
  127. }
  128. // TODO: This could be hidden inside a private class.
  129. /**
  130. * This is used internally by the server and should not be called directly.
  131. */
  132. @Override
  133. public void run() {
  134. Log.d(TAG, "running");
  135. while (isRunning) {
  136. try {
  137. Socket client = socket.accept();
  138. if (client == null) {
  139. continue;
  140. }
  141. Log.d(TAG, "client connected");
  142. DataSource data = getData(readRequest(client));
  143. processRequest(data, client);
  144. } catch (SocketTimeoutException e) {
  145. // Do nothing
  146. } catch (IOException e) {
  147. Log.e(TAG, "Error connecting to client", e);
  148. }
  149. }
  150. Log.d(TAG, "Server interrupted or stopped. Shutting down.");
  151. }
  152. /**
  153. * Returns a DataSource object for a given request.
  154. *
  155. * This method must be implemented by subclasses.
  156. *
  157. * @param request The path of the resource requested. e.g. /index.html
  158. * @return A DataSource that provides meta-data and a stream to the resource.
  159. */
  160. protected abstract DataSource getData(String request);
  161. /*
  162. * Get the HTTP request line from the client and
  163. * parse out the path of the request.
  164. *
  165. * @return a URL-decoded string of the request.
  166. */
  167. private String readRequest(Socket client) {
  168. InputStream is;
  169. String firstLine;
  170. try {
  171. is = client.getInputStream();
  172. // We really don't need 8k (default) buffer (it throws a warning)
  173. // 2k is big enough: http://www.boutell.com/newfaq/misc/urllength.html
  174. BufferedReader reader = new BufferedReader(new InputStreamReader(is),
  175. 2048);
  176. firstLine = reader.readLine();
  177. } catch (IOException e) {
  178. Log.e(TAG, "Error parsing request from client", e);
  179. return null;
  180. }
  181. try {
  182. StringTokenizer st = new StringTokenizer(firstLine);
  183. st.nextToken(); // Skip method
  184. return URLDecoder.decode(st.nextToken(), "x-www-form-urlencoded");
  185. } catch (UnsupportedEncodingException e) {
  186. return null;
  187. }
  188. }
  189. /*
  190. * Sends the HTTP response to the client, including
  191. * headers (as applicable) and content.
  192. */
  193. private void processRequest(DataSource dataSource, Socket client)
  194. throws IllegalStateException, IOException {
  195. if (dataSource == null) {
  196. Log.e(TAG, "Invalid (null) resource.");
  197. client.close();
  198. return;
  199. }
  200. Log.d(TAG, "setting response headers");
  201. StringBuilder httpString = new StringBuilder();
  202. httpString.append(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1),
  203. HttpStatus.SC_OK, "OK"));
  204. httpString.append("\n");
  205. httpString.append("Content-Type: ").append(dataSource.getContentType());
  206. httpString.append("\n");
  207. // Some content (e.g. streams) does not define a length
  208. long length = dataSource.getContentLength();
  209. if( length >= 0 ) {
  210. httpString.append("Content-Length: ").append(length);
  211. httpString.append("\n");
  212. }
  213. httpString.append("\n");
  214. Log.d(TAG, "headers done");
  215. InputStream data = null;
  216. try {
  217. data = dataSource.createInputStream();
  218. byte[] buffer = httpString.toString().getBytes();
  219. int readBytes;
  220. Log.d(TAG, "writing to client");
  221. client.getOutputStream().write(buffer, 0, buffer.length);
  222. // Start sending content.
  223. byte[] buff = new byte[1024 * 50];
  224. while (isRunning ) {
  225. readBytes = data.read(buff, 0, buff.length);
  226. if (readBytes == -1) {
  227. if (simulateStream) {
  228. data.close();
  229. data = dataSource.createInputStream();
  230. readBytes = data.read(buff, 0, buff.length);
  231. if (readBytes == -1) {
  232. throw new IOException("Error re-opening data source for looping.");
  233. }
  234. } else {
  235. break;
  236. }
  237. }
  238. client.getOutputStream().write(buff, 0, readBytes);
  239. }
  240. } catch (SocketException e) {
  241. // Ignore when the client breaks connection
  242. Log.w(TAG, "Ignoring " + e.getMessage());
  243. } catch (IOException e) {
  244. Log.e(TAG, "Error getting content stream.", e);
  245. } catch (Exception e) {
  246. Log.e(TAG, "Error streaming file content.", e);
  247. } finally {
  248. if (data != null) {
  249. data.close();
  250. }
  251. client.close();
  252. }
  253. }
  254. /**
  255. * An abstract class that provides meta-data and access to a stream
  256. * for resources.
  257. */
  258. protected abstract class DataSource {
  259. /**
  260. * Returns a MIME-compatible content type (e.g. "text/html") for the resource.
  261. * This method must be implemented.
  262. * @return A MIME content type.
  263. */
  264. public abstract String getContentType();
  265. /**
  266. * Creates and opens an input stream that returns the contents
  267. * of the resource.
  268. * This method must be implemented.
  269. * @return An <code>InputStream</code> to access the resource.
  270. * @throws IOException If the implementing class produces an error when opening the stream.
  271. */
  272. public abstract InputStream createInputStream() throws IOException;
  273. /**
  274. * Returns the length of resource in bytes.
  275. *
  276. * By default this returns -1, which causes no content-type
  277. * header to be sent to the client. This would make sense for
  278. * a stream content of unknown or undefined length. If your
  279. * resource has a defined length you should override this
  280. * method and return that.
  281. *
  282. * @return The length of the resource in bytes.
  283. */
  284. public long getContentLength() {
  285. return -1;
  286. }
  287. }
  288. }