All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.net.http.HttpRequest Maven / Gradle / Ivy

Go to download

A versatile Java(TM) library that implements many useful container and utility classes.

There is a newer version: 1.1.12
Show newest version

/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2012, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.net.http;

import static java.util.logging.Level.FINE;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.io.Multiplexer;
import de.unkrig.commons.io.PercentEncodingInputStream;
import de.unkrig.commons.io.PercentEncodingOutputStream;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;

/**
 * Representation of an HTTP request.
 */
public
class HttpRequest extends HttpMessage {

    private static final Charset CHARSET_UTF_8 = Charset.forName("UTF-8");

    private static final Charset CHARSET_ISO_8859_1 = Charset.forName("ISO-8859-1");

    private static final Pattern REQUEST_LINE_PATTERN = (
        Pattern.compile("(\\p{Alpha}+) ([^ ]+)(?: HTTP/(\\d+\\.\\d+))?")
    );

    private Method method;
    private String httpVersion;

    /**
     * Representation of the various HTTP methods.
     */
    public enum Method { GET, POST, HEAD, PUT }

    /**
     * Parses and returns one HTTP request from the given {@link InputStream}.
     */
    public static HttpRequest
    read(InputStream in) throws IOException, InvalidHttpMessageException {

        // Read and parse first request line.
        Method method;
        String httpVersion;
        URI    uri;
        {
            String requestLine = HttpMessage.readLine(in);
            LOGGER.fine(">>> " + requestLine);

            Matcher matcher = REQUEST_LINE_PATTERN.matcher(requestLine);
            if (!matcher.matches()) {
                LOGGER.warning("Invalid request line '" + requestLine + "'");
                throw new IOException("Invalid request line");
            }

            method = Method.valueOf(matcher.group(1));

            try {
                uri = new URI(matcher.group(2));
            } catch (URISyntaxException use) {
                throw new InvalidHttpMessageException(use);
            }

            httpVersion = matcher.group(3);
            if (httpVersion == null) httpVersion = "0.9";
        }

        return new HttpRequest(method, uri, httpVersion, in);
    }

    public
    HttpRequest(Method method, URI uri, String httpVersion, InputStream in) throws IOException {
        super(in, true, method == Method.POST || method == Method.PUT);
        this.method        = method;
        this.httpVersion   = httpVersion;
        this.uri           = uri;
        this.uriQueryValid = true;
        this.parameterList = null;
        this.parameterMap  = null;
    }

    public
    HttpRequest(Method method, URI uri, String httpVersion) {
        super(method == Method.POST || method == Method.PUT);
        this.method        = method;
        this.httpVersion   = httpVersion;
        this.uri           = uri;
        this.uriQueryValid = true;
        this.parameterList = null;
        this.parameterMap  = null;
    }

    /**
     * @return The HTTP request's {@link Method}
     */
    public Method
    getMethod() { return this.method; }

    /** @return This HTTP request's HTTP version, as given in the request line */
    public String
    getHttpVersion() { return this.httpVersion; }

    /** Query component is valid iff {@link #uriQueryValid}. */
    private URI     uri;
    private boolean uriQueryValid;

    /** {@code null} indicates it needs to be updated from the URI. */
    @Nullable private List> parameterList = new ArrayList>();

    /** {@code null} indicates it needs to be updated from the URI. */
    @Nullable private Map> parameterMap = new HashMap>();

    /** @return The URI of this HTTP request */
    public URI
    getUri() {
        if (!this.uriQueryValid) {
            if (this.parameterList != null) this.updateUriFromParameterList();
            this.uriQueryValid = true;
        }
        return this.uri;
    }

    /** Changes the URI of this HTTP request. */
    public final void
    setUri(URI uri) {
        this.uri           = uri;
        this.uriQueryValid = true;
        this.parameterList = null;
        this.parameterMap  = null;
    }

    /**
     * @return The parameters of this request, in order, as they exist in the body (POST, PUT) or in the query string
     *         (all other HTTP methods)
     */
    public List>
    getParameterList() throws IOException {

        if (this.parameterList == null) {
            this.updateParameterListFromQueryOrBody();
            this.parameterMap = null;
        }

        return Collections.unmodifiableList(this.parameterList);
    }

