kilim.http.HttpRequest.java Source code

Java tutorial

Introduction

Here is the source code for kilim.http.HttpRequest.java

Source

/* Copyright (c) 2006, Sriram Srinivasan
 *
 * You may distribute this software under the terms of the license 
 * specified in the file "License"
 */

package kilim.http;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;

import kilim.Pausable;
import kilim.nio.EndPoint;
import me.jor.util.Log4jUtil;

import org.apache.commons.logging.Log;

/**
 * This object encapsulates a bytebuffer (via HttpMsg). HttpRequestParser creates an instance of this object, but only
 * converts a few of the important fields into Strings; the rest are maintained as ranges (offset + length) in the
 * bytebuffer. Use {@link #getHeader(String)} to get the appropriate field.
 */
public class HttpRequest extends HttpMsg {
    private static final Log log = Log4jUtil.getLog(HttpRequest.class);
    // All the header related members of this class are initialized by the HttpRequestParser class.

    /**
     * The original header. All string variables that pertain to the message's header are either subsequences of this
     * header, or interned (all known keywords).
     */
    public String method;

    /**
     * The UTF8 decoded path from the HTTP header.
     */
    public String uriPath;

    public int nFields;
    /**
     * Keys present in the HTTP header
     */
    public String keys[];

    // range variables encode the offset and length within the header. The strings corresponding
    // to these variables are created lazily.
    public int versionRange;
    public int uriFragmentRange;
    public int queryStringRange;
    public int[] valueRanges;

    public int contentOffset;
    public int contentLength;

    /**
     * The read cursor, used in the read* methods.
     */
    public int iread;

    public HttpRequest() {
        keys = new String[5];
        valueRanges = new int[5];
    }

    /** 
     * Get the value for a given key
     * @param key
     * @return null if the key is not present in the header.
     */
    public String getHeader(String key) {
        for (int i = 0; i < nFields; i++) {
            if (key.equalsIgnoreCase(keys[i])) {
                return extractRange(valueRanges[i]);
            }
        }
        return ""; // no point returning null
    }

    /**
     * @return the query part of the URI. 
     */
    public String getQuery() {
        return extractRange(queryStringRange);
    }

    public String version() {
        return extractRange(versionRange);
    }

    public boolean keepAlive() {
        return isOldHttp() ? "Keep-Alive".equals(getHeader("Connection;"))
                : !("close".equals(getHeader("Connection")));
    }

    public KeyValues getQueryComponents() {
        String q = getQuery();
        int len = q.length();
        if (q == null || len == 0)
            return new KeyValues(0);

        int numPairs = 0;
        for (int i = 0; i < len; i++) {
            if (q.charAt(i) == '=')
                numPairs++;
        }
        KeyValues components = new KeyValues(numPairs);

        int beg = 0;
        String key = null;
        boolean url_encoded = false;
        for (int i = 0; i <= len; i++) {
            char c = (i == len) ? '&' // pretending there's an artificial marker at the end of the string, to capture
                    // the last component
                    : q.charAt(i);

            if (c == '+' || c == '%')
                url_encoded = true;
            if (c == '=' || c == '&') {
                String comp = q.substring(beg, i);
                if (url_encoded) {
                    try {
                        comp = URLDecoder.decode(comp, "UTF-8");
                    } catch (UnsupportedEncodingException ignore) {
                    }
                }
                if (key == null) {
                    key = comp;
                } else {
                    components.put(key, comp);
                    key = null;
                }
                beg = i + 1;
                url_encoded = false; // for next time
            }
        }
        return components;
    }

    public String uriFragment() {
        return extractRange(uriFragmentRange);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder(500);
        sb.append("method: ").append(method).append('\n').append("version: ").append(version()).append('\n')
                .append("path = ").append(uriPath).append('\n').append("uri_fragment = ").append(uriFragment())
                .append('\n').append("query = ").append(getQueryComponents()).append('\n');
        for (int i = 0; i < nFields; i++) {
            sb.append(keys[i]).append(": ").append(extractRange(valueRanges[i])).append('\n');
        }

        return sb.toString();
    }

    /**
     * @return true if version is 1.0 or earlier
     */
    public boolean isOldHttp() {
        final byte b1 = (byte) '1';
        int offset = versionRange >> 16;
        return (buffer.get(offset) < b1 || buffer.get(offset + 2) < b1);
    }

    /**
     * Clear the request object so that it can be reused for the next message. 
     */
    public void reuse() {
        method = null;
        uriPath = null;
        versionRange = 0;
        uriFragmentRange = queryStringRange = 0;
        contentOffset = 0;
        contentLength = 0;

        if (buffer != null) {
            buffer.clear();
        }
        for (int i = 0; i < nFields; i++) {
            keys[i] = null;
        }
        nFields = 0;
    }

    /*
     * Internal methods 
     */
    public void readFrom(EndPoint endpoint) throws Pausable, IOException {
        iread = 0;
        readHeader(endpoint);
        readBody(endpoint);
    }

    public void readHeader(EndPoint endpoint) throws Pausable, IOException {
        buffer = ByteBuffer.allocate(1024);
        int headerLength = 0;
        int n;
        do {
            n = readLine(endpoint); // includes 2 bytes for CRLF
            headerLength += n;
        } while (n > 2 || headerLength <= 2); // until blank line (CRLF), but just blank line is not enough.
        // dumpBuffer(buffer);
        HttpRequestParser.initHeader(this, headerLength);
        contentOffset = headerLength; // doesn't mean there's necessarily any content.
        String cl = getHeader("Content-Length");
        if (cl.length() > 0) {
            try {
                contentLength = Integer.parseInt(cl);
            } catch (NumberFormatException nfe) {
                throw new IOException("Malformed Content-Length hdr");
            }
        } else if ((getHeader("Transfer-Encoding").indexOf("chunked") >= 0)
                || (getHeader("TE").indexOf("chunked") >= 0)) {
            contentLength = -1;
        } else {
            contentLength = 0;
        }
    }

