/*
* Copyright 2010 david varnes.
*
* 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 org.freeswitch.esl.client.transport.message;
import org.freeswitch.esl.client.internal.HeaderParser;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decoder used by the IO processing pipeline. Client consumers should never need to use
* this class.
* <p>
* Follows the following decode algorithm (from FreeSWITCH wiki)
* <pre>
* Look for \n\n in your receive buffer
*
* Examine data for existence of Content-Length
*
* If NOT present, process event and remove from receive buffer
*
* IF present, Shift buffer to remove 'header'
* Evaluate content-length value
*
* Loop until receive buffer size is >= Content-length
* Extract content-length bytes from buffer and process
* </pre>
*
* @author david varnes
*/
public class EslFrameDecoder extends ReplayingDecoder<EslFrameDecoder.State>
{
/**
* Line feed character
*/
static final byte LF = 10;
protected static enum State
{
READ_HEADER,
READ_BODY,
}
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private final int maxHeaderSize;
private EslMessage currentMessage;
private boolean treatUnknownHeadersAsBody = false;
public EslFrameDecoder( int maxHeaderSize )
{
super( State.READ_HEADER );
if (maxHeaderSize <= 0)
{
throw new IllegalArgumentException(
"maxHeaderSize must be a positive integer: " +
maxHeaderSize);
}
this.maxHeaderSize = maxHeaderSize;
}
public EslFrameDecoder( int maxHeaderSize, boolean treatUnknownHeadersAsBody )
{
this( maxHeaderSize );
this.treatUnknownHeadersAsBody = treatUnknownHeadersAsBody;
}
@Override
protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer,
State state ) throws Exception
{
log.trace( "decode() : state [{}]", state );
switch ( state )
{
case READ_HEADER:
if ( currentMessage == null )
{
currentMessage = new EslMessage();
}
/*
* read '\n' terminated lines until reach a single '\n'
*/
boolean reachedDoubleLF = false;
while ( ! reachedDoubleLF )
{
// this will read or fail
String headerLine = readToLineFeedOrFail( buffer, maxHeaderSize );
log.debug( "read header line [{}]", headerLine );
if ( ! headerLine.isEmpty() )
{
// split the header line
String[] headerParts = HeaderParser.splitHeader( headerLine );
Name headerName = Name.fromLiteral( headerParts[0] );
if ( headerName == null )
{
if ( treatUnknownHeadersAsBody )
{
// cache this 'header' as a body line <-- useful for Outbound client mode
currentMessage.addBodyLine( headerLine );
}
else
{
throw new IllegalStateException( "Unhandled ESL header [" + headerParts[0] + ']' );
}
}
currentMessage.addHeader( headerName, headerParts[1] );
}
else
{
reachedDoubleLF = true;
}
// do not read in this line again
checkpoint();
}
// have read all headers - check for content-length
if ( currentMessage.hasContentLength() )
{
checkpoint( State.READ_BODY );
log.debug( "have content-length, decoding body .." );
// force the next section
return null;
}
else
{
// end of message
checkpoint( State.READ_HEADER );
// send message upstream
EslMessage decodedMessage = currentMessage;
currentMessage = null;
return decodedMessage;
}
case READ_BODY:
/*
* read the content-length specified
*/
int contentLength = currentMessage.getContentLength();
ChannelBuffer bodyBytes = buffer.readBytes( contentLength );
log.debug( "read [{}] body bytes", bodyBytes.writerIndex() );
// most bodies are line based, so split on LF
while( bodyBytes.readable() )
{
String bodyLine = readLine( bodyBytes, contentLength );
log.debug( "read body line [{}]", bodyLine );
currentMessage.addBodyLine( bodyLine );
}
// end of message
checkpoint( State.READ_HEADER );
// send message upstream
EslMessage decodedMessage = currentMessage;
currentMessage = null;
return decodedMessage;
default:
throw new Error( "Illegal state: [" + state + ']' );
}
}
private String readToLineFeedOrFail( ChannelBuffer buffer, int maxLineLegth ) throws TooLongFrameException
{
StringBuilder sb = new StringBuilder(64);
while ( true )
{
// this read might fail
byte nextByte = buffer.readByte();
if ( nextByte == LF )
{
return sb.toString();
}
else
{
// Abort decoding if the decoded line is too large.
if ( sb.length() >= maxLineLegth )
{
throw new TooLongFrameException(
"ESL header line is longer than " + maxLineLegth + " bytes.");
}
sb.append( (char) nextByte );
}
}
}
private String readLine( ChannelBuffer buffer, int maxLineLength ) throws TooLongFrameException
{
StringBuilder sb = new StringBuilder(64);
while ( buffer.readable() )
{
// this read should always succeed
byte nextByte = buffer.readByte();
if (nextByte == LF)
{
return sb.toString();
}
else
{
// Abort decoding if the decoded line is too large.
if ( sb.length() >= maxLineLength )
{
throw new TooLongFrameException(
"ESL message line is longer than " + maxLineLength + " bytes.");
}
sb.append( (char) nextByte );
}
}
return sb.toString();
}
}