HTTPClient.Codecs Maven / Gradle / Ivy
Show all versions of grinder-httpclient Show documentation
/*
* @(#)Codecs.java 0.3-3 06/05/2001
*
* This file is part of the HTTPClient package
* Copyright (C) 1996-2001 Ronald Tschalär
*
* 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 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA
*
* For questions, suggestions, bug-reports, enhancement-requests etc.
* I may be contacted at:
*
* [email protected]
*
* The HTTPClient's home page is located at:
*
* http://www.innovation.ch/java/HTTPClient/
*
* This file contains modifications for use with "The Grinder"
* (http://grinder.sourceforge.net) under the terms of the LGPL. They
* are marked below with the comment "GRINDER MODIFICATION".
*/
package HTTPClient;
import java.util.BitSet;
import java.util.Vector;
import java.util.StringTokenizer;
import java.io.IOException;
import java.io.EOFException;
import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLConnection;
/**
* This class collects various encoders and decoders.
*
* @version 0.3-3 06/05/2001
* @author Ronald Tschalär
*/
public class Codecs
{
private static BitSet BoundChar;
private static BitSet EBCDICUnsafeChar;
private static byte[] Base64EncMap, Base64DecMap;
private static char[] UUEncMap;
private static byte[] UUDecMap;
private final static String ContDisp = "\r\nContent-Disposition: form-data; name=\"";
private final static String FileName = "\"; filename=\"";
private final static String ContType = "\r\nContent-Type: ";
private final static String Boundary = "\r\n----------ieoau._._+2_8_GoodLuck8.3-dskdfJwSJKl234324jfLdsjfdAuaoei-----";
// Class Initializer
static
{
// rfc-2046 & rfc-2045: (bcharsnospace & token)
// used for multipart codings
BoundChar = new BitSet(256);
for (int ch='0'; ch <= '9'; ch++) BoundChar.set(ch);
for (int ch='A'; ch <= 'Z'; ch++) BoundChar.set(ch);
for (int ch='a'; ch <= 'z'; ch++) BoundChar.set(ch);
BoundChar.set('+');
BoundChar.set('_');
BoundChar.set('-');
BoundChar.set('.');
// EBCDIC unsafe characters to be quoted in quoted-printable
// See first NOTE in section 6.7 of rfc-2045
EBCDICUnsafeChar = new BitSet(256);
EBCDICUnsafeChar.set('!');
EBCDICUnsafeChar.set('"');
EBCDICUnsafeChar.set('#');
EBCDICUnsafeChar.set('$');
EBCDICUnsafeChar.set('@');
EBCDICUnsafeChar.set('[');
EBCDICUnsafeChar.set('\\');
EBCDICUnsafeChar.set(']');
EBCDICUnsafeChar.set('^');
EBCDICUnsafeChar.set('`');
EBCDICUnsafeChar.set('{');
EBCDICUnsafeChar.set('|');
EBCDICUnsafeChar.set('}');
EBCDICUnsafeChar.set('~');
// rfc-2045: Base64 Alphabet
byte[] map = {
(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F',
(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L',
(byte)'M', (byte)'N', (byte)'O', (byte)'P', (byte)'Q', (byte)'R',
(byte)'S', (byte)'T', (byte)'U', (byte)'V', (byte)'W', (byte)'X',
(byte)'Y', (byte)'Z',
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f',
(byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l',
(byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r',
(byte)'s', (byte)'t', (byte)'u', (byte)'v', (byte)'w', (byte)'x',
(byte)'y', (byte)'z',
(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
(byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' };
Base64EncMap = map;
Base64DecMap = new byte[128];
for (int idx=0; idxstr
*/
public final static String base64Encode(String str)
{
if (str == null) return null;
try
{ return new String(base64Encode(str.getBytes("8859_1")), "8859_1"); }
catch (UnsupportedEncodingException uee)
{ throw new Error(uee.toString()); }
}
/**
* This method encodes the given byte[] using the base64-encoding
* specified in RFC-2045 (Section 6.8).
*
* @param data the data
* @return the base64-encoded data
*/
public final static byte[] base64Encode(byte[] data)
{
if (data == null) return null;
int sidx, didx;
byte dest[] = new byte[((data.length+2)/3)*4];
// 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
for (sidx=0, didx=0; sidx < data.length-2; sidx += 3)
{
dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 |
(data[sidx] << 4) & 077];
dest[didx++] = Base64EncMap[(data[sidx+2] >>> 6) & 003 |
(data[sidx+1] << 2) & 077];
dest[didx++] = Base64EncMap[data[sidx+2] & 077];
}
if (sidx < data.length)
{
dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
if (sidx < data.length-1)
{
dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 |
(data[sidx] << 4) & 077];
dest[didx++] = Base64EncMap[(data[sidx+1] << 2) & 077];
}
else
dest[didx++] = Base64EncMap[(data[sidx] << 4) & 077];
}
// add padding
for ( ; didx < dest.length; didx++)
dest[didx] = (byte) '=';
return dest;
}
/**
* This method decodes the given string using the base64-encoding
* specified in RFC-2045 (Section 6.8).
*
* @param str the base64-encoded string.
* @return the decoded str.
*/
public final static String base64Decode(String str)
{
if (str == null) return null;
try
{ return new String(base64Decode(str.getBytes("8859_1")), "8859_1"); }
catch (UnsupportedEncodingException uee)
{ throw new Error(uee.toString()); }
}
/**
* This method decodes the given byte[] using the base64-encoding
* specified in RFC-2045 (Section 6.8).
*
* @param data the base64-encoded data.
* @return the decoded data.
*/
public final static byte[] base64Decode(byte[] data)
{
if (data == null) return null;
int tail = data.length;
while (data[tail-1] == '=') tail--;
byte dest[] = new byte[tail - data.length/4];
// ascii printable to 0-63 conversion
for (int idx = 0; idx >> 4) & 003) );
dest[didx+1] = (byte) ( ((data[sidx+1] << 4) & 255) |
((data[sidx+2] >>> 2) & 017) );
dest[didx+2] = (byte) ( ((data[sidx+2] << 6) & 255) |
(data[sidx+3] & 077) );
}
if (didx < dest.length)
dest[didx] = (byte) ( ((data[sidx] << 2) & 255) |
((data[sidx+1] >>> 4) & 003) );
if (++didx < dest.length)
dest[didx] = (byte) ( ((data[sidx+1] << 4) & 255) |
((data[sidx+2] >>> 2) & 017) );
return dest;
}
/**
* This method encodes the given byte[] using the unix uuencode
* encding. The output is split into lines starting with the encoded
* number of encoded octets in the line and ending with a newline.
* No line is longer than 45 octets (60 characters), not including
* length and newline.
*
* Note: just the raw data is encoded; no 'begin' and 'end'
* lines are added as is done by the unix uuencode
utility.
*
* @param data the data
* @return the uuencoded data
*/
public final static char[] uuencode(byte[] data)
{
if (data == null) return null;
if (data.length == 0) return new char[0];
int line_len = 45; // line length, in octets
int sidx, didx;
char nl[] = System.getProperty("line.separator", "\n").toCharArray(),
dest[] = new char[(data.length+2)/3*4 +
((data.length+line_len-1)/line_len)*(nl.length+1)];
// split into lines, adding line-length and line terminator
for (sidx=0, didx=0; sidx+line_len < data.length; )
{
// line length
dest[didx++] = UUEncMap[line_len];
// 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
for (int end = sidx+line_len; sidx < end; sidx += 3)
{
dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077];
dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 |
(data[sidx] << 4) & 077];
dest[didx++] = UUEncMap[(data[sidx+2] >>> 6) & 003 |
(data[sidx+1] << 2) & 077];
dest[didx++] = UUEncMap[data[sidx+2] & 077];
}
// line terminator
for (int idx=0; idx>> 2) & 077];
dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 |
(data[sidx] << 4) & 077];
dest[didx++] = UUEncMap[(data[sidx+2] >>> 6) & 003 |
(data[sidx+1] << 2) & 077];
dest[didx++] = UUEncMap[data[sidx+2] & 077];
}
if (sidx < data.length-1)
{
dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077];
dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 |
(data[sidx] << 4) & 077];
dest[didx++] = UUEncMap[(data[sidx+1] << 2) & 077];
dest[didx++] = UUEncMap[0];
}
else if (sidx < data.length)
{
dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077];
dest[didx++] = UUEncMap[(data[sidx] << 4) & 077];
dest[didx++] = UUEncMap[0];
dest[didx++] = UUEncMap[0];
}
// line terminator
for (int idx=0; idxrdr throws an IOException
*/
private final static byte[] uudecode(BufferedReader rdr)
throws ParseException, IOException
{
String line, file_name;
int file_mode;
// search for beginning
while ((line = rdr.readLine()) != null && !line.startsWith("begin "))
;
if (line == null)
throw new ParseException("'begin' line not found");
// parse 'begin' line
StringTokenizer tok = new StringTokenizer(line);
tok.nextToken(); // throw away 'begin'
try // extract mode
{ file_mode = Integer.parseInt(tok.nextToken(), 8); }
catch (Exception e)
{ throw new ParseException("Invalid mode on line: " + line); }
try // extract name
{ file_name = tok.nextToken(); }
catch (java.util.NoSuchElementException e)
{ throw new ParseException("No file name found on line: " + line); }
// read and parse body
byte[] body = new byte[1000];
int off = 0;
while ((line = rdr.readLine()) != null && !line.equals("end"))
{
byte[] tmp = uudecode(line.toCharArray());
if (off + tmp.length > body.length)
body = Util.resizeArray(body, off+1000);
System.arraycopy(tmp, 0, body, off, tmp.length);
off += tmp.length;
}
if (line == null)
throw new ParseException("'end' line not found");
return Util.resizeArray(body, off);
}
/**
* This method decodes the given uuencoded char[].
*
* Note: just the actual data is decoded; any 'begin' and
* 'end' lines such as those generated by the unix uuencode
* utility must not be included.
*
* @param data the uuencode-encoded data.
* @return the decoded data.
*/
public final static byte[] uudecode(char[] data)
{
if (data == null) return null;
int sidx, didx;
byte dest[] = new byte[data.length/4*3];
for (sidx=0, didx=0; sidx < data.length; )
{
// get line length (in number of encoded octets)
int len = UUDecMap[data[sidx++]];
// ascii printable to 0-63 and 4-byte to 3-byte conversion
int end = didx+len;
for (; didx < end-2; sidx += 4)
{
byte A = UUDecMap[data[sidx]],
B = UUDecMap[data[sidx+1]],
C = UUDecMap[data[sidx+2]],
D = UUDecMap[data[sidx+3]];
dest[didx++] = (byte) ( ((A << 2) & 255) | ((B >>> 4) & 003) );
dest[didx++] = (byte) ( ((B << 4) & 255) | ((C >>> 2) & 017) );
dest[didx++] = (byte) ( ((C << 6) & 255) | (D & 077) );
}
if (didx < end)
{
byte A = UUDecMap[data[sidx]],
B = UUDecMap[data[sidx+1]];
dest[didx++] = (byte) ( ((A << 2) & 255) | ((B >>> 4) & 003) );
}
if (didx < end)
{
byte B = UUDecMap[data[sidx+1]],
C = UUDecMap[data[sidx+2]];
dest[didx++] = (byte) ( ((B << 4) & 255) | ((C >>> 2) & 017) );
}
// skip padding
while (sidx < data.length &&
data[sidx] != '\n' && data[sidx] != '\r')
sidx++;
// skip end of line
while (sidx < data.length &&
(data[sidx] == '\n' || data[sidx] == '\r'))
sidx++;
}
return Util.resizeArray(dest, didx);
}
/**
* This method does a quoted-printable encoding of the given string
* according to RFC-2045 (Section 6.7). Note: this assumes
* 8-bit characters.
*
* @param str the string
* @return the quoted-printable encoded string
*/
public final static String quotedPrintableEncode(String str)
{
if (str == null) return null;
char map[] =
{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'},
nl[] = System.getProperty("line.separator", "\n").toCharArray(),
res[] = new char[(int) (str.length()*1.5)],
src[] = str.toCharArray();
char ch;
int cnt = 0,
didx = 1,
last = 0,
slen = str.length();
for (int sidx=0; sidx < slen; sidx++)
{
ch = src[sidx];
if (ch == nl[0] && match(src, sidx, nl)) // Rule #4
{
if (res[didx-1] == ' ') // Rule #3
{
res[didx-1] = '=';
res[didx++] = '2';
res[didx++] = '0';
}
else if (res[didx-1] == '\t') // Rule #3
{
res[didx-1] = '=';
res[didx++] = '0';
res[didx++] = '9';
}
res[didx++] = '\r';
res[didx++] = '\n';
sidx += nl.length - 1;
cnt = didx;
}
else if (ch > 126 || (ch < 32 && ch != '\t') || ch == '=' ||
EBCDICUnsafeChar.get((int) ch))
{ // Rule #1, #2
res[didx++] = '=';
res[didx++] = map[(ch & 0xf0) >>> 4];
res[didx++] = map[ch & 0x0f];
}
else // Rule #1
{
res[didx++] = ch;
}
if (didx > cnt+70) // Rule #5
{
res[didx++] = '=';
res[didx++] = '\r';
res[didx++] = '\n';
cnt = didx;
}
if (didx > res.length-5)
res = Util.resizeArray(res, res.length+500);
}
return String.valueOf(res, 1, didx-1);
}
private final static boolean match(char[] str, int start, char[] arr)
{
if (str.length < start + arr.length) return false;
for (int idx=1; idx < arr.length; idx++)
if (str[start+idx] != arr[idx]) return false;
return true;
}
/**
* This method does a quoted-printable decoding of the given string
* according to RFC-2045 (Section 6.7). Note: this method
* expects the whole message in one chunk, not line by line.
*
* @param str the message
* @return the decoded message
* @exception ParseException If a '=' is not followed by a valid
* 2-digit hex number or '\r\n'.
*/
public final static String quotedPrintableDecode(String str)
throws ParseException
{
if (str == null) return null;
char res[] = new char[(int) (str.length()*1.1)],
src[] = str.toCharArray(),
nl[] = System.getProperty("line.separator", "\n").toCharArray();
int last = 0,
didx = 0,
slen = str.length();
for (int sidx=0; sidx= slen-1)
throw new ParseException("Premature end of input detected");
if (src[sidx] == '\n' || src[sidx] == '\r')
{ // Rule #5
sidx++;
if (src[sidx-1] == '\r' &&
src[sidx] == '\n')
sidx++;
}
else // Rule #1
{
char repl;
int hi = Character.digit(src[sidx], 16),
lo = Character.digit(src[sidx+1], 16);
if ((hi | lo) < 0)
throw new ParseException(new String(src, sidx-1, 3) +
" is an invalid code");
else
{
repl = (char) (hi << 4 | lo);
sidx += 2;
}
res[didx++] = repl;
}
last = didx;
}
else if (ch == '\n' || ch == '\r') // Rule #4
{
if (ch == '\r' && sidx < slen && src[sidx] == '\n')
sidx++;
for (int idx=0; idx res.length-nl.length-2)
res = Util.resizeArray(res, res.length+500);
}
return new String(res, 0, didx);
}
/**
* This method urlencodes the given string. This method is here for
* symmetry reasons and just calls java.net.URLEncoder.encode().
*
* @param str the string
* @return the url-encoded string
*/
public final static String URLEncode(String str)
{
if (str == null) return null;
/** ++GRINDER MODIFICATION **/
//return java.net.URLEncoder.encode(str);
try {
// encode() javadoc references W3C recommendation to use UTF-8.
return java.net.URLEncoder.encode(str, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
/** --GRINDER MODIFICATION **/
}
/**
* This method decodes the given urlencoded string.
*
* @param str the url-encoded string
* @return the decoded string
* @exception ParseException If a '%' is not followed by a valid
* 2-digit hex number.
*/
public final static String URLDecode(String str) throws ParseException
{
if (str == null) return null;
/** ++GRINDER MODIFICATION **/
// The original implementation only handles extended ASCII encoding, and
// so URLDecode(URLEncode(s)) was not necessarily equal to s.
// We delegate to URLDecoder instead.
try {
return java.net.URLDecoder.decode(str, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
catch (IllegalArgumentException e) {
throw new ParseException(e.getMessage());
}
/*
char[] res = new char[str.length()];
int didx = 0;
for (int sidx=0; sidxcont_type parameter, which must be of the
* form 'multipart/form-data; boundary=...'. Any encoded files are created
* in the directory specified by dir using the encoded filename.
*
* Note: Does not handle nested encodings (yet).
*
*
Examples: If you're receiving a multipart/form-data encoded response
* from a server you could use something like:
*
* NVPair[] opts = Codecs.mpFormDataDecode(resp.getData(),
* resp.getHeader("Content-type"), ".");
*
* If you're using this in a Servlet to decode the body of a request from
* a client you could use something like:
*
* byte[] body = new byte[req.getContentLength()];
* new DataInputStream(req.getInputStream()).readFully(body);
* NVPair[] opts = Codecs.mpFormDataDecode(body, req.getContentType(),
* ".");
*
* (where 'req' is the HttpServletRequest).
*
* Assuming the data received looked something like:
*
* -----------------------------114975832116442893661388290519
* Content-Disposition: form-data; name="option"
*
* doit
* -----------------------------114975832116442893661388290519
* Content-Disposition: form-data; name="comment"; filename="comment.txt"
*
* Gnus and Gnats are not Gnomes.
* -----------------------------114975832116442893661388290519--
*
* you would get one file called comment.txt in the current
* directory, and opts would contain two elements: {"option", "doit"}
* and {"comment", "comment.txt"}
*
* @param data the form-data to decode.
* @param cont_type the content type header (must contain the
* boundary string).
* @param dir the directory to create the files in.
* @param mangler the filename mangler, or null if no mangling is
* to be done. This is invoked just before each
* file is created and written, thereby allowing
* you to control the names of the files.
* @return an array of name/value pairs, one for each part;
* the name is the 'name' attribute given in the
* Content-Disposition header; the value is either
* the name of the file if a filename attribute was
* found, or the contents of the part.
* @exception IOException If any file operation fails.
* @exception ParseException If an error during parsing occurs.
*/
public final static NVPair[] mpFormDataDecode(byte[] data, String cont_type,
String dir,
FilenameMangler mangler)
throws IOException, ParseException
{
// Find and extract boundary string
String bndstr = Util.getParameter("boundary", cont_type);
if (bndstr == null)
throw new ParseException("'boundary' parameter not found in Content-type: " + cont_type);
byte[] srtbndry = ( "--" + bndstr + "\r\n").getBytes("8859_1"),
boundary = ("\r\n--" + bndstr + "\r\n").getBytes("8859_1"),
endbndry = ("\r\n--" + bndstr + "--" ).getBytes("8859_1");
// setup search routines
int[] bs = Util.compile_search(srtbndry),
bc = Util.compile_search(boundary),
be = Util.compile_search(endbndry);
// let's start parsing the actual data
int start = Util.findStr(srtbndry, bs, data, 0, data.length);
if (start == -1) // didn't even find the start
throw new ParseException("Starting boundary not found: " +
new String(srtbndry, "8859_1"));
start += srtbndry.length;
NVPair[] res = new NVPair[10];
boolean done = false;
int idx;
for (idx=0; !done; idx++)
{
// find end of this part
int end = Util.findStr(boundary, bc, data, start, data.length);
if (end == -1) // must be the last part
{
end = Util.findStr(endbndry, be, data, start, data.length);
if (end == -1)
throw new ParseException("Ending boundary not found: " +
new String(endbndry, "8859_1"));
done = true;
}
// parse header(s)
String hdr, name=null, value, filename=null, cont_disp = null;
while (true)
{
int next = findEOL(data, start) + 2;
if (next-2 <= start) break; // empty line -> end of headers
hdr = new String(data, start, next-2-start, "8859_1");
start = next;
// handle line continuation
byte ch;
while (next < data.length-1 &&
((ch = data[next]) == ' ' || ch == '\t'))
{
next = findEOL(data, start) + 2;
hdr += new String(data, start, next-2-start, "8859_1");
start = next;
}
if (!hdr.regionMatches(true, 0, "Content-Disposition", 0, 19))
continue;
Vector pcd =
Util.parseHeader(hdr.substring(hdr.indexOf(':')+1));
HttpHeaderElement elem = Util.getElement(pcd, "form-data");
if (elem == null)
throw new ParseException("Expected 'Content-Disposition: form-data' in line: "+hdr);
NVPair[] params = elem.getParams();
name = filename = null;
for (int pidx=0; pidx end)
throw new ParseException("End of header not found at offset "+end);
if (cont_disp == null)
throw new ParseException("Missing 'Content-Disposition' header at offset "+start);
// handle data for this part
if (filename != null) // It's a file
{
if (mangler != null)
filename = mangler.mangleFilename(filename, name);
if (filename != null && filename.length() > 0)
{
File file = new File(dir, filename);
FileOutputStream out = new FileOutputStream(file);
out.write(data, start, end-start);
out.close();
}
value = filename;
}
else // It's simple data
{
value = new String(data, start, end-start, "8859_1");
}
if (idx >= res.length)
res = Util.resizeArray(res, idx+10);
res[idx] = new NVPair(name, value);
start = end + boundary.length;
}
return Util.resizeArray(res, idx);
}
/**
* Searches for the next CRLF in an array.
*
* @param arr the byte array to search.
* @param off the offset at which to start the search.
* @return the position of the CR or (arr.length-2) if not found
*/
private final static int findEOL(byte[] arr, int off)
{
while (off < arr.length-1 &&
!(arr[off++] == '\r' && arr[off] == '\n'));
return off-1;
}
/**
* This method encodes name/value pairs and files into a byte array
* using the multipart/form-data encoding.
*
* @param opts the simple form-data to encode (may be null);
* for each NVPair the name refers to the 'name'
* attribute to be used in the header of the part,
* and the value is contents of the part.
* @param files the files to encode (may be null); for each
* NVPair the name refers to the 'name' attribute
* to be used in the header of the part, and the
* value is the actual filename (the file will be
* read and it's contents put in the body of that
* part).
* @param ct_hdr this returns a new NVPair in the 0'th element
* which contains name = "Content-Type",
* value = "multipart/form-data; boundary=..."
* (the reason this parameter is an array is
* because a) that's the only way to simulate
* pass-by-reference and b) you need an array for
* the headers parameter to the Post() or Put()
* anyway).
* @return an encoded byte array containing all the opts
* and files.
* @exception IOException If any file operation fails.
* @see #mpFormDataEncode(HTTPClient.NVPair[], HTTPClient.NVPair[], HTTPClient.NVPair[], HTTPClient.FilenameMangler)
*/
public final static byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files,
NVPair[] ct_hdr)
throws IOException
{
return mpFormDataEncode(opts, files, ct_hdr, null);
}
private static NVPair[] dummy = new NVPair[0];
/**
* This method encodes name/value pairs and files into a byte array
* using the multipart/form-data encoding. The boundary is returned
* as part of ct_hdr.
*
Example:
*
* NVPair[] opts = { new NVPair("option", "doit") };
* NVPair[] file = { new NVPair("comment", "comment.txt") };
* NVPair[] hdrs = new NVPair[1];
* byte[] data = Codecs.mpFormDataEncode(opts, file, hdrs);
* con.Post("/cgi-bin/handle-it", data, hdrs);
*
* data will look something like the following:
*
* -----------------------------114975832116442893661388290519
* Content-Disposition: form-data; name="option"
*
* doit
* -----------------------------114975832116442893661388290519
* Content-Disposition: form-data; name="comment"; filename="comment.txt"
* Content-Type: text/plain
*
* Gnus and Gnats are not Gnomes.
* -----------------------------114975832116442893661388290519--
*
* where the "Gnus and Gnats ..." is the contents of the file
* comment.txt in the current directory.
*
* If no elements are found in the parameters then a zero-length
* byte[] is returned and the content-type is set to
* application/octet-string (because a multipart must
* always have at least one part.
*
*
For files an attempt is made to discover the content-type, and if
* found a Content-Type header will be added to that part. The content type
* is retrieved using java.net.URLConnection.guessContentTypeFromName() -
* see java.net.URLConnection.setFileNameMap() for how to modify that map.
* Note that under JDK 1.1 by default the map seems to be empty. If you
* experience troubles getting the server to accept the data then make
* sure the fileNameMap is returning a content-type for each file (this
* may mean you'll have to set your own).
*
* @param opts the simple form-data to encode (may be null);
* for each NVPair the name refers to the 'name'
* attribute to be used in the header of the part,
* and the value is contents of the part.
* null elements in the array are ingored.
* @param files the files to encode (may be null); for each
* NVPair the name refers to the 'name' attribute
* to be used in the header of the part, and the
* value is the actual filename (the file will be
* read and it's contents put in the body of
* that part). null elements in the array
* are ingored.
* @param ct_hdr this returns a new NVPair in the 0'th element
* which contains name = "Content-Type",
* value = "multipart/form-data; boundary=..."
* (the reason this parameter is an array is
* because a) that's the only way to simulate
* pass-by-reference and b) you need an array for
* the headers parameter to the Post() or Put()
* anyway). The exception to this is that if no
* opts or files are given the type is set to
* "application/octet-stream" instead.
* @param mangler the filename mangler, or null if no mangling is
* to be done. This allows you to change the name
* used in the filename attribute of the
* Content-Disposition header. Note: the mangler
* will be invoked twice for each filename.
* @return an encoded byte array containing all the opts
* and files.
* @exception IOException If any file operation fails.
*/
public final static byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files,
NVPair[] ct_hdr,
FilenameMangler mangler)
throws IOException
{
byte[] boundary = Boundary.getBytes("8859_1"),
cont_disp = ContDisp.getBytes("8859_1"),
cont_type = ContType.getBytes("8859_1"),
filename = FileName.getBytes("8859_1");
int len = 0,
hdr_len = boundary.length + cont_disp.length+1 + 2 + 2;
// \r\n -- bnd \r\n C-D: ..; n=".." \r\n \r\n
if (opts == null) opts = dummy;
if (files == null) files = dummy;
// Calculate the length of the data
for (int idx=0; idx>8 & 0xff)) new_c += 0x00000100;
while (!BoundChar.get(new_c>>16 & 0xff)) new_c += 0x00010000;
while (!BoundChar.get(new_c>>24 & 0xff)) new_c += 0x01000000;
boundary[40] = (byte) (new_c & 0xff);
boundary[42] = (byte) (new_c>>8 & 0xff);
boundary[44] = (byte) (new_c>>16 & 0xff);
boundary[46] = (byte) (new_c>>24 & 0xff);
int off = 2;
int[] bnd_cmp = Util.compile_search(boundary);
for (int idx=0; idx= boundary.length &&
Util.findStr(boundary, bnd_cmp, res, start, pos) != -1)
continue NewBound;
}
for (int idx=0; idx 0)
{
int got = fin.read(res, pos, nlen);
nlen -= got;
pos += got;
}
fin.close();
if ((pos-start) >= boundary.length &&
Util.findStr(boundary, bnd_cmp, res, start, pos) != -1)
continue NewBound;
}
break NewBound;
}
System.arraycopy(boundary, 0, res, pos, boundary.length);
pos += boundary.length;
res[pos++] = (byte) '-';
res[pos++] = (byte) '-';
res[pos++] = (byte) '\r';
res[pos++] = (byte) '\n';
if (pos != len)
throw new Error("Calculated "+len+" bytes but wrote "+pos+" bytes!");
/* the boundary parameter should be quoted (rfc-2046, section 5.1.1)
* but too many script authors are not capable of reading specs...
* So, I give up and don't quote it.
*/
ct_hdr[0] = new NVPair("Content-Type",
"multipart/form-data; boundary=" +
new String(boundary, 4, boundary.length-4, "8859_1"));
return res;
}
private static class CT extends URLConnection
{
protected static final String getContentType(String fname)
{
return guessContentTypeFromName(fname);
}
private CT() { super(null); }
public void connect() { }
}
/**
* Turns an array of name/value pairs into the string
* "name1=value1&name2=value2&name3=value3". The names and values are
* first urlencoded. This is the form in which form-data is passed to
* a cgi script.
*
* @param pairs the array of name/value pairs
* @return a string containg the encoded name/value pairs
*/
public final static String nv2query(NVPair pairs[])
{
if (pairs == null)
return null;
int idx;
StringBuffer qbuf = new StringBuffer();
for (idx = 0; idx < pairs.length; idx++)
{
if (pairs[idx] != null)
qbuf.append(URLEncode(pairs[idx].getName()) + "=" +
URLEncode(pairs[idx].getValue()) + "&");
}
if (qbuf.length() > 0)
qbuf.setLength(qbuf.length()-1); // remove trailing '&'
return qbuf.toString();
}
/**
* Turns a string of the form "name1=value1&name2=value2&name3=value3"
* into an array of name/value pairs. The names and values are
* urldecoded. The query string is in the form in which form-data is
* received in a cgi script.
*
* @param query the query string containing the encoded name/value pairs
* @return an array of NVPairs
* @exception ParseException If the '=' is missing in any field, or if
* the urldecoding of the name or value fails
*/
public final static NVPair[] query2nv(String query) throws ParseException
{
if (query == null) return null;
int idx = -1,
cnt = 1;
while ((idx = query.indexOf('&', idx+1)) != -1) cnt ++;
NVPair[] pairs = new NVPair[cnt];
for (idx=0, cnt=0; cnt= end)
throw new ParseException("'=' missing in " +
query.substring(idx, end));
pairs[cnt] =
new NVPair(URLDecode(query.substring(idx,eq)),
URLDecode(query.substring(eq+1,end)));
idx = end + 1;
}
return pairs;
}
/**
* Encodes data used the chunked encoding. last signales if
* this is the last chunk, in which case the appropriate footer is
* generated.
*
* @param data the data to be encoded; may be null.
* @param ftrs optional headers to include in the footer (ignored if
* not last); may be null.
* @param last whether this is the last chunk.
* @return an array of bytes containing the chunk
*/
public final static byte[] chunkedEncode(byte[] data, NVPair[] ftrs,
boolean last)
{
return
chunkedEncode(data, 0, data == null ? 0 : data.length, ftrs, last);
}
/**
* Encodes data used the chunked encoding. last signales if
* this is the last chunk, in which case the appropriate footer is
* generated.
*
* @param data the data to be encoded; may be null.
* @param off an offset into the data
* @param len the number of bytes to take from data
* @param ftrs optional headers to include in the footer (ignored if
* not last); may be null.
* @param last whether this is the last chunk.
* @return an array of bytes containing the chunk
*/
public final static byte[] chunkedEncode(byte[] data, int off, int len,
NVPair[] ftrs, boolean last)
{
if (data == null)
{
data = new byte[0];
len = 0;
}
if (last && ftrs == null) ftrs = new NVPair[0];
// get length of data as hex-string
String hex_len = Integer.toString(len, 16);
// calculate length of chunk
int res_len = 0;
if (len > 0) // len CRLF data CRLF
res_len += hex_len.length() + 2 + len + 2;
if (last)
{
res_len += 1 + 2; // 0 CRLF
for (int idx=0; idx 0)
{
int hlen = hex_len.length();
try
{ System.arraycopy(hex_len.getBytes("8859_1"), 0, res, r_off, hlen); }
catch (UnsupportedEncodingException uee)
{ throw new Error(uee.toString()); }
r_off += hlen;
res[r_off++] = (byte) '\r';
res[r_off++] = (byte) '\n';
System.arraycopy(data, off, res, r_off, len);
r_off += len;
res[r_off++] = (byte) '\r';
res[r_off++] = (byte) '\n';
}
if (last)
{
res[r_off++] = (byte) '0';
res[r_off++] = (byte) '\r';
res[r_off++] = (byte) '\n';
for (int idx=0; idx Integer.MAX_VALUE) // Huston, what the hell are you sending?
throw new ParseException("Can't deal with chunk lengths greater " +
"Integer.MAX_VALUE: " + clen + " > " +
Integer.MAX_VALUE);
if (clen > 0) // it's a chunk
{
byte[] res = new byte[(int) clen];
int off = 0, len = 0;
while (len != -1 && off < res.length)
{
len = input.read(res, off, res.length-off);
off += len;
}
if (len == -1)
throw new ParseException("Premature EOF while reading chunk;" +
"Expected: "+res.length+" Bytes, " +
"Received: "+(off+1)+" Bytes");
input.read(); // CR
input.read(); // LF
return res;
}
else // it's the end
{
NVPair[] res = new NVPair[0];
BufferedReader reader = new BufferedReader(new InputStreamReader(input, "8859_1"));
String line;
// read and parse footer
while ((line = reader.readLine()) != null && line.length() > 0)
{
int colon = line.indexOf(':');
if (colon == -1)
throw new ParseException("Error in Footer format: no "+
"':' found in '" + line + "'");
res = Util.resizeArray(res, res.length+1);
res[res.length-1] = new NVPair(line.substring(0, colon).trim(),
line.substring(colon+1).trim());
}
return res;
}
}
/**
* Gets the length of the chunk.
*
* @param input the stream from which to read the next chunk.
* @return the length of chunk to follow (w/o trailing CR LF).
* @exception ParseException If any exception during parsing occured.
* @exception IOException If any exception during reading occured.
*/
final static long getChunkLength(InputStream input)
throws ParseException, IOException
{
byte[] hex_len = new byte[16]; // if they send more than 8EB chunks...
int off = 0,
ch;
// read chunk length
while ((ch = input.read()) > 0 && (ch == ' ' || ch == '\t')) ;
if (ch < 0)
throw new EOFException("Premature EOF while reading chunk length");
hex_len[off++] = (byte) ch;
while ((ch = input.read()) > 0 && ch != '\r' && ch != '\n' &&
ch != ' ' && ch != '\t' && ch != ';' &&
off < hex_len.length)
hex_len[off++] = (byte) ch;
while ((ch == ' ' || ch == '\t') && (ch = input.read()) > 0) ;
if (ch == ';') // chunk-ext (ignore it)
while ((ch = input.read()) > 0 && ch != '\r' && ch != '\n') ;
if (ch < 0)
throw new EOFException("Premature EOF while reading chunk length");
if (ch != '\n' && (ch != '\r' || input.read() != '\n'))
throw new ParseException("Didn't find valid chunk length: " +
new String(hex_len, 0, off, "8859_1"));
// parse chunk length
try
{ return Long.parseLong(new String(hex_len, 0, off, "8859_1").trim(),
16); }
catch (NumberFormatException nfe)
{ throw new ParseException("Didn't find valid chunk length: " +
new String(hex_len, 0, off, "8859_1") ); }
}
}