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

de.unkrig.commons.net.http.HttpMessage 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.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
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.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import de.unkrig.commons.io.FixedLengthInputStream;
import de.unkrig.commons.io.FixedLengthOutputStream;
import de.unkrig.commons.io.HexOutputStream;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.io.Multiplexer;
import de.unkrig.commons.io.WriterOutputStream;
import de.unkrig.commons.io.XMLFormatterWriter;
import de.unkrig.commons.lang.protocol.ConsumerUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil.Produmer;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.net.http.io.ChunkedInputStream;
import de.unkrig.commons.net.http.io.ChunkedOutputStream;
import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.util.logging.LogUtil;

/**
 * Representation of an HTTP request or response.
 */
public
class HttpMessage {

    // CHECKSTYLE LineLength:OFF
    // CHECKSTYLE JavadocVariable:OFF
    static final Logger          LOGGER         = Logger.getLogger(HttpMessage.class.getName());
    private static final Pattern HEADER_PATTERN = Pattern.compile("([ -~&&[^()<>@,;:\\\\/\\[\\]?={} \\t]]+)\\s*:\\s*(.*?)\\s*");
    // CHECKSTYLE LineLength:ON
    // CHECKSTYLE JavadocVariable:ON

    private static final DateFormat[] HEADER_DATE_FORMATS = {

        // RFC 822, updated by RFC 1123 (preferred format):
        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH),

        // RFC 850, obsoleted by RFC 1036:
        new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss 'GMT'", Locale.ENGLISH),

