// =========================================================================== // Copyright (c) 1996 Mort Bay Consulting Pty. Ltd. All rights reserved. // $Id: HttpMessage.java,v 1.15.2.9 2003/07/12 01:53:10 gregwilkins Exp $ // --------------------------------------------------------------------------- package org.mortbay.http; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import org.mortbay.util.Code; import org.mortbay.util.QuotedStringTokenizer; import org.mortbay.util.TypeUtil; /* ------------------------------------------------------------ */ /** 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.15.2.9 2003/07/12 01:53:10 gregwilkins Exp $ * @author Greg Wilkins (gregw) */ public abstract class HttpMessage { /* ------------------------------------------------------------ */ 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 HttpFields _trailer; protected boolean _acceptTrailer; 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=new HttpFields(); _trailer=null; // XXX - also need to cancel any encodings added to output stream! } /* ------------------------------------------------------------ */ 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. *
* __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.
*
* @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()
{
if (_header!=null && _trailer==null)
return _header.getFieldNames();
final Enumeration e1=_header.getFieldNames();
final Enumeration e2=_trailer.getFieldNames();
return new Enumeration()
{
public boolean hasMoreElements()
{
return (e1.hasMoreElements() ||
e2.hasMoreElements());
}
public Object nextElement()
throws NoSuchElementException
{
if (e1.hasMoreElements())
return e1.nextElement();
return e2.nextElement();
}
};
}
/* ------------------------------------------------------------ */
/** 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)
{
boolean contains = _header.containsKey(name);
if (!contains && _trailer!=null)
contains = _trailer.containsKey(name);
return contains;
}
/* ------------------------------------------------------------ */
/** 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)
{
String field = _header.get(name);
if (field==null && _trailer!=null)
field=_trailer.get(name);
return field;
}
/* ------------------------------------------------------------ */
/** Get a multi valued 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 Enumeration of field values or null
*/
public Enumeration getFieldValues(String name)
{
Enumeration enum = _header.getValues(name);
if (enum==null && _trailer!=null)
enum=_trailer.getValues(name);
return enum;
}
/* ------------------------------------------------------------ */
/** Get a multi valued message field.
* Get a field from a message header. If no header field is found,
* trailer fields are searched.
* @param name The field name
* @param separators String of separators.
* @return Enumeration of field values or null
*/
public Enumeration getFieldValues(String name,String separators)
{
Enumeration enum = _header.getValues(name,separators);
if (enum==null && _trailer!=null)
enum=_trailer.getValues(name,separators);
return enum;
}
/* ------------------------------------------------------------ */
/* Which fields to set?
* If the message is editable, then a header fields are returned.
* Otherwise if the message is sending a HTTP/1.1 message,
* then a trailer field is returned if it has been set.
* @return Header or Trailer fields
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
protected HttpFields setFields()
throws IllegalStateException
{
if (_state==__MSG_EDITABLE)
return _header;
if (_acceptTrailer &&
_state==__MSG_SENDING &&
_version.equals(__HTTP_1_1))
{
if (_trailer==null)
_trailer=new HttpFields();
return _trailer;
}
throw new IllegalStateException("Can't set fields in "+
__state[_state]+
" for "+_version);
}
/* ------------------------------------------------------------ */
/** 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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public String setField(String name, String value)
throws IllegalStateException
{
HttpFields fields=setFields();
if (HttpFields.__ContentType.equalsIgnoreCase(name))
setMimeAndEncoding(value);
return fields.put(name,value);
}
/* ------------------------------------------------------------ */
/** Set the mimeType and CharacterEncodings.
* Normally called from setField("Content-Type",type);
* @param contentType A mimetype with optional char encoding param.
*/
protected void setMimeAndEncoding(String contentType)
{
_characterEncoding=null;
_mimeType=contentType;
if (contentType!=null)
{
int i0=contentType.indexOf(';');
if (i0>=0)
{
_mimeType=contentType.substring(0,i0).trim();
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);
}
}
}
}
/* ------------------------------------------------------------ */
/** 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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void setField(String name, List value)
throws IllegalStateException
{
HttpFields fields=setFields();
fields.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
{
HttpFields fields=setFields();
fields.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)
{
int v=_header.getIntField(name);
if (v==-1 && _trailer!=null)
v=_trailer.getIntField(name);
return v;
}
/* -------------------------------------------------------------- */
/** 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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void setIntField(String name, int value)
throws IllegalStateException
{
setFields().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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void addIntField(String name, int value)
throws IllegalStateException
{
setFields().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)
{
long d=_header.getDateField(name);
if (d<0 && _trailer!=null)
d=_trailer.getDateField(name);
return d;
}
/* -------------------------------------------------------------- */
/** 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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void setDateField(String name, Date date)
{
setFields().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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void addDateField(String name, Date date)
{
setFields().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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public void setDateField(String name, long date)
{
setFields().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)
{
setFields().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
* @exception IllegalStateException Not editable or sending 1.1
* with trailers
*/
public String removeField(String name)
throws IllegalStateException
{
HttpFields fields=setFields();
return fields.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;
}
/* ------------------------------------------------------------ */
/** Get the HTTP chunked trailer (also called trailer).
* @return Trailer or null
*/
public HttpFields getTrailer()
{
if (_state!=__MSG_EDITABLE)
throw new IllegalStateException("Can't get trailer in "+__state[_state]);
if (_acceptTrailer && _trailer==null)
_trailer=new HttpFields();
return _trailer;
}
/* ------------------------------------------------------------ */
/** Set if trailers are accepted.
* @param acceptTrailer If true, setField() may use trailers.
*/
public void setAcceptTrailer(boolean acceptTrailer)
{
_acceptTrailer=acceptTrailer;
}
/* ------------------------------------------------------------ */
/** Set if trailers are accepted.
*/
public boolean acceptTrailer()
{
return _acceptTrailer;
}
/* -------------------------------------------------------------- */
/** 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)
{
_characterEncoding=encoding;
}
/* -------------------------------------------------------------- */
public int getContentLength()
{
return getIntField(HttpFields.__ContentLength);
}
/* ------------------------------------------------------------ */
public void setContentLength(int len)
{
setIntField(HttpFields.__ContentLength,len);
}
/* -------------------------------------------------------------- */
public String getContentType()
{
return getField(HttpFields.__ContentType);
}
/* ------------------------------------------------------------ */
public void setContentType(String contentType)
{
setField(HttpFields.__ContentType,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=null;
_dotVersion=0;
_header.clear();
if (_trailer!=null)
_trailer.destroy();
_trailer=null;
_acceptTrailer=false;
_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;
_trailer=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)
{
Code.warning(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);
}
}