    public void dumpBuffer(ByteBuffer buffer) {
        byte[] ba = buffer.array();
        int len = buffer.position();
        StringBuilder print = new StringBuilder();
        for (int i = 0; i < len; i++) {
            print.append((char) ba[i]);
        }
        log.debug(print);
    }

    public void addField(String key, int valRange) {
        if (keys.length == nFields) {
            keys = (String[]) Utils.growArray(keys, 5);
            valueRanges = Utils.growArray(valueRanges, 5);
        }
        keys[nFields] = key;
        valueRanges[nFields] = valRange;
        nFields++;
    }

    // complement of HttpRequestParser.encodeRange
    public String extractRange(int range) {
        int beg = range >> 16;
        int end = range & 0xFFFF;
        return extractRange(beg, end);
    }

    public String extractRange(int beg, int end) {
        return new String(buffer.array(), beg, (end - beg));
    }

    /*
     * Read entire content into request's buffer
     */
    public void readBody(EndPoint endpoint) throws Pausable, IOException {
        iread = contentOffset;
        if (contentLength > 0) {
            fill(endpoint, contentOffset, contentLength);
            iread = contentOffset + contentLength;
        } else if (contentLength == -1) {
            // CHUNKED
            readAllChunks(endpoint);
        }
        readTrailers(endpoint);
    }

    public void readTrailers(EndPoint endpoint) {
    }

    /*
     * Read all chunks until  a chunksize of 0 is received, then consolidate the chunks into a single contiguous chunk.
     * At the end of this method, the entire content is available in the requests buffer, starting at contentOffset and
     * of length contentLength.
     */
    public void readAllChunks(EndPoint endpoint) throws IOException, Pausable {
        IntList chunkRanges = new IntList(); // alternate numbers in this list refer to the start and end offsets of chunks.
        do {
            int n = readLine(endpoint); // read chunk size text into buffer
            int beg = iread;
            int size = parseChunkSize(buffer, iread - n, iread); // Parse size in hex, ignore extension
            if (size == 0)
                break;
            // If the chunk has not already been read in, do so
            fill(endpoint, iread, size + 2 /*chunksize + CRLF*/);
            // record chunk start and end
            chunkRanges.add(beg);
            chunkRanges.add(beg + size); // without the CRLF
            iread += size + 2; // for the next round.
        } while (true);

        // / consolidate all chunkRanges
        if (chunkRanges.numElements == 0) {
            contentLength = 0;
            return;
        }
        contentOffset = chunkRanges.get(0); // first chunk's beginning
        int endOfLastChunk = chunkRanges.get(1); // first chunk's end

        byte[] bufa = buffer.array();
        for (int i = 2; i < chunkRanges.numElements; i += 2) {
            int beg = chunkRanges.get(i);
            int chunkSize = chunkRanges.get(i + 1) - beg;
            System.arraycopy(bufa, beg, bufa, endOfLastChunk, chunkSize);
            endOfLastChunk += chunkSize;
        }
        // TODO move all trailer stuff up
        contentLength = endOfLastChunk - contentOffset;

        // At this point, the contentOffset and contentLen give the entire content 
    }

    public static byte CR = (byte) '\r';
    public static byte LF = (byte) '\n';
    static final byte b0 = (byte) '0', b9 = (byte) '9';
    static final byte ba = (byte) 'a', bf = (byte) 'f';
    static final byte bA = (byte) 'A', bF = (byte) 'F';
    static final byte SEMI = (byte) ';';

    public static int parseChunkSize(ByteBuffer buffer, int start, int end) throws IOException {
        byte[] bufa = buffer.array();
        int size = 0;
        for (int i = start; i < end; i++) {
            byte b = bufa[i];
            if (b >= b0 && b <= b9) {
                size = size * 16 + (b - b0);
            } else if (b >= ba && b <= bf) {
                size = size * 16 + ((b - ba) + 10);
            } else if (b >= bA && b <= bF) {
                size = size * 16 + ((b - bA) + 10);
            } else if (b == CR || b == SEMI) {
                // SEMI-colon starts a chunk extension. We ignore extensions currently.
                break;
            } else {
                throw new IOException("Error parsing chunk size; unexpected char " + b + " at offset " + i);
            }
        }
        return size;
    }

    // topup if request's buffer doesn't have all the bytes yet.
    public void fill(EndPoint endpoint, int offset, int size) throws IOException, Pausable {
        int total = offset + size;
        int currentPos = buffer.position();
        if (total > buffer.position()) {
            buffer = endpoint.fill(buffer, (total - currentPos));
        }
    }

    public int readLine(EndPoint endpoint) throws IOException, Pausable {
        int ireadSave = iread;
        int i = ireadSave;
        while (true) {
            int end = buffer.position();
            byte[] bufa = buffer.array();
            for (; i < end; i++) {
                if (bufa[i] == CR) {
                    ++i;
                    if (i >= end) {
                        buffer = endpoint.fill(buffer, 1);
                        bufa = buffer.array(); // fill could have changed the buffer.
                        end = buffer.position();
                    }
                    if (bufa[i] != LF) {
                        throw new IOException("Expected LF at " + i);
                    }
                    ++i;
                    int lineLength = i - ireadSave;
                    iread = i;
                    return lineLength;
                }
            }
            buffer = endpoint.fill(buffer, 1); // no CRLF found. fill a bit more and start over.
        }
    }
}