/*
* E-XML Library: For XML, XML-RPC, HTTP, and related.
* Copyright (C) 2002-2008 Elias Ross
*
* genman@noderunner.net
* http://noderunner.net/~genman
*
* 1025 NE 73RD ST
* SEATTLE WA 98115
* USA
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* $Id$
*/
package net.noderunner.http;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Contains utility functions for common HTTP I/O tasks.
*/
public class HttpUtil
{
static final String CRLF = "\r\n";
private static byte junk[] = null;
private static final BitSet noencode;
private static final byte hex[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'};
static {
noencode = new BitSet(256);
for (int i = 'a'; i <= 'z'; i++)
noencode.set(i);
for (int i = 'A'; i <= 'Z'; i++)
noencode.set(i);
for (int i = '0'; i <= '9'; i++)
noencode.set(i);
noencode.set('-');
noencode.set('_');
noencode.set('.');
noencode.set('*');
}
/**
* Cannot be instantiated.
*/
private HttpUtil() {
}
/**
* Reads an input stream until EOF.
* Returns the number of bytes read.
*/
public static int readFully(InputStream stream)
throws IOException
{
if (stream == null)
throw new IllegalArgumentException("Null stream");
// not synchronized, but not a big deal
if (junk == null)
junk = new byte[1024];
int total = 0;
int got = 0;
while (true) {
got = stream.read(junk, 0, junk.length);
if (got == -1)
return total;
total += got;
}
}
/**
* Reads an input stream until EOF.
* Returns the bytes read.
*/
public static byte[] read(InputStream stream)
throws IOException
{
byte b[] = new byte[128];
ByteArrayOutputStream bo = new ByteArrayOutputStream();
int got = 0;
while (true) {
got = stream.read(b, 0, b.length);
if (got == -1)
return bo.toByteArray();
bo.write(b, 0, got);
}
}
/**
* Returns a character reader for reading HTTP data.
* Returns either an <code>InputStreamReader</code>, wrapping
* either a {@link ChunkedInputStream} or
* {@link LimitedInputStream}, based on the supplied headers.
* If the headers did not indicate data was being sent, returns
* an input stream for reading the rest of the document.
*/
public static InputStream wrapInputStream(InputStream stream, MessageHeaders hl)
throws IOException
{
if (stream == null)
throw new IllegalArgumentException("Null stream");
if (hl == null)
throw new IllegalArgumentException("Null headers");
boolean chunked = hl.contains(MessageHeader.MH_TRANSFER_ENCODING_CHUNKED);
if (chunked)
return new ChunkedInputStream(stream);
String s = hl.getFieldContent(MessageHeader.FN_CONTENT_LENGTH);
if (s == null)
return stream;
try {
int length = Integer.parseInt(s);
return new LimitedInputStream(stream, length);
} catch (NumberFormatException e) {
throw new HttpException("content-length not a number:" + s);
}
}
/**
* Returns a new input stream delegating to the given input stream
* that does nothing when closed.
*/
public static InputStream uncloseableInputStream(final InputStream stream)
throws IOException
{
return new FilterInputStream(stream) {
@Override
public void close() throws IOException {
}
};
}
/**
* Returns a single line from a 8-bit <code>InputStream</code>
* (which is assumed to be ASCII).
*/
public static String readHttpLine(InputStream in)
throws IOException
{
StringBuilder sb = new StringBuilder();
int c = 0;
while (true) {
c = in.read();
if (c == -1)
throw new EOFException("EOF processing HTTP request");
if (c == '\n')
break;
if (c == '\r') {
c = in.read();
if (c != '\n')
throw new HttpException("Expected LN character reading HTTP information");
break;
}
sb.append((char)c);
if (sb.length() > 1024)
throw new HttpException("Header line too long");
}
return sb.toString();
}
/**
* URL encodes a series of parameters.
* Names are followed by their value in the array. For example:
* <pre>
* byte[] buf;
* buf = HttpUtil.urlEncode(new String[] { "name1", "value1", "name2", "value2" });
* </pre>
* results in the byte array:
* <pre>
* "name1=value1&name2=value2"
* </pre>
* Strings are converted to bytes using the given encoding.
* @param nvPairs array of name-value pairs, must be
* even in length
* @param encoding Java encoding, or null to use the default encoding
* @return a byte array which can be used in {@link EasyHttpClient#doPostUrlEncoded}
* @throws UnsupportedEncodingException if the encoding is invalid
*/
public static byte[] urlEncode(String[] nvPairs, String encoding)
throws UnsupportedEncodingException
{
if (nvPairs == null)
throw new IllegalArgumentException("Null array");
if (nvPairs.length % 2 != 0)
throw new IllegalArgumentException("Odd length array");
ByteArrayOutputStream bb;
bb = new ByteArrayOutputStream(16 * nvPairs.length);
for (int i = 0; i < nvPairs.length; i += 2)
appendNV(bb, nvPairs[i], nvPairs[i + 1], encoding);
return bb.toByteArray();
}
/**
* URL encodes a series of parameters, using the
* Strings are converted to the default platform encoding.
*/
public static byte[] urlEncode(String[] nvPairs) {
try {
return urlEncode(nvPairs, null);
} catch (UnsupportedEncodingException e) {
throw new Error("should not be here");
}
}
/**
* URL encodes a single value, writing its value to
* an output stream.
*/
public static void urlEncode(ByteArrayOutputStream os, byte[] buf)
{
for (int i = 0; i < buf.length; i++) {
byte b = buf[i];
if (noencode.get(b)) {
os.write(b);
} else if (b == ' ') {
os.write((byte)'+');
} else {
os.write((byte)'%');
int low = (b & 0x0f);
int high = ((b & 0xf0) >> 4);
os.write(hex[high]);
os.write(hex[low]);
}
}
}
/**
* Appends a key-value pair to an output stream buffer.
*/
private static void appendNV(ByteArrayOutputStream os, String n, String v, String enc)
throws java.io.UnsupportedEncodingException
{
if (os.size() > 0)
os.write((byte)'&');
if (enc == null)
urlEncode(os, n.getBytes());
else
urlEncode(os, n.getBytes(enc));
if (v != null) {
os.write((byte)'=');
if (enc == null)
urlEncode(os, v.getBytes());
else
urlEncode(os, v.getBytes(enc));
}
}
/**
* URL encodes a <code>Map</code>.
* @param map a name-value map of entries to encode
* @param encoding Java encoding, or null to use the default encoding
* @return a byte buffer which can be used in {@link EasyHttpClient#doPostUrlEncoded}
* @throws UnsupportedEncodingException if the encoding is invalid
*/
public static byte[] urlEncode(Map<String, String> map, String encoding)
throws UnsupportedEncodingException
{
if (map == null)
throw new IllegalArgumentException("Null map");
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (Map.Entry<String, String> me : map.entrySet()) {
appendNV(os, me.getKey(), me.getValue(), encoding);
}
return os.toByteArray();
}
/**
* URL encodes a <code>Map</code>.
* @param map a name-value map of entries to encode
* @return a byte buffer which can be used in {@link EasyHttpClient#doPostUrlEncoded}
* Strings are converted to the default platform encoding.
*/
public static byte[] urlEncode(Map<String, String> map) {
try {
return urlEncode(map, null);
} catch (UnsupportedEncodingException e) {
throw new Error("should not be here");
}
}
/**
* URL decodes a string.
*/
@SuppressWarnings("deprecation")
private static String decode(String s) {
return URLDecoder.decode(s);
}
/**
* Returns the number of times a character appears in a string.
*/
private static int count(String s, char c) {
int count = 0;
for (int i = 0; i < s.length(); i++)
if (s.charAt(i) == c)
count++;
return count;
}
/**
* Creates and returns a new array one entry longer, with a new value at the end.
* @param sa existing array
* @param s new value
* @return new array
*/
public static String[] add(String sa[], String s) {
String sa2[] = new String[sa.length + 1];
System.arraycopy(sa, 0, sa2, 0, sa.length);
sa2[sa.length] = s;
return sa2;
}
private static void put(Map<String, String[]> map, String n, String v) {
String[] sa = map.get(n);
if (sa == null)
map.put(n, new String[] { v });
else
map.put(n, add(sa, v));
}
/**
* URL decodes a string into a <code>Map</code>.
* Names without values are placed in the map, but given a
* <code>null</code> value. For example:
* <pre>
* foo&bar=&baz
* </pre>
* results in a map with keys <code>foo</code>, <code>bar</code>,
* <code>baz</code> mapped to <code>""</code>.
* If the same key appears more than once, it is added to the array.
* This method will not throw exceptions
* even if the data is irregular.
*
* @param urlEncodedData data in the URL encoded format
* @return a map containing the decoded name-value pairs
* @throws IllegalArgumentException if null data is passed in
* @see #urlEncode
*/
public static Map<String, String[]> urlDecode(String urlEncodedData) {
if (urlEncodedData == null)
throw new IllegalArgumentException("Null urlEncodedData");
urlEncodedData = urlEncodedData.trim();
StringTokenizer st = new StringTokenizer(urlEncodedData, "&=", true);
Map<String, String[]> map = new HashMap<String, String[]>();
String n;
String eq;
String v;
while (st.hasMoreTokens()) {
n = st.nextToken();
if (!st.hasMoreTokens()) {
put(map, decode(n), "");
break;
}
eq = st.nextToken();
if (!st.hasMoreTokens()) {
put(map, decode(n), "");
break;
}
if (eq.equals("&")) {
put(map, decode(n), "");
continue;
}
v = st.nextToken();
if (!v.equals("&")) {
String dn = decode(n);
String dv = decode(v);
put(map, dn, dv);
} else {
put(map, decode(n), "");
continue;
}
if (st.hasMoreTokens())
st.nextToken(); // &
}
return map;
}
/**
* URL decodes an input stream.
* @return mapping of name value pairs
*/
public static Map<String, String[]> urlDecode(InputStream is) throws IOException {
byte[] bs = HttpUtil.read(is);
return urlDecode(new String(bs, "ASCII"));
}
/**
* URL decodes an input stream.
* @return array of name value pairs
*/
public static String[] urlDecodeToArray(InputStream is) throws IOException {
byte[] bs = HttpUtil.read(is);
return urlDecodeToArray(new String(bs, "ASCII"));
}
/**
* Performs the same operation as {@link #urlDecode}, except the
* returned data is in an ordered array.
*
* @see #urlEncode
*/
public static String[] urlDecodeToArray(String urlEncodedData) {
if (urlEncodedData == null)
throw new IllegalArgumentException("Null urlEncodedData");
urlEncodedData = urlEncodedData.trim();
StringTokenizer st = new StringTokenizer(urlEncodedData, "&=", true);
int amps = count(urlEncodedData, '&');
String array[] = new String[(amps + 1) * 2];
String n;
String eq;
String v;
int i = -2;
while (st.hasMoreTokens()) {
i += 2;
n = st.nextToken();
array[i] = decode(n);
if (!st.hasMoreTokens())
break;
eq = st.nextToken();
if (!st.hasMoreTokens())
break;
if (eq.equals("&"))
continue;
v = st.nextToken();
if (!v.equals("&"))
array[i + 1] = decode(v);
else
continue;
if (st.hasMoreTokens())
st.nextToken(); // &
}
return array;
}
/**
* Discards the contents of a <code>BufferedReader</code>.
* If the reader is <code>null</code>, does nothing:
* This is to facilitate the coding of cases when the data is of
* no use to the client.
*/
public static void discard(BufferedReader r)
throws IOException
{
if (r == null)
return;
while (r.readLine() != null);
}
/**
* Returns the contents of a <code>BufferedReader</code> as a String.
* By default, each line is appended with
* <code>System.getProperty("line.separator")</code>.
* This may or may not be the original line termination character used.
*
* @throws IllegalArgumentException if the reader is null
*/
public static String read(BufferedReader r)
throws IOException
{
if (r == null)
throw new IllegalArgumentException("null reader");
String sep = System.getProperty("line.separator", "\n");
StringBuilder sb = new StringBuilder(128);
String line;
while ((line = r.readLine()) != null) {
sb.append(line).append(sep);
}
return sb.toString();
}
}