    /**
     * Changes this HTTP request's parameters.
     */
    public void
    setParameterList(Iterable> parameters) {

        List> pl = this.parameterList;
        if (pl == null) {
            pl = (this.parameterList = new ArrayList>());
        } else {
            pl.clear();
        }
        for (Entry e : parameters) {
            pl.add(entry(e.getKey(), e.getValue()));
        }
        this.uriQueryValid = false;
        this.parameterMap  = null;
    }

    /** @return The values of all parameters with the given {@code name} */
    @Nullable public String[]
    getParameter(String name) throws IOException {

        this.getParameterMap();

        List l = this.getParameterMap().get(name);
        return l == null ? null : l.toArray(new String[l.size()]);
    }

    /** Adds another parameter. */
    public void
    addParameter(String name, String value) throws IOException {
        this.addParameter(name, new String[] { value });
    }

    /** Adds a multi-value parameter. */
    public void
    addParameter(String name, String[] values) throws IOException {

        // Modify parameterList.
        {
            List> pl = this.getParameterList();
            for (String value : values) pl.add(entry(name, value));
        }

        // Modify parameterMap.
        {
            Map> pm = this.getParameterMap();
            List              l  = pm.get(name);
            if (l == null) {
                l = new ArrayList();
                pm.put(name, l);
            }
            for (String value : values) l.add(value);
        }

        // Invalidate uri.
        if (this.method != Method.POST && this.method != Method.PUT) this.uriQueryValid = false;
    }

    /** Removes all parameters with the given name and adds another parameter. */
    public void
    setParameter(String name, String value) throws IOException {
        this.setParameter(name, new String[] { value });
    }

    /** Removes all parameters with the given name and adds another multi-value parameter. */
    public void
    setParameter(String name, String[] values) throws IOException {

        this.getParameterMap();

        // Modify the parameterMap.
        {
            List l = this.getParameterMap().get(name);
            if (l == null) {
                this.addParameter(name, values);
                return;
            }
            l.clear();
            for (String value : values) l.add(value);
        }

        // Modify the parameterList.
        {
            List> pl = this.getParameterList();
            for (Iterator> it = pl.iterator(); it.hasNext();) {
                if (it.next().getKey().equals(name)) it.remove();
            }
            for (String value : values) pl.add(entry(name, value));
        }

        // Invalidate uri.
        if (this.method != Method.POST && this.method != Method.PUT) this.uriQueryValid = false;
    }

    private void
    updateParameterListFromQueryOrBody() throws IOException {

        String query;
        if (this.method == Method.POST || this.method == Method.PUT) {
            query = IoUtil.readAll(new InputStreamReader(
                this.removeBody().inputStream(),
                this.getCharset()
            ));
        } else {
            assert this.uriQueryValid;
            query = this.uri.getQuery();
        }

        List> pl = (this.parameterList = new ArrayList>());
        pl.addAll(decodeParameters(query));
    }

    private void
    updateUriFromParameterList() {
        List> pl = this.parameterList;
        assert pl != null;
        try {
            this.uri = new URI(
                this.uri.getScheme(),
                this.uri.getUserInfo(),
                this.uri.getHost(),
                this.uri.getPort(),
                this.uri.getPath(),
                encodeParameters(pl),
                this.uri.getFragment()
            );
        } catch (URISyntaxException use) {
            if (LOGGER.isLoggable(FINE)) LOGGER.log(FINE, "Updating URI", use);
        }
    }

    private Map>
    getParameterMap() throws IOException {
        Map> pm = this.parameterMap;
        if (pm != null) return pm;

        pm = (this.parameterMap = new HashMap>());

        for (Entry e : this.getParameterList()) {

            String key   = e.getKey();
            String value = e.getValue();

            List l = pm.get(key);
            if (l == null) {
                l = new ArrayList();
                pm.put(key, l);
            }
            l.add(value);
        }
        return pm;
    }

