/*********************************************************************
* HttpHeaderParser.java
* created on 02.04.2006 by netseeker
* $Source: /cvsroot/ejoe/EJOE/src/de/netseeker/ejoe/http/HttpHeaderParser.java,v $
* $Date: 2007/11/17 10:57:01 $
* $Revision: 1.2 $
*
* ====================================================================
*
* Copyright 2006 netseeker aka Michael Manske
*
* 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.
* ====================================================================
*
* This file is part of the de.netseeker.ejoe.http framework.
* For more information on the author, please see
* <http://www.manskes.de/>.
*
*********************************************************************/
package de.netseeker.ejoe.http;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.netseeker.ejoe.cache.ByteBufferAllocator;
/**
* This is the main HTTP header parser of EJOE. It's able to validate an ByteBuffer if it contains a valid HTTP header
* and can detect:
* <ul>
* <li>the end of the HTTP header</li>
* <li>the length of the HTTP header in bytes</li>
* <li>upcoming content length</li>
* </ul>
* It's not possible to create an instance directly, instead use HttpRequestParser or HttpResponseParser to create
* concrete parser instances.
*
* @author netseeker
* @since 0.3.9.1
*/
public class HttpHeaderParser
{
private static final Logger logger = Logger.getLogger( HttpHeaderParser.class.getName() );
protected static final String LINE_SEP = "\r\n";
protected static final Charset csISO = Charset.forName( "ISO-8859-1" );
protected static final Pattern pLineEnd = Pattern.compile( "$", Pattern.MULTILINE );
protected static final Pattern pContentLength = Pattern.compile( ".*^Content-Length:\\s([^\\s]+)$.*",
Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE
| Pattern.DOTALL );
protected static final Pattern pConnection = Pattern.compile( ".*^Connection:\\s([^\\s]+)$.*",
Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE
| Pattern.DOTALL );
private int contentLength;
private int headerLength;
private CharBuffer charHeader;
private ByteBuffer byteHeader;
private boolean hasPrereadContent = false;
private boolean isPersistentConnection = false;
private boolean hasCompression = false;
/**
* Hidden constructor, only child classes are permitted to create a new instance
*
* @param buf ByteBuffer expected to contain a HTTP header [ + optional preread content ]
*/
protected HttpHeaderParser(ByteBuffer buf)
{
this.byteHeader = buf;
this.charHeader = csISO.decode( buf );
this.byteHeader.rewind();
this.contentLength = extractContentLength();
this.isPersistentConnection = isConnectionPersistent();
this.headerLength = extractHeaderLength( buf );
this.hasPrereadContent = (byteHeader.limit() > headerLength);
if ( this.hasPrereadContent )
{
resizeByteHeaderForFurtherReading();
}
}
/**
* Returns the content length
*
* @return the content length
*/
public int getContentLength()
{
return contentLength;
}
/**
* Returns the length of the HTTP header in bytes without any preread content
*
* @return the length of the header
*/
public int getHeaderLength()
{
return headerLength;
}
/**
* Checks if the HTTP header seems to be valid
*
* @return true if the HTTP header seems to be valid (at least for EJOE) otherwise false
*/
public boolean isValid()
{
return ((headerLength > 0) && (contentLength >= 0));
}
/**
* @return The decoded underlying buffer
*/
public CharBuffer getCharHeader()
{
return charHeader;
}
/**
* @return the underlying ByteBuffer
*/
public ByteBuffer getByteHeader()
{
return byteHeader;
}
/**
* @return true if the underlying ByteBuffer already contains one or more preread bytes of content data
*/
public boolean hasPrereadContent()
{
return hasPrereadContent;
}
/**
* Indicates whether the value for the HTTP header line "Connection:" was "close" or "open".
*
* @return true if the HTTP header did contain a "Connection:" header line and the value was "open" otherwise false
*/
public boolean isPersistentConnection()
{
return isPersistentConnection;
}
/**
* Returns the compression setting in the HTTP-Header.
*
* @return true if the HTTP header requested use of GZIP compression otherwise false
*/
public boolean hasCompression()
{
return hasCompression;
}
protected void setCompression( boolean enable )
{
hasCompression = enable;
}
/**
* Determines the length of the expected content as stated in the "Content-Length:" section of the header
*
* @return the content length
*/
protected int extractContentLength()
{
try
{
Matcher matcher = pContentLength.matcher( getCharHeader() );
if ( matcher.matches() )
{
return Integer.parseInt( matcher.group( 1 ).trim() );
}
}
catch ( Exception e )
{
logger.log( Level.WARNING, "Failed to determine content length!", e );
}
return 0;
}
/**
* Checks if the client has requested a persistent connection
*
* @return true if the client requested a persitent connection otherwise false
*/
protected boolean isConnectionPersistent()
{
try
{
Matcher matcher = pConnection.matcher( getCharHeader() );
if ( matcher.matches() )
{
return matcher.group( 1 ).trim().equalsIgnoreCase( "keep-alive" );
}
}
catch ( Exception e )
{
logger.log( Level.WARNING, "Failed to determine content length!", e );
}
return false;
}
/**
* Resizes and repositions the internal ByteBuffer for upcoming read operations which will append content data to
* the buffer
*/
private void resizeByteHeaderForFurtherReading()
{
this.byteHeader.position( headerLength );
this.byteHeader.compact();
if ( this.byteHeader.capacity() < getContentLength() )
{
this.byteHeader = ByteBufferAllocator.reAllocate( this.byteHeader, getContentLength() );
}
if ( this.byteHeader.limit() != getContentLength() )
{
this.byteHeader.limit( getContentLength() );
}
}
/**
* Searches the ByteBuffer for two line breaks (either "\r\n\r\n" or "\n\n") to detect the end of HTTP header data
*
* @param bb ByteBuffer containing HTTP header data
* @return the length of the HTTP header in bytes
*/
protected static int extractHeaderLength( ByteBuffer bb )
{
int length = -1;
if ( bb.limit() >= 3 )
{
int pos = bb.position();
for ( int i = 3; i < bb.limit(); i++ )
{
if ( ((bb.get( i - 3 ) == '\r') && (bb.get( i - 2 ) == '\n') && (bb.get( i - 1 ) == '\r') && (bb
.get( i ) == '\n'))
|| ((bb.get( i - 1 ) == '\n') && (bb.get( i ) == '\n')) )
{
length = i + 1;
break;
}
}
bb.position( pos );
}
return length;
}
/**
* Checks if the HTTP is complete. A header is then complete if either "\r\n\r\n" or "\n\n" are found within the
* given ByteBuffer
*
* @param bb ByteBuffer containing HTTP header data
* @return true if the HTTP header is complete (the buffer contains a terminated HTTP header [ + optional prearead
* content ]) otherwise false
*/
public static boolean isComplete( ByteBuffer bb )
{
int pos = bb.position();
int limit = bb.limit();
bb.flip();
boolean complete = (extractHeaderLength( bb ) != -1);
bb.limit( limit );
bb.position( pos );
return complete;
}
}