blob: 34885415e0b42490778abf1acc9e1f07624f752a [file] [log] [blame]
/**
* Copyright (c) 2006 Parity Communications, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sergey Yakovlev - initial API and implementation
*/
package org.eclipse.ecf.internal.provider.rss.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.net.MalformedURLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.TimeZone;
import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.internal.provider.rss.RssDebugOptions;
import org.eclipse.ecf.internal.provider.rss.RssPlugin;
/**
* A HTTP message. It can be read from an InputStream or constructed from
* scratch. The body of the message is only read on demand and cached in a
* byte[] for later use. Provides functions for getting and setting headers
*
*/
public class HttpMessage {
protected static String DEFAULT_VERSION = "HTTP/1.1";
protected String startLine = null;
protected Hashtable headers = new Hashtable(20);
protected boolean hasBody = false;
protected byte[] body = null;
protected int length = 0;
private InputStream in = null;
/** Creates a new instance of HttpMessage */
public HttpMessage() {
super();
}
public HttpMessage(InputStream in) throws MalformedURLException, IOException {
this.in = in;
readHead();
}
protected void trace(String msg) {
Trace.trace(RssPlugin.PLUGIN_ID, RssDebugOptions.DEBUG, msg);
}
protected void dumpStack(String msg, Throwable e) {
Trace.catching(RssPlugin.PLUGIN_ID, RssDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), "", e);
}
/**
* Reads the head of the message (startline and headers) from the
* InputStream
* @throws IOException
*/
public void readHead() throws IOException {
trace("readHead start");
String line;
// read startline & ignore empty lines before startline
for (int i = 0; i < 100; i++) {
if (!((startLine = readLine(in)).equals(""))) {
break;
}
}
if (startLine.equals("")) {
trace("startLine empty");
throw new IOException();
}
// Logs startLine
trace("startLine: " + startLine);
// read headers until CRLF
String actHeader = null;
while (!(line = readLine(in)).equals("")) {
if (line.startsWith(" ") || line.startsWith("\t")) {
// multiline header
if (actHeader != null) {
headers.put(actHeader, ((String) headers.get(actHeader)) + "\n" + line.trim());
}
} else {
// header
actHeader = (line.substring(0, line.indexOf(":"))).toLowerCase();
if (!headers.containsKey(actHeader)) {
// normal case
headers.put(actHeader, ((line.substring(line.indexOf(":") + 1)).trim()));
} else {
// header is already defined -> make comma seperated list
// RFC2086 page 31
String list = (String) headers.get(actHeader);
list += "," + (line.substring(line.indexOf(":") + 1)).trim();
headers.put(actHeader, list);
}
}
// Logs header
trace(line);
}
}
public String getHeader(String name) {
return (String) headers.get(name.toLowerCase());
}
public Date getDateHeader(String name) {
Date date;
SimpleDateFormat dateFormatter;
final String header = this.getHeader(name);
// try rfc 1123 date first
dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
date = dateFormatter.parse(header);
return date;
} catch (final java.text.ParseException e1) {
// ok, now try rfc 850 date
dateFormatter = new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.ENGLISH);
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
date = dateFormatter.parse(header);
return date;
} catch (final java.text.ParseException e2) {
// last, try asctime date format
dateFormatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.ENGLISH);
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
date = dateFormatter.parse(header);
return date;
} catch (final java.text.ParseException e3) {
trace("Could not parse date: " + e3.getMessage());
}
}
}
return null;
}
public Enumeration getHeaders() {
return headers.keys();
}
public void setHeader(String name, String value) {
headers.put(name.toLowerCase(), value);
}
public String setDateHeader(String name, Date value) {
// make rfc 1123 (and rfc 2068) compliant date
final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
final String strDate = dateFormatter.format(value).toString();
setHeader(name, strDate);
return strDate;
}
public void removeHeader(String name) {
headers.remove(name.toLowerCase());
}
public boolean containsHeader(String name) {
return headers.containsKey(name.toLowerCase());
}
public boolean hasBody() {
return hasBody;
}
/**
* retrieves the body of the message as a byte[]. If the body is not read in
* yet, it is read from the InputStream and cached in an internal array
* @return byte []
* @throws IOException
*/
public byte[] getBody() throws IOException {
final ByteArrayOutputStream b = new ByteArrayOutputStream();
readBody(b);
body = b.toByteArray();
return body;
}
/**
* sets the body of the message and therefore overrides a received body
* @param body
*/
public void setBody(byte[] body) {
this.body = body;
length = body.length;
hasBody = true;
}
/**
* overridden by HttpRequest and HttpResponse
*/
protected String getStartLine() {
return startLine;
}
/**
* writes the message (startline, headers, body) out to the specified
* OutputStream
*/
protected void writeToStream(String startLine, OutputStream out) throws IOException {
out.write((startLine + "\n").getBytes());
final Enumeration e = headers.keys();
if (body != null) {
setHeader("content-length", String.valueOf(body.length));
}
// write headers out:
// * we should not change the order, although it should not matter...
while (e.hasMoreElements()) {
final String hName = (String) e.nextElement();
out.write((hName + ": " + (String) headers.get(hName) + "\n").getBytes());
}
out.write(("\n").getBytes());
// write body out
if (hasBody) {
readBody(out);
}
}
/**
* change default version
* @param version
*/
public void setDefaultVersion(String version) {
DEFAULT_VERSION = version;
}
/**
* utility function: reads a header line from the stream according to rfc
* (taken from muffin)
*/
protected String readLine(InputStream in) throws IOException {
char buf[] = new char[128];
int offset = 0;
int ch;
while ((ch = in.read()) > -1) {
if (ch == '\n') {
break;
} else if (ch == '\r') {
final int tmpch = in.read();
if (tmpch != '\n') {
if (!(in instanceof PushbackInputStream)) {
trace("creating PushbackInputStream");
in = new PushbackInputStream(in);
}
((PushbackInputStream) in).unread(tmpch);
}
break;
} else {
if (offset == buf.length) {
final char tmpbuf[] = buf;
buf = new char[tmpbuf.length * 2];
System.arraycopy(tmpbuf, 0, buf, 0, offset);
}
buf[offset++] = (char) ch;
}
}
return String.copyValueOf(buf, 0, offset);
}
/**
* read body of message in and write it to specified OutputStream.
* content-length is determined according to RFC 2068 page 31 1.) if a body
* must not be present (hasBody == false), ignore body content. 2.) if
* chunked transfer-ecoding, this defines the length 3.) content-length
* header 4.) multipart/byteranges -> ignore, we can't handle them (todo!!)
* 5.) read all content in until server closes connection
*/
private void readBody(OutputStream out) throws IOException {
if (hasBody && body == null) {
if ((containsHeader("Transfer-Encoding")) && (getHeader("Transfer-Encoding").equals("chunked"))) {
// decoding-routine for chunked transfer-encoding
// (according to rfc 2616 section 19.4.6.)
int n;
int chunk_size;
String line;
final byte[] buffer = new byte[8192];
final ByteArrayOutputStream byte_out = new ByteArrayOutputStream(8192);
length = 0;
line = readLine(in);
chunk_size = Integer.parseInt(line.substring(0, line.indexOf(" ") > -1 ? line.indexOf(" ") : line.length()), 16);
while (chunk_size > 0) {
for (int len = 0; len < chunk_size;) {
n = in.read(buffer, 0, Math.min(chunk_size - len, buffer.length));
if (n == -1) {
break;
}
len += n;
byte_out.write(buffer, 0, n);
}
line = readLine(in); // trailing crlf
length += chunk_size;
line = readLine(in);
chunk_size = Integer.parseInt(line.substring(0, line.indexOf(" ") > -1 ? line.indexOf(" ") : line.length()), 16);
}
// headers come last...
String actHeader = null;
while (!(line = readLine(in)).equals("")) {
if (line.startsWith(" ") || line.startsWith("\t")) {
// multiline header
headers.put(actHeader, ((String) headers.get(actHeader)) + "\n" + line);
} else {
actHeader = (line.substring(0, line.indexOf(":"))).toLowerCase();
headers.put(actHeader, ((line.substring(line.indexOf(":") + 1)).trim()));
}
}
setHeader("content-length", "" + length);
// Proxies/gateways MUST remove any transfer coding prior to
// forwarding a message via a MIME-compliant protocol (RFC 2068,
// sec.19.4.6).
removeHeader("transfer-encoding");
out.write(byte_out.toByteArray());
} else if (headers.containsKey("content-length")) {
// header defines length of content
length = Integer.parseInt(getHeader("Content-Length"));
int n;
int len = 0;
final byte buffer[] = new byte[8192];
while (len < length) {
n = in.read(buffer, 0, Math.min(length - len, buffer.length));
if (n == -1) {
break;
}
len += n;
out.write(buffer, 0, n);
out.flush();
}
} else {
// just read until server closes connection
int n;
final byte buffer[] = new byte[8192];
while (true) {
n = in.read(buffer, 0, buffer.length);
if (n == -1) {
break;
}
out.write(buffer, 0, n);
out.flush();
}
}
} else if (hasBody) { // body != null
out.write(body);
}
}
}