    /**
     *  {@code List>}
     *  

* is transformed into: *

* {@code a=b&c=d} */ @Nullable private static String encodeParameters(List> parameterList) { Iterator> it = parameterList.iterator(); if (!it.hasNext()) return null; ByteArrayOutputStream baos; try { baos = new ByteArrayOutputStream(); PercentEncodingOutputStream peos = new PercentEncodingOutputStream(baos); for (;;) { Entry e = it.next(); new OutputStreamWriter(peos, CHARSET_UTF_8).write(e.getKey()); peos.writeUnencoded('='); new OutputStreamWriter(peos, CHARSET_UTF_8).write(e.getValue()); if (!it.hasNext()) break; peos.writeUnencoded('&'); } } catch (IOException ioe) { if (LOGGER.isLoggable(FINE)) LOGGER.log(FINE, "Decoding parameters", ioe); return null; } @SuppressWarnings("deprecation") String result = new String(baos.toByteArray(), 0); return result; } @SuppressWarnings("deprecation") private static List> decodeParameters(@Nullable String s) throws IOException { if (s == null) return Collections.emptyList(); byte[] bytes; { int len = s.length(); bytes = new byte[len]; s.getBytes(0, len, bytes, 0); // <= Deprecated, but exactly what we want. } return decodeParameters(bytes); } private static List> decodeParameters(byte[] bytes) throws IOException { int len = bytes.length; List> result = new ArrayList>(); for (int off = 0; off < len;) { int to; for (to = off; to < len && bytes[to] != '=' && bytes[to] != '&'; to++); String key = readAll(new PercentEncodingInputStream(new ByteArrayInputStream(bytes, off, to - off))); if (to == len) { result.add(entry(key, "")); break; } else if (bytes[to] == '&') { result.add(entry(key, "")); off = to + 1; } else { off = to + 1; for (to = off; to < len && bytes[to] != '&'; to++); String value = readAll(new PercentEncodingInputStream(new ByteArrayInputStream(bytes, off, to - off))); result.add(entry(key, value)); if (to == len) break; off = to + 1; } } return result; } @NotNullByDefault(false) private static Entry entry(final String key, final String value) { return new Map.Entry() { @Override public String getKey() { return key; } @Override public String getValue() { return value; } @Override public String setValue(String value) { throw new UnsupportedOperationException("setValue"); } }; } private static String readAll(InputStream is) throws IOException { // try { // return IoUtil.readAll(new InputStreamReader(is, CHARSET_UTF_8)); // } catch (MalformedInputException mie) { return IoUtil.readAll(new InputStreamReader(is, CHARSET_ISO_8859_1)); // } } /** Changes the HTTP method of this request. */ public void setMethod(Method method) { this.method = method; } /** Changes the HTTP version of this request. */ public void setHttpVersion(String httpVersion) { this.httpVersion = httpVersion; } /** Writes this HTTP request to the given {@link OutputStream}. */ public void write(OutputStream out) throws IOException { { String requestLine = this.method + " " + this.uri; if (!"0.9".equals(this.httpVersion)) requestLine += " HTTP/" + this.httpVersion; LOGGER.fine("<<< " + requestLine); Writer w = new OutputStreamWriter(out, Charset.forName("ASCII")); w.write(requestLine + "\r\n"); w.flush(); } this.writeHeadersAndBody("<<< ", out); } /** * Reads one HTTP request from {@code in} through the {@code multiplexer} and passes it to the {@code * requestConsumer}. */ public static void read( final ReadableByteChannel in, final Multiplexer multiplexer, final ConsumerWhichThrows requestConsumer ) throws IOException { ConsumerWhichThrows requestLineConsumer = new ConsumerWhichThrows() { @Override public void consume(String requestLine) throws IOException { final HttpRequest.Method method; final URI uri; final String httpVersion; { Matcher matcher = REQUEST_LINE_PATTERN.matcher(requestLine); if (!matcher.matches()) { LOGGER.warning("Invalid request line '" + requestLine + "'"); throw new InvalidHttpMessageException("Invalid request line"); } method = Method.valueOf(matcher.group(1)); try { uri = new URI(matcher.group(2)); } catch (URISyntaxException use) { throw new InvalidHttpMessageException(use); } httpVersion = matcher.group(3) == null ? "0.9" : matcher.group(3); } HttpMessage.readHeaders(in, multiplexer, new ConsumerWhichThrows, IOException>() { @Override public void consume(List headers) throws IOException { final HttpRequest httpRequest = new HttpRequest(method, uri, httpVersion); if (method == Method.POST || method == Method.PUT) { httpRequest.readBody(in, multiplexer, new RunnableWhichThrows() { @Override public void run() throws IOException { requestConsumer.consume(httpRequest); } }); } else { requestConsumer.consume(httpRequest); } } }); } }; readLine(in, multiplexer, requestLineConsumer); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy