org.npr.android.test.HttpServer.java Source code

Java tutorial

Introduction

Here is the source code for org.npr.android.test.HttpServer.java

Source

// 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;
        }

    }

}