de.unkrig.commons.net.http.HttpRequest Maven / Gradle / Ivy
Show all versions of de-unkrig-commons Show documentation
/*
* 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);
}
}