// ======================================================================== // $Id: HttpMessage.java,v 1.41 2006/04/04 22:28:02 gregwilkins Exp $ // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package net.lightbody.bmp.proxy.jetty.http; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import net.lightbody.bmp.proxy.jetty.util.LogSupport; import net.lightbody.bmp.proxy.jetty.util.QuotedStringTokenizer; import net.lightbody.bmp.proxy.jetty.util.TypeUtil; import org.apache.commons.logging.Log; import java.io.*; import java.util.*; /* ------------------------------------------------------------ */ /** HTTP Message base. * This class forms the basis of HTTP requests and replies. It provides * header fields, content and optional trailer fields, while managing the * state of the message. * * @version $Id: HttpMessage.java,v 1.41 2006/04/04 22:28:02 gregwilkins Exp $ * @author Greg Wilkins (gregw) */ public abstract class HttpMessage { private static Log log = LogFactory.getLog(HttpMessage.class); /* ------------------------------------------------------------ */ public final static String __SCHEME ="http"; public final static String __SSL_SCHEME ="https"; /* ------------------------------------------------------------ */ public final static String __HTTP_0_9 ="HTTP/0.9"; public final static String __HTTP_1_0 ="HTTP/1.0"; public final static String __HTTP_1_1 ="HTTP/1.1"; public final static String __HTTP_1_X ="HTTP/1."; /* ------------------------------------------------------------ */ public interface HeaderWriter { void writeHeader(HttpMessage httpMessage) throws IOException; } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /** Message States. */ public final static int __MSG_EDITABLE=0, // Created locally, all set methods enabled __MSG_BAD=1, // Bad message/ __MSG_RECEIVED=2, // Received from connection. __MSG_SENDING=3, // Headers sent. __MSG_SENT=4; // Entity and trailers sent. public final static String[] __state = { "EDITABLE", "BAD", "RECEIVED", "SENDING", "SENT" }; /* ------------------------------------------------------------ */ protected int _state=__MSG_EDITABLE; protected String _version; protected int _dotVersion; protected HttpFields _header=new HttpFields(); protected HttpConnection _connection; protected String _characterEncoding; protected String _mimeType; protected Object _wrapper; protected Map _attributes; /* ------------------------------------------------------------ */ /** Constructor. */ protected HttpMessage() {} /* ------------------------------------------------------------ */ /** Constructor. */ protected HttpMessage(HttpConnection connection) { _connection=connection; } /* ------------------------------------------------------------ */ /** Set a wrapper object. * A wrapper object is an object associated with this message and * presents it with an different interface. The * primary example of a HttpRequest facade is ServletHttpRequest. * A single facade object may be associated with the message with * this call and retrieved with the getFacade method. */ public void setWrapper(Object wrapper) { _wrapper=wrapper; } /* ------------------------------------------------------------ */ /** Get an associated wrapper object. * @return Wrapper message or null. */ public Object getWrapper() { return _wrapper; } /* ------------------------------------------------------------ */ protected void reset() { _state=__MSG_EDITABLE; _header.clear(); } /* ------------------------------------------------------------ */ public HttpConnection getHttpConnection() { return _connection; } /* ------------------------------------------------------------ */ public InputStream getInputStream() { if (_connection==null) return null; return _connection.getInputStream(); } /* ------------------------------------------------------------ */ public OutputStream getOutputStream() { if (_connection==null) return null; return _connection.getOutputStream(); } /* ------------------------------------------------------------ */ /** Get the message state. * <PRE> * __MSG_EDITABLE = 0 - Created locally, all set methods enabled * __MSG_BAD = 1 - Bad message or send failure. * __MSG_RECEIVED = 2 - Received from connection. * __MSG_SENDING = 3 - Headers sent. * __MSG_SENT = 4 - Entity and trailers sent. * </PRE> * @return the state. */ public int getState() { return _state; } /* ------------------------------------------------------------ */ /** Set the message state. * This method should be used by experts only as it can prevent * normal handling of a request/response. * @param state The new state * @return the last state. */ public int setState(int state) { int last=_state; _state=state; return last; } /* ------------------------------------------------------------ */ /** Get the protocol version. * @return return the version. */ public String getVersion() { return _version; } /* ------------------------------------------------------------ */ /** Get the protocol version. * @return return the version dot (0.9=-1 1.0=0 1.1=1) */ public int getDotVersion() { return _dotVersion; } /* ------------------------------------------------------------ */ /** Get field names. * @return Enumeration of Field Names */ public Enumeration getFieldNames() { return _header.getFieldNames(); } /* ------------------------------------------------------------ */ /** Does the header or trailer contain a field? * @param name Name of the field * @return True if contained in header or trailer. */ public boolean containsField(String name) { return _header.containsKey(name); } /* ------------------------------------------------------------ */ /** Get a message field. * Get a field from a message header. If no header field is found, * trailer fields are searched. * @param name The field name * @return field value or null */ public String getField(String name) { return _header.get(name); } /* ------------------------------------------------------------ */ /** Get a multi valued message field. * Get a field from a message header. * @param name The field name * @return Enumeration of field values or null */ public Enumeration getFieldValues(String name) { return _header.getValues(name); } /* ------------------------------------------------------------ */ /** Get a multi valued message field. * Get a field from a message header. * @param name The field name * @param separators String of separators. * @return Enumeration of field values or null */ public Enumeration getFieldValues(String name,String separators) { return _header.getValues(name,separators); } /* ------------------------------------------------------------ */ /** Set a field value. * If the message is editable, then a header field is set. Otherwise * if the message is sending and a HTTP/1.1 version, then a trailer * field is set. * @param name Name of field * @param value New value of field * @return Old value of field */ public String setField(String name, String value) { if (_state!=__MSG_EDITABLE) return null; if (HttpFields.__ContentType.equalsIgnoreCase(name)) { String old=_header.get(name); setContentType(value); return old; } return _header.put(name,value); } /* ------------------------------------------------------------ */ /** Set a multi-value field value. * If the message is editable, then a header field is set. Otherwise * if the meesage is sending and a HTTP/1.1 version, then a trailer * field is set. * @param name Name of field * @param value New values of field */ public void setField(String name, List value) { if (_state!=__MSG_EDITABLE) return; _header.put(name,value); } /* ------------------------------------------------------------ */ /** Add to a multi-value field value. * If the message is editable, then a header field is set. Otherwise * if the meesage is sending and a HTTP/1.1 version, then a trailer * field is set. * @param name Name of field * @param value New value to add to the field * @exception IllegalStateException Not editable or sending 1.1 * with trailers */ public void addField(String name, String value) throws IllegalStateException { if (_state!=__MSG_EDITABLE) return; _header.add(name,value); } /* -------------------------------------------------------------- */ /** Get a field as an integer value. * Look in header and trailer fields. * Returns the value of an integer field, or -1 if not found. * The case of the field name is ignored. * @param name the case-insensitive field name */ public int getIntField(String name) { return _header.getIntField(name); } /* -------------------------------------------------------------- */ /** Sets the value of an integer field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param value the field integer value */ public void setIntField(String name, int value) { if (_state!=__MSG_EDITABLE) return; _header.put(name, TypeUtil.toString(value)); } /* -------------------------------------------------------------- */ /** Adds the value of an integer field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param value the field integer value */ public void addIntField(String name, int value) { if (_state!=__MSG_EDITABLE) return; _header.add(name, TypeUtil.toString(value)); } /* -------------------------------------------------------------- */ /** Get a header as a date value. * Look in header and trailer fields. * Returns the value of a date field, or -1 if not found. * The case of the field name is ignored. * @param name the case-insensitive field name */ public long getDateField(String name) { return _header.getDateField(name); } /* -------------------------------------------------------------- */ /** Sets the value of a date field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param date the field date value */ public void setDateField(String name, Date date) { if (_state!=__MSG_EDITABLE) return; _header.putDateField(name,date); } /* -------------------------------------------------------------- */ /** Adds the value of a date field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param date the field date value */ public void addDateField(String name, Date date) { if (_state!=__MSG_EDITABLE) return; _header.addDateField(name,date); } /* -------------------------------------------------------------- */ /** Sets the value of a date field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param date the field date value */ public void setDateField(String name, long date) { if (_state!=__MSG_EDITABLE) return; _header.putDateField(name,date); } /* -------------------------------------------------------------- */ /** Add the value of a date field. * Header or Trailer fields are set depending on message state. * @param name the field name * @param date the field date value * @exception IllegalStateException Not editable or sending 1.1 * with trailers */ public void addDateField(String name, long date) { if (_state!=__MSG_EDITABLE) return; _header.addDateField(name,date); } /* ------------------------------------------------------------ */ /** Remove a field. * If the message is editable, then a header field is removed. Otherwise * if the message is sending and a HTTP/1.1 version, then a trailer * field is removed. * @param name Name of field * @return Old value of field */ public String removeField(String name) throws IllegalStateException { if (_state!=__MSG_EDITABLE) return null; return _header.remove(name); } /* ------------------------------------------------------------ */ /** Set the request version * @param version the HTTP version string (eg HTTP/1.1) * @exception IllegalStateException message is not EDITABLE */ public void setVersion(String version) { if (_state!=__MSG_EDITABLE) throw new IllegalStateException("Not EDITABLE"); if (version.equalsIgnoreCase(__HTTP_1_1)) { _dotVersion=1; _version=__HTTP_1_1; } else if (version.equalsIgnoreCase(__HTTP_1_0)) { _dotVersion=0; _version=__HTTP_1_0; } else if (version.equalsIgnoreCase(__HTTP_0_9)) { _dotVersion=-1; _version=__HTTP_0_9; } else throw new IllegalArgumentException("Unknown version"); } /* ------------------------------------------------------------ */ /** Get the HTTP header fields. * @return Header or null */ public HttpFields getHeader() { if (_state!=__MSG_EDITABLE) throw new IllegalStateException("Can't get header in "+__state[_state]); return _header; } /* -------------------------------------------------------------- */ public int getContentLength() { return getIntField(HttpFields.__ContentLength); } /* ------------------------------------------------------------ */ public void setContentLength(int len) { setIntField(HttpFields.__ContentLength,len); } /* -------------------------------------------------------------- */ /** Character Encoding. * The character encoding is extracted from the ContentType field * when set. * @return Character Encoding or null */ public String getCharacterEncoding() { return _characterEncoding; } /* ------------------------------------------------------------ */ /** Set Character Encoding. * @param encoding An encoding that can override the encoding set * from the ContentType field. */ public void setCharacterEncoding(String encoding,boolean setField) { if (isCommitted()) return; if (encoding==null) { // Clear any encoding. if (_characterEncoding!=null) { _characterEncoding=null; if (setField) _header.put(HttpFields.__ContentType,_mimeType); } } else { // No, so just add this one to the mimetype _characterEncoding=encoding; if (setField && _mimeType!=null) { _header.put(HttpFields.__ContentType, _mimeType+";charset="+ QuotedStringTokenizer.quote(_characterEncoding,";= ")); } } } /* -------------------------------------------------------------- */ public String getContentType() { return getField(HttpFields.__ContentType); } /* ------------------------------------------------------------ */ public void setContentType(String contentType) { if (isCommitted()) return; if (contentType==null) { _mimeType=null; _header.remove(HttpFields.__ContentType); } else { // Look for encoding in contentType int i0=contentType.indexOf(';'); if (i0>0) { // Strip params off mimetype _mimeType=contentType.substring(0,i0).trim(); // Look for charset int i1=contentType.indexOf("charset=",i0); if (i1>=0) { i1+=8; int i2 = contentType.indexOf(' ',i1); _characterEncoding = (0<i2) ? contentType.substring(i1,i2) : contentType.substring(i1); _characterEncoding = QuotedStringTokenizer.unquote(_characterEncoding); } else // No encoding in the params. { if (_characterEncoding!=null) // Add any previously set encoding. contentType+=";charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); } } else // No encoding and no other params { _mimeType=contentType; // Add any previously set encoding. if (_characterEncoding!=null) contentType+=";charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); } _header.put(HttpFields.__ContentType,contentType); } } /* ------------------------------------------------------------ */ public void updateMimeType() { _mimeType=null; _characterEncoding=null; String contentType= _header.get(HttpFields.__ContentType); if (contentType!=null) { // Look for encoding in contentType int i0=contentType.indexOf(';'); if (i0>0) { // Strip params off mimetype _mimeType=contentType.substring(0,i0).trim(); // Look for charset int i1=contentType.indexOf("charset=",i0); if (i1>=0) { i1+=8; int i2 = contentType.indexOf(' ',i1); _characterEncoding = (0<i2) ? contentType.substring(i1,i2) : contentType.substring(i1); _characterEncoding = QuotedStringTokenizer.unquote(_characterEncoding); } } else { _mimeType=contentType; } } } /* -------------------------------------------------------------- */ /** Mime Type. * The mime type is extracted from the contenttype field when set. * @return Content type without parameters */ public String getMimeType() { return _mimeType; } /* ------------------------------------------------------------ */ /** Recycle the message. */ void recycle(HttpConnection connection) { _state=__MSG_EDITABLE; _version=__HTTP_1_1; _dotVersion=1; _header.clear(); _connection=connection; _characterEncoding=null; _mimeType=null; if (_attributes!=null) _attributes.clear(); } /* ------------------------------------------------------------ */ /** Destroy the message. * Help the garbage collector by nulling everything that we can. */ public void destroy() { recycle(null); if (_header!=null) _header.destroy(); _header=null; } /* ------------------------------------------------------------ */ /** Convert to String. * The message header is converted to a String. * @return String */ public synchronized String toString() { StringWriter writer = new StringWriter(); int save_state=_state; try{ _state=__MSG_EDITABLE; writeHeader(writer); } catch(IOException e) { log.warn(LogSupport.EXCEPTION,e); } finally { _state=save_state; } return writer.toString(); } /* ------------------------------------------------------------ */ /** Write the message header. * @param writer */ abstract void writeHeader(Writer writer) throws IOException; /* ------------------------------------------------------------ */ public boolean isCommitted() { return _state==__MSG_SENDING || _state==__MSG_SENT; } /* ------------------------------------------------------------ */ /** * @return true if the message has been modified. */ public boolean isDirty() { HttpOutputStream out=(HttpOutputStream)getOutputStream(); return _state!=__MSG_EDITABLE || ( out!=null && out.isWritten()); } /* ------------------------------------------------------------ */ /** Get a request attribute. * @param name Attribute name * @return Attribute value */ public Object getAttribute(String name) { if (_attributes==null) return null; return _attributes.get(name); } /* ------------------------------------------------------------ */ /** Set a request attribute. * @param name Attribute name * @param attribute Attribute value * @return Previous Attribute value */ public Object setAttribute(String name, Object attribute) { if (_attributes==null) _attributes=new HashMap(11); return _attributes.put(name,attribute); } /* ------------------------------------------------------------ */ /** Get Attribute names. * @return Enumeration of Strings */ public Enumeration getAttributeNames() { if (_attributes==null) return Collections.enumeration(Collections.EMPTY_LIST); return Collections.enumeration(_attributes.keySet()); } /* ------------------------------------------------------------ */ /** Remove a request attribute. * @param name Attribute name */ public void removeAttribute(String name) { if (_attributes!=null) _attributes.remove(name); } }