// Copyright 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.npr.android.test; import android.util.Log; import org.apache.http.HttpStatus; import org.apache.http.ProtocolVersion; import org.apache.http.message.BasicStatusLine; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URLDecoder; import java.net.UnknownHostException; import java.util.StringTokenizer; // TODO: This is a test framework piece and therefore needs a unit-test. /** * An abstract HTTP server used for testing. * * Implementing classes must define the <code>getData</code> method. */ public abstract class HttpServer implements Runnable { private static final String TAG = HttpServer.class.getName(); private int port = 0; private boolean isRunning = true; private ServerSocket socket; private Thread thread; private boolean simulateStream = false; /** * Returns the port that the server is running on. The host is localhost * (127.0.0.1). * * @return A port number assigned by the OS. */ public int getPort() { return port; } /** * Prepare the server to start. * * This only needs to be called once per instance. Once initialized, the * server can be started and stopped as needed. */ public void init() { try { socket = new ServerSocket(port, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); socket.setSoTimeout(5000); port = socket.getLocalPort(); Log.d(TAG, "Server stated at " + socket.getInetAddress().getHostAddress() + ":" + port); } catch (UnknownHostException e) { Log.e(TAG, "Error initializing server", e); } catch (IOException e) { Log.e(TAG, "Error initializing server", e); } } /** * Start the server. */ public void start() { thread = new Thread(this); thread.start(); } /** * Stop the server. * * This stops the thread listening to the port. It may take up to five seconds * to close the service and this call blocks until that occurs. */ public void stop() { isRunning = false; if (thread == null) { Log.w(TAG, "Server was stopped without being started."); return; } Log.d(TAG, "Stopping server."); thread.interrupt(); try { thread.join(5000); } catch (InterruptedException e) { Log.w(TAG, "Server was interrupted while stopping", e); } } /** * Determines if the server is running (i.e. has been * <code>start</code>ed and has not been <code>stop</code>ed. * * @return <code>true</code> if the server is running, otherwise <code>false</code> */ public boolean isRunning() { return isRunning; } /** * Sets a value that determines whether the server will simulate an * open-ended stream by looping the content of the DataSource. This * is false, by default. * * @param simulateStreaming <code>true</code> to loop content, else <code>false</code> */ protected void setSimulateStream(boolean simulateStreaming) { simulateStream = simulateStreaming; } /** * Determines if the server is configured to loop content, simulating an * open-ended stream. This is false, by default. * @return <code>true</code> to loop content, else <code>false</code> */ public boolean isSimulatingStream() { return simulateStream; } // TODO: This could be hidden inside a private class. /** * This is used internally by the server and should not be called directly. */ @Override public void run() { Log.d(TAG, "running"); while (isRunning) { try { Socket client = socket.accept(); if (client == null) { continue; } Log.d(TAG, "client connected"); DataSource data = getData(readRequest(client)); processRequest(data, client); } catch (SocketTimeoutException e) { // Do nothing } catch (IOException e) { Log.e(TAG, "Error connecting to client", e); } } Log.d(TAG, "Server interrupted or stopped. Shutting down."); } /** * Returns a DataSource object for a given request. * * This method must be implemented by subclasses. * * @param request The path of the resource requested. e.g. /index.html * @return A DataSource that provides meta-data and a stream to the resource. */ protected abstract DataSource getData(String request); /* * Get the HTTP request line from the client and * parse out the path of the request. * * @return a URL-decoded string of the request. */ private String readRequest(Socket client) { InputStream is; String firstLine; try { is = client.getInputStream(); // We really don't need 8k (default) buffer (it throws a warning) // 2k is big enough: http://www.boutell.com/newfaq/misc/urllength.html BufferedReader reader = new BufferedReader(new InputStreamReader(is), 2048); firstLine = reader.readLine(); } catch (IOException e) { Log.e(TAG, "Error parsing request from client", e); return null; } try { StringTokenizer st = new StringTokenizer(firstLine); st.nextToken(); // Skip method return URLDecoder.decode(st.nextToken(), "x-www-form-urlencoded"); } catch (UnsupportedEncodingException e) { return null; } } /* * Sends the HTTP response to the client, including * headers (as applicable) and content. */ private void processRequest(DataSource dataSource, Socket client) throws IllegalStateException, IOException { if (dataSource == null) { Log.e(TAG, "Invalid (null) resource."); client.close(); return; } Log.d(TAG, "setting response headers"); StringBuilder httpString = new StringBuilder(); httpString.append(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK")); httpString.append("\n"); httpString.append("Content-Type: ").append(dataSource.getContentType()); httpString.append("\n"); // Some content (e.g. streams) does not define a length long length = dataSource.getContentLength(); if( length >= 0 ) { httpString.append("Content-Length: ").append(length); httpString.append("\n"); } httpString.append("\n"); Log.d(TAG, "headers done"); InputStream data = null; try { data = dataSource.createInputStream(); byte[] buffer = httpString.toString().getBytes(); int readBytes; Log.d(TAG, "writing to client"); client.getOutputStream().write(buffer, 0, buffer.length); // Start sending content. byte[] buff = new byte[1024 * 50]; while (isRunning ) { readBytes = data.read(buff, 0, buff.length); if (readBytes == -1) { if (simulateStream) { data.close(); data = dataSource.createInputStream(); readBytes = data.read(buff, 0, buff.length); if (readBytes == -1) { throw new IOException("Error re-opening data source for looping."); } } else { break; } } client.getOutputStream().write(buff, 0, readBytes); } } catch (SocketException e) { // Ignore when the client breaks connection Log.w(TAG, "Ignoring " + e.getMessage()); } catch (IOException e) { Log.e(TAG, "Error getting content stream.", e); } catch (Exception e) { Log.e(TAG, "Error streaming file content.", e); } finally { if (data != null) { data.close(); } client.close(); } } /** * An abstract class that provides meta-data and access to a stream * for resources. */ protected abstract class DataSource { /** * Returns a MIME-compatible content type (e.g. "text/html") for the resource. * This method must be implemented. * @return A MIME content type. */ public abstract String getContentType(); /** * Creates and opens an input stream that returns the contents * of the resource. * This method must be implemented. * @return An <code>InputStream</code> to access the resource. * @throws IOException If the implementing class produces an error when opening the stream. */ public abstract InputStream createInputStream() throws IOException; /** * Returns the length of resource in bytes. * * By default this returns -1, which causes no content-type * header to be sent to the client. This would make sense for * a stream content of unknown or undefined length. If your * resource has a defined length you should override this * method and return that. * * @return The length of the resource in bytes. */ public long getContentLength() { return -1; } } }