/** * 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); } } }