/*
* JBoss, Home of Professional Open Source
*
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* by the @author tags. See the COPYRIGHT.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.netty.handler.codec.frame;
import java.lang.reflect.Array;
import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicReference;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
/**
* Decodes the received {@link ChannelBuffer}s into a meaningful frame object.
* <p>
* In a stream-based transport such as TCP/IP, packets can be fragmented and
* reassembled during transmission even in a LAN environment. For example,
* let us assume you have received three packets:
* <pre>
* +-----+-----+-----+
* | ABC | DEF | GHI |
* +-----+-----+-----+
* </pre>
* because of the packet fragmentation, a server can receive them like the
* following:
* <pre>
* +----+-------+---+---+
* | AB | CDEFG | H | I |
* +----+-------+---+---+
* </pre>
* <p>
* {@link FrameDecoder} helps you defrag the received packets into one or more
* meaningful <strong>frames</strong> that could be easily understood by the
* application logic. In case of the example above, your {@link FrameDecoder}
* implementation could defrag the received packets like the following:
* <pre>
* +-----+-----+-----+
* | ABC | DEF | GHI |
* +-----+-----+-----+
* </pre>
* <p>
* The following code shows an example handler which decodes a frame whose
* first 4 bytes header represents the length of the frame, excluding the
* header.
* <pre>
* MESSAGE FORMAT
* ==============
*
* Offset: 0 4 (Length + 4)
* +--------+------------------------+
* Fields: | Length | Actual message content |
* +--------+------------------------+
*
* DECODER IMPLEMENTATION
* ======================
*
* public class IntegerHeaderFrameDecoder extends FrameDecoder {
*
* protected Object decode(ChannelHandlerContext ctx,
* Channel channel,
* ChannelBuffer buf) throws Exception {
*
* // Make sure if the length field was received.
* if (buf.readableBytes() < 4) {
* // The length field was not received yet - return null.
* // This method will be invoked again when more packets are
* // received and appended to the buffer.
* return <strong>null</strong>;
* }
*
* // The length field is in the buffer.
*
* // Mark the current buffer position before reading the length field
* // because the whole frame might not be in the buffer yet.
* // We will reset the buffer position to the marked position if
* // there's not enough bytes in the buffer.
* buf.markReaderIndex();
*
* // Read the length field.
* int length = buf.readInt();
*
* // Make sure if there's enough bytes in the buffer.
* if (buf.readableBytes() < length) {
* // The whole bytes were not received yet - return null.
* // This method will be invoked again when more packets are
* // received and appended to the buffer.
*
* // Reset to the marked position to read the length field again
* // next time.
* buf.resetReaderIndex();
*
* return <strong>null</strong>;
* }
*
* // There's enough bytes in the buffer. Read it.
* ChannelBuffer frame = buf.readBytes(length);
*
* // Successfully decoded a frame. Return the decoded frame.
* return <strong>frame</strong>;
* }
* }
* </pre>
*
* <h3>Returning a POJO rather than a {@link ChannelBuffer}</h3>
* <p>
* Please note that you can return an object of a different type than
* {@link ChannelBuffer} in your {@code decode()} and {@code decodeLast()}
* implementation. For example, you could return a
* <a href="http://en.wikipedia.org/wiki/POJO">POJO</a> so that the next
* {@link ChannelUpstreamHandler} receives a {@link MessageEvent} which
* contains a POJO rather than a {@link ChannelBuffer}.
*
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)
*
* @version $Rev:231 $, $Date:2008-06-12 16:44:50 +0900 (목, 12 6월 2008) $
*
* @apiviz.landmark
*/
@ChannelPipelineCoverage("one")
public abstract class FrameDecoder extends SimpleChannelUpstreamHandler {
private final boolean unfold;
private final AtomicReference<ChannelBuffer> cumulation =
new AtomicReference<ChannelBuffer>();
protected FrameDecoder() {
this(false);
}
protected FrameDecoder(boolean unfold) {
this.unfold = unfold;
}
@Override
public void messageReceived(
ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object m = e.getMessage();
if (!(m instanceof ChannelBuffer)) {
ctx.sendUpstream(e);
return;
}
ChannelBuffer input = (ChannelBuffer) m;
if (!input.readable()) {
return;
}
ChannelBuffer cumulation = cumulation(ctx);
if (cumulation.readable()) {
cumulation.discardReadBytes();
cumulation.writeBytes(input);
callDecode(ctx, e.getChannel(), cumulation, e.getRemoteAddress());
} else {
callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
if (input.readable()) {
cumulation.writeBytes(input);
}
}
}
@Override
public void channelDisconnected(
ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
cleanup(ctx, e);
}
@Override
public void channelClosed(
ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
cleanup(ctx, e);
}
@Override
public void exceptionCaught(
ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
ctx.sendUpstream(e);
}
/**
* Decodes the received packets so far into a frame.
*
* @param ctx the context of this handler
* @param channel the current channel
* @param buffer the cumulative buffer of received packets so far
*
* @return the decoded frame if a full frame was received and decoded.
* {@code null} if there's not enough data in the buffer to decode a frame.
*/
protected abstract Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception;
/**
* Decodes the received data so far into a frame when the channel is
* disconnected.
*
* @param ctx the context of this handler
* @param channel the current channel
* @param buffer the cumulative buffer of received packets so far
*
* @return the decoded frame if a full frame was received and decoded.
* {@code null} if there's not enough data in the buffer to decode a frame.
*/
protected Object decodeLast(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
return decode(ctx, channel, buffer);
}
private void callDecode(
ChannelHandlerContext context, Channel channel,
ChannelBuffer cumulation, SocketAddress remoteAddress) throws Exception {
while (cumulation.readable()) {
int oldReaderIndex = cumulation.readerIndex();
Object frame = decode(context, channel, cumulation);
if (frame == null) {
if (oldReaderIndex == cumulation.readerIndex()) {
// Seems like more data is required.
// Let us wait for the next notification.
break;
} else {
// Previous data has been discarded.
// Probably it is reading on.
continue;
}
} else if (oldReaderIndex == cumulation.readerIndex()) {
throw new IllegalStateException(
"decode() method must read at least one byte " +
"if it returned a frame.");
}
fireMessageReceived(context, remoteAddress, frame);
}
}
private void fireMessageReceived(ChannelHandlerContext context, SocketAddress remoteAddress, Object result) {
if (unfold) {
if (result instanceof Object[]) {
for (Object r: (Object[]) result) {
Channels.fireMessageReceived(context, r, remoteAddress);
}
} else if (result.getClass().isArray()){
int length = Array.getLength(result);
for (int i = 0; i < length; i ++) {
Channels.fireMessageReceived(context, Array.get(result, i), remoteAddress);
}
} else if (result instanceof Iterable) {
for (Object r: (Iterable<?>) result) {
Channels.fireMessageReceived(context, r, remoteAddress);
}
} else {
Channels.fireMessageReceived(context, result, remoteAddress);
}
} else {
Channels.fireMessageReceived(context, result, remoteAddress);
}
}
private void cleanup(ChannelHandlerContext ctx, ChannelStateEvent e)
throws Exception {
ChannelBuffer cumulation = cumulation(ctx);
try {
if (cumulation.readable()) {
// Make sure all frames are read before notifying a closed channel.
callDecode(ctx, ctx.getChannel(), cumulation, null);
if (cumulation.readable()) {
// and send the remainders too if necessary.
Object partialFrame = decodeLast(ctx, ctx.getChannel(), cumulation);
if (partialFrame != null) {
fireMessageReceived(ctx, null, partialFrame);
}
}
}
} finally {
ctx.sendUpstream(e);
}
}
private ChannelBuffer cumulation(ChannelHandlerContext ctx) {
ChannelBuffer buf = cumulation.get();
if (buf == null) {
buf = ChannelBuffers.dynamicBuffer(
ctx.getChannel().getConfig().getBufferFactory());
if (!cumulation.compareAndSet(null, buf)) {
buf = cumulation.get();
}
}
return buf;
}
}