        // ANSI C's asctime() format:
        new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.ENGLISH),
    };
    static { for (DateFormat df : HEADER_DATE_FORMATS) df.setTimeZone(TimeZone.getTimeZone("UTC")); }

    private final List headers = new ArrayList();

    /**
     * The life cycle of a {@link Body} is as follows:
     * 
    *
  1. * Create the object by calling one of the following methods: *
      *
    • {@link HttpMessage#body(String, Charset)} *
    • {@link HttpMessage#body(InputStream)} *
    • {@link HttpMessage#body(File)} *
    • {@link HttpMessage#body(ConsumerWhichThrows)} *
    *
  2. *
  3. * Call exactly one of the following methods: *
      *
    • {@link #string(Charset)} *
    • {@link #inputStream()} *
    • {@link #write(OutputStream)} *
    • {@link #dispose()} *
    *
  4. *
  5. Call {@link #dispose()} as many times as you want *
* Otherwise, a resource leak will occur. */ public interface Body { /** * @return The contents of the body of an {@link HttpMessage} as a string * @see Body */ String string(Charset charset) throws IOException; /** * The caller is responsible for closing the returned {@link InputStream}. * * @return Produces the contents of the body of an {@link HttpMessage} as a stream of bytes * @see Body */ InputStream inputStream() throws IOException; /** * Writes the contents of the body to the given stream. * * @see Body */ void write(OutputStream stream) throws IOException; /** * Releases any resources associated with this object. * * @see Body */ void dispose(); } /** * Representation of a non-existent HTTP request/response body. */ public static final Body NO_BODY = new Body() { @Override public String string(Charset charset) { throw new UnsupportedOperationException("NO_BODY"); } @Override public InputStream inputStream() { throw new UnsupportedOperationException("NO_BODY"); } @Override public void write(OutputStream stream) { throw new UnsupportedOperationException("NO_BODY"); } @Override public void dispose() {} }; /** * Representation of an empty HTTP request/response body. */ public static final Body EMPTY_BODY = new Body() { @Override public String string(Charset charset) { return ""; } @Override public InputStream inputStream() { return IoUtil.EMPTY_INPUT_STREAM; } @Override public void write(OutputStream stream) {} @Override public void dispose() {} }; /** * {@code null} iff this message does not have a body. */ private Body body = NO_BODY; /** * Constructor for outgoing messages. */ protected HttpMessage(boolean hasBody) { this.body = hasBody ? EMPTY_BODY : NO_BODY; } /** * Constructor for incoming messages. *

* Notice that in will be read and closed when the body of this message is processed or disposed * (see {@link Body}). *

*/ protected HttpMessage(InputStream in, boolean hasHeaders, boolean hasBody) throws IOException { // Read the headers. if (hasHeaders) { String line = HttpMessage.readLine(in); while (line.length() > 0) { String headerLine = line; for (;;) { line = HttpMessage.readLine(in); if (line.length() == 0 || " \t".indexOf(line.charAt(0)) == -1) break; headerLine += "\r\n" + line; } LOGGER.fine(">>> " + headerLine); Matcher matcher = HEADER_PATTERN.matcher(headerLine); if (!matcher.matches()) throw new IOException("Invalid HTTP header line '" + headerLine + "'"); this.headers.add(new MessageHeader(matcher.group(1), matcher.group(2))); } } // Read the body. if (hasBody) { final Produmer rawByteCount = ConsumerUtil.store(); final Produmer decodedByteCount = ConsumerUtil.store(); // Determine the raw message body size. if (LOGGER.isLoggable(FINE)) { in = IoUtil.wye(in, IoUtil.lengthWritten(ConsumerUtil.cumulate(rawByteCount, 0))); } // Insert a logging Wye-Reader if logging is enabled. if (LOGGER.isLoggable(FINE)) { boolean isXml = false; LOGGER.fine("Reading message body"); String contentType = this.getHeader("Content-Type"); if (contentType != null) { ParametrizedHeaderValue phv = new ParametrizedHeaderValue(contentType); if ("text/xml".equalsIgnoreCase(phv.getToken())) isXml = true; } Writer logWriter = LogUtil.logWriter(LOGGER, FINE, ">>> "); in = IoUtil.wye(in, ( isXml ? new WriterOutputStream(new XMLFormatterWriter(logWriter)) : new HexOutputStream(logWriter) )); } // Process "Content-Length" and "Transfer-Encoding" headers. { long cl = this.getLongHeader("Content-Length"); if (cl != -1) { in = new FixedLengthInputStream(in, cl); } else { String tes = this.getHeader("Transfer-Encoding"); if (tes != null) { if (!"chunked".equalsIgnoreCase(tes)) { throw new IOException("Message with unsupported transfer encoding '" + tes + "' received"); } LOGGER.fine("Reading message with chunked contents"); in = new ChunkedInputStream(in); } else { LOGGER.fine("Reading message with streaming contents"); ; } } } // Process "Content-Encoding" header. if ("gzip".equalsIgnoreCase(this.getHeader("Content-Encoding"))) { in = new GZIPInputStream(in); } // Track the decoded message body size. if (LOGGER.isLoggable(FINE)) { in = IoUtil.wye(in, IoUtil.lengthWritten(ConsumerUtil.cumulate(decodedByteCount, 0))); } // Report on the raw and on the decoded message body size. if (LOGGER.isLoggable(FINE)) { in = IoUtil.onEndOfInput(in, new Runnable() { @Override public void run() { LOGGER.fine( "Message body size was " + NumberFormat.getInstance().format(rawByteCount.produce()) + " (raw) " + NumberFormat.getInstance().format(decodedByteCount.produce()) + " (decoded)" ); } }); } this.setBody(HttpMessage.body(in)); } } /** * Appends another HTTP header. */ public void addHeader(String name, String value) { this.headers.add(new MessageHeader(name, value)); } /** * Appends another HTTP header. */ public void addHeader(String name, int value) { this.addHeader(name, Integer.toString(value)); } /** * Appends another HTTP header. */ public void addHeader(String name, long value) { this.addHeader(name, Long.toString(value)); } /** * Appends another HTTP header. */ public void addHeader(String name, Date value) { this.addHeader(name, HEADER_DATE_FORMATS[0].format(value)); } /** * Changes the value of the first header with the given {@code name}. */ public void setHeader(String name, String value) { for (MessageHeader header : this.headers) { if (header.getName().equalsIgnoreCase(name)) { header.setValue(value); return; } } this.headers.add(new MessageHeader(name, value)); } /** * Changes the value of the first header with the given {@code name}. */ public void setHeader(String name, int value) { this.setHeader(name, Integer.toString(value)); } /** * Changes the value of the first header with the given {@code name}. */ public void setHeader(String name, long value) { this.setHeader(name, Long.toString(value)); } /** * Changes the value of the first header with the given {@code name}. */ public void setHeader(String name, Date value) { this.setHeader(name, HEADER_DATE_FORMATS[0].format(value)); } /** * Remove all headers with the given {@code name}. */ public void removeHeader(String name) { for (Iterator it = this.headers.iterator(); it.hasNext();) { MessageHeader h = it.next(); if (h.getName().equalsIgnoreCase(name)) it.remove(); } } /** * @return the value of the first message header with that {@code name}, or {@code null} */ @Nullable public final String getHeader(String name) { for (MessageHeader mh : this.headers) { if (mh.getName().equalsIgnoreCase(name)) return mh.getValue(); } return null; } /** * @return {@code -1} iff a header with this name does not exist */ public int getIntHeader(String name) throws IOException { String s = this.getHeader(name); if (s != null) { try { return Integer.parseInt(s); } catch (NumberFormatException nfe) { // SUPPRESS CHECKSTYLE AvoidHidingCause throw new IOException("'" + name + "' message header has invalid value '" + s + "'"); } } return -1; } /** * @return {@code -1L} iff a header with this name does not exist */ public final long getLongHeader(String name) throws IOException { String s = this.getHeader(name); if (s != null) { try { return Long.parseLong(s); } catch (NumberFormatException nfe) { // SUPPRESS CHECKSTYLE HidingCause throw new IOException("'" + name + "' message header has invalid value '" + s + "'"); } } return -1L; } /** * @return {@code null} iff a header with this name does not exist */ @Nullable public Date getDateHeader(String name) throws IOException { String s = this.getHeader(name); if (s == null) return null; for (DateFormat df : HEADER_DATE_FORMATS) { try { return df.parse(s); } catch (ParseException pe) {} } throw new IOException("Cannot parse date header '" + name + ": " + s + "'"); } /** * @return the values of the message headers with that {@code name}, or an empty array */ public String[] getHeaders(String name) { List values = new ArrayList(); for (MessageHeader mh : this.headers) { if (mh.getName().equalsIgnoreCase(name)) values.add(mh.getValue()); } return values.toArray(new String[values.size()]); } /** * The returned list is backed by the {@link HttpMessage}! */ public List getHeaders() { return this.headers; } /** * Removes the body from this {@link HttpMessage} for analysis or modification. It can later be re-attached to * the same (or a different) {@link HttpMessage} through {@link #setBody(Body)}. */ public Body removeBody() { Body result = this.body; this.body = NO_BODY; return result; } /** * @see Body */ public static Body body(final String text, final Charset charset) { return new Body() { @Nullable String text2 = text; @Override public String string(Charset charset) { String result = this.text2; if (result == null) throw new IllegalStateException(); this.text2 = null; return result; } @Override public InputStream inputStream() throws IOException { if (this.text2 == null) throw new IllegalStateException(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); new OutputStreamWriter(baos, charset).write(this.text2); this.text2 = null; return new ByteArrayInputStream(baos.toByteArray()); } @Override public void write(OutputStream stream) throws IOException { if (this.text2 == null) throw new IllegalStateException(); { OutputStreamWriter w = new OutputStreamWriter(stream, charset); w.write(this.text2); w.flush(); } this.text2 = null; } @Override public void dispose() { this.text2 = null; } }; } /** * The {@link InputStream} will be closed by {@link Body#string(Charset)}, {@link Body#inputStream()}, {@link * Body#write(OutputStream)}, {@link Body#dispose()} and {@link Body#dispose()}. * * @see Body */ public static Body body(final InputStream in) { return new Body() { /** * {@code Null} means that that one of {@link Body#string(Charset)}, {@link Body#inputStream()}, {@link * Body#write(OutputStream)}, {@link Body#dispose()} or {@link Body#dispose()} has been called before. */ @Nullable InputStream in2 = in; @Override public String string(Charset charset) throws IOException { InputStream in3 = this.in2; if (in3 == null) throw new IllegalStateException(); final String result = IoUtil.readAll(new InputStreamReader(in3, charset)); try { in3.close(); } catch (Exception e) {} this.in2 = null; return result; } @Override public InputStream inputStream() { InputStream in3 = this.in2; if (in3 == null) throw new IllegalStateException(); this.in2 = null; return in3; } @Override public void write(OutputStream stream) throws IOException { InputStream in3 = this.in2; if (in3 == null) throw new IllegalStateException(); IoUtil.copy(in3, stream); try { in3.close(); } catch (Exception e) {} this.in2 = null; } @Override public void dispose() { InputStream in3 = this.in2; if (in3 != null) { try { IoUtil.skipAll(in3); } catch (Exception e) {} try { in3.close(); } catch (Exception e) {} } this.in2 = null; } }; } /** * @see Body */ public static Body body(final File file) throws FileNotFoundException { return HttpMessage.body(new FileInputStream(file)); } /** * @see Body */ public static Body body(final ConsumerWhichThrows writer) { return new Body() { @Nullable ConsumerWhichThrows writer2 = writer; @Override public String string(Charset charset) throws IOException { ConsumerWhichThrows w3 = this.writer2; if (w3 == null) throw new IllegalStateException(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); w3.consume(baos); this.writer2 = null; return IoUtil.readAll(new InputStreamReader(new ByteArrayInputStream(baos.toByteArray()), charset)); } @Override public InputStream inputStream() throws IOException { ConsumerWhichThrows w3 = this.writer2; if (w3 == null) throw new IllegalStateException(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); w3.consume(baos); this.writer2 = null; return new ByteArrayInputStream(baos.toByteArray()); } @Override public void write(OutputStream stream) throws IOException { ConsumerWhichThrows w3 = this.writer2; if (w3 == null) throw new IllegalStateException(); w3.consume(stream); this.writer2 = null; } @Override public void dispose() { this.writer2 = null; } }; } /** * Disposes the current body of this message and adopts the given {@link Body} object as the new body. */ public void setBody(Body body) { this.body.dispose(); this.body = body; } /** * Determines the charset from the "Content-Type" header. */ public Charset getCharset() { String ct = this.getHeader("Content-Type"); if (ct != null) { ParametrizedHeaderValue phv = new ParametrizedHeaderValue(ct); String token = phv.getToken(); if (token.startsWith("text/") || "application/x-www-form-urlencoded".equalsIgnoreCase(token)) { String charsetName = phv.getParameter("charset"); if (charsetName != null) { try { return Charset.forName(charsetName); } catch (IllegalCharsetNameException ncsne) { ; } } } } return DEFAULT_CHARSET; } private static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); /** * Writes this message's headers and body to the given {@link OutputStream}. Also closes the {@link OutputStream} * iff there is neither a "Content-Length:" header, nor a "Transfer-Encoding: chunked" header, nor the message * is very small. */ protected void writeHeadersAndBody(final String prefix, final OutputStream out) throws IOException { if (this.body == NO_BODY) { this.writeHeaders(prefix, out); return; } // Check for "chunked" transfer encoding. { String tes = this.getHeader("Transfer-Encoding"); if (tes != null) { if (!"chunked".equalsIgnoreCase(tes)) { throw new IOException("Message with unsupported transfer encoding '" + tes + "' received"); } LOGGER.fine("Writing message with chunked contents"); // Chunked transfer encoding. this.writeHeaders(prefix, out); OutputStream cos = new ChunkedOutputStream(IoUtil.unclosableOutputStream(out)); this.writeBody(prefix, cos); cos.close(); return; } } // Check for a "Content-Length" header. { long contentLength = this.getLongHeader("Content-Length"); if (contentLength >= 0L) { // Content length known. this.writeHeaders(prefix, out); FixedLengthOutputStream flos = new FixedLengthOutputStream( IoUtil.unclosableOutputStream(out), contentLength ); this.writeBody(prefix, flos); flos.close(); return; } } // The message has neither a header "Transfer-Encoding: chunked" nor a "Content-Length" header, so the length // of the message is determined by closing the connection. Since this is terribly inefficient, an attempt is // made to measure the length of the body if it is small. final byte[] buffer = new byte[4000]; final int[] count = new int[1]; this.writeBody(prefix, new OutputStream() { @Override public void write(int b) throws IOException { this.write(new byte[] { (byte) b }, 0, 1); } @NotNullByDefault(false) @Override public void write(byte[] b, int off, int len) throws IOException { if (count[0] == -1) { out.write(b, off, len); } else if (count[0] + len > buffer.length) { HttpMessage.this.writeHeaders(prefix, out); out.write(buffer, 0, count[0]); count[0] = -1; out.write(b, off, len); } else { System.arraycopy(b, off, buffer, count[0], len); count[0] += len; } } }); if (count[0] == -1L) { // Unstreaming failed; nothing left to be done. out.close(); return; } this.setHeader("Content-Length", count[0]); this.writeHeaders(prefix, out); out.write(buffer, 0, count[0]); } /** * Writes the body of this message synchronously to the given {@link OutputStream}. */ private void writeBody(String prefix, OutputStream out) throws IOException { // Check "Content-Encoding: gzip" GZIPOutputStream finishable = null; if ("gzip".equalsIgnoreCase(this.getHeader("Content-Encoding"))) { LOGGER.fine(prefix + "GZIP-encoded contents"); out = (finishable = new GZIPOutputStream(out)); } final boolean isXml; { String ct = this.getHeader("Content-Type"); isXml = ct != null && ct.indexOf("text/xml") != -1; } if (LOGGER.isLoggable(FINE)) { LOGGER.fine(prefix + "Writing message body:"); Writer lw = LogUtil.logWriter(LOGGER, FINE, prefix); out = IoUtil.tee(out, ( isXml ? new WriterOutputStream(new XMLFormatterWriter(lw)) : new HexOutputStream(lw) )); } this.body.write(out); if (finishable != null) finishable.finish(); out.flush(); } private void writeHeaders(String prefix, OutputStream out) throws IOException { // Headers and blank line. { Writer w = new OutputStreamWriter(out, Charset.forName("ASCII")); for (MessageHeader header : this.getHeaders()) { LOGGER.fine(prefix + header.getName() + ": " + header.getValue()); w.write(header.getName() + ": " + header.getValue() + "\r\n"); } w.write("\r\n"); w.flush(); } } /** * @return the line read, excluding the trailing CRLF */ public static String readLine(InputStream in) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); for (;;) { int c = in.read(); if (c == -1) throw new EOFException(); if (c == '\r') { c = in.read(); if (c != '\n') throw new IOException("LF instead of " + c + " expected after CR"); in.available(); return new String(baos.toByteArray(), "ISO-8859-1"); } baos.write(c); } } /** * Reads one HTTP request from {@code in} through the {@code multiplexer} and passes it to the {@code * requestConsumer}. */ public static void readLine( final ReadableByteChannel in, final Multiplexer multiplexer, final ConsumerWhichThrows lineConsumer ) throws IOException { RunnableWhichThrows lineParser = new RunnableWhichThrows() { final ByteBuffer buffer = ByteBuffer.allocate(1); final ByteArrayOutputStream line = new ByteArrayOutputStream(); int state; @Override public void run() throws IOException { this.buffer.rewind(); in.read(this.buffer); byte b = this.buffer.get(0); switch (this.state) { case 0: // Start. if (b == '\r') { this.state = 1; break; } this.line.write(b); break; case 1: // After CR if (b != '\n') { throw new InvalidHttpMessageException( "HTTP header line: CR is not followed by LF, but '" + (0xff & b) + "'" ); } lineConsumer.consume(new String(this.line.toByteArray(), "ISO-8859-1")); return; } multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, this); } }; multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, lineParser); } /** * Reads HTTP headers up to and including the terminating empty line. */ public static void readHeaders( final ReadableByteChannel in, final Multiplexer multiplexer, final ConsumerWhichThrows, IOException> consumer ) throws IOException { HttpMessage.readLine(in, multiplexer, new ConsumerWhichThrows() { @Nullable String headerLine; final List headers = new ArrayList(); @Override public void consume(String line) throws IOException { if (" \t".indexOf(line.charAt(0)) != -1) { if (this.headerLine == null) { throw new InvalidHttpMessageException("Unexpected leading continuation line '" + line + "'"); } this.headerLine += "\r\n" + line; HttpMessage.readLine(in, multiplexer, this); return; } LOGGER.fine(">>> " + this.headerLine); Matcher matcher = HEADER_PATTERN.matcher(this.headerLine); if (!matcher.matches()) { throw new InvalidHttpMessageException("Invalid HTTP header line '" + this.headerLine + "'"); } this.headers.add(new MessageHeader(matcher.group(1), matcher.group(2))); if (line.length() == 0) { consumer.consume(this.headers); return; } this.headerLine = line; HttpMessage.readLine(in, multiplexer, this); } }); } /** * Reads the body contents of this message into a buffer (depending on the 'Content-Length' and 'Transfer-Encoding' * headers). */ protected void readBody( ReadableByteChannel in, Multiplexer multiplexer, final RunnableWhichThrows finished ) throws IOException { final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); final RunnableWhichThrows runnable = new RunnableWhichThrows() { @Override public void run() throws IOException { InputStream in = new ByteArrayInputStream(buffer.toByteArray()); // Process "Content-Encoding" header. if ("gzip".equalsIgnoreCase(HttpMessage.this.getHeader("Content-Encoding"))) { LOGGER.fine("GZIP-encoded content"); in = new GZIPInputStream(in); } if (LOGGER.isLoggable(FINE)) { boolean isXml = false; LOGGER.fine("Reading message body"); String contentType = HttpMessage.this.getHeader("Content-Type"); if (contentType != null) { ParametrizedHeaderValue phv = new ParametrizedHeaderValue(contentType); if ("text/xml".equalsIgnoreCase(phv.getToken())) isXml = true; } Writer logWriter = LogUtil.logWriter(LOGGER, FINE, ">>> "); in = IoUtil.wye(in, ( isXml ? new WriterOutputStream(new XMLFormatterWriter(logWriter)) : new HexOutputStream(logWriter) )); } HttpMessage.this.setBody(HttpMessage.body(in)); finished.run(); } }; // Read the body contents. { long cl = this.getLongHeader("Content-Length"); if (cl != -1) { HttpMessage.read(in, multiplexer, cl, buffer, runnable); } else { String tes = this.getHeader("Transfer-Encoding"); if (tes != null) { if (!"chunked".equalsIgnoreCase(tes)) { throw new IOException("Message with unsupported transfer encoding '" + tes + "' received"); } LOGGER.fine("Reading chunked contents"); HttpMessage.readChunked(in, multiplexer, buffer, runnable); } else { LOGGER.fine("Reading streaming contents"); HttpMessage.read(in, multiplexer, buffer, runnable); } } } } /** * Reads a chunked message body from {@code in} into the {@code buffer} and runs {@code finished}. */ private static void readChunked( final ReadableByteChannel in, final Multiplexer multiplexer, final OutputStream buffer, final RunnableWhichThrows finished ) throws IOException { new RunnableWhichThrows() { @Override public void run() throws IOException { final RunnableWhichThrows chunkReader = this; HttpMessage.readLine(in, multiplexer, new ConsumerWhichThrows() { @Override public void consume(String line) throws IOException { // Ignore the blank line between chunks. if (line.length() == 0) { HttpMessage.readLine(in, multiplexer, this); return; } // Strip the chunk extension. { int idx = line.indexOf(';'); if (idx != -1) line = line.substring(0, idx); } // Parse and validate the chunk size. long available; try { available = Long.parseLong(line, 16); } catch (NumberFormatException nfe) { // SUPPRESS CHECKSTYLE HidingCause throw new IOException("Invalid chunk size field '" + line + "'"); } if (available < 0) throw new IOException("Negative chunk size field '" + line + "'"); // Last chunk? if (available == 0L) { finished.run(); return; } // Read chunk contents and then the next chunk header. HttpMessage.read(in, multiplexer, available, buffer, chunkReader); } }); } }.run(); } /** * Reads exactly bytes from {@code in} until end-of-input into the {@code buffer} and then runs {@code finished}. */ private static void read( final ReadableByteChannel in, final Multiplexer multiplexer, final OutputStream os, final RunnableWhichThrows finished ) throws IOException { multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, new RunnableWhichThrows() { final ByteBuffer buffer = ByteBuffer.allocate(8192); @Override public void run() throws IOException { this.buffer.rewind(); int r = in.read(this.buffer); if (r == -1) { finished.run(); return; } os.write(this.buffer.array(), 0, r); multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, this); } }); } /** * Reads exactly {@code n} bytes from {@code in} into the {@code buffer} and then runs {@code finished}. */ private static void read( final ReadableByteChannel in, final Multiplexer multiplexer, final long n, final OutputStream os, final RunnableWhichThrows finished ) throws IOException { if (n == 0L) { finished.run(); return; } multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, new RunnableWhichThrows() { int count = (int) n; final ByteBuffer buffer = ByteBuffer.allocate(8192); @Override public void run() throws IOException { this.buffer.rewind(); this.buffer.limit(Math.min(this.count, this.buffer.capacity())); int r = in.read(this.buffer); if (r == -1) throw new EOFException(); os.write(this.buffer.array(), 0, r); this.count -= r; if (this.count == 0) { finished.run(); return; } multiplexer.register((SelectableChannel) in, SelectionKey.OP_READ, this); } }); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy