de.unkrig.commons.net.http.HttpMessage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of de-unkrig-commons Show documentation
Show all versions of de-unkrig-commons Show documentation
A versatile Java(TM) library that implements many useful container and utility classes.
/*
* 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:
*
* -
* 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)}
*
*
* -
* Call exactly one of the following methods:
*
* - {@link #string(Charset)}
*
- {@link #inputStream()}
*
- {@link #write(OutputStream)}
*
- {@link #dispose()}
*
*
* - 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);
}
});
}
}