package redis.netty4; import com.google.common.base.Charsets; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; import java.io.IOException; import java.util.List; /** * Netty codec for Redis */ public class RedisReplyDecoder extends ReplayingDecoder<Void> { public static final char CR = '\r'; public static final char LF = '\n'; private static final char ZERO = '0'; // We track the current multibulk reply in the case // where we do not get a complete reply in a single // decode invocation. private MultiBulkReply reply; public RedisReplyDecoder() { this(true); } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> objects) throws Exception { objects.add(receive(byteBuf)); } public RedisReplyDecoder(boolean checkpointEnabled) { this.checkpointEnabled = checkpointEnabled; } private boolean checkpointEnabled; public ByteBuf readBytes(ByteBuf is) throws IOException { long l = readLong(is); if (l > Integer.MAX_VALUE) { throw new IllegalArgumentException("Java only supports arrays up to " + Integer.MAX_VALUE + " in size"); } int size = (int) l; if (size == -1) { return null; } ByteBuf buffer = is.readSlice(size); int cr = is.readByte(); int lf = is.readByte(); if (cr != CR || lf != LF) { throw new IOException("Improper line ending: " + cr + ", " + lf); } return buffer; } public static long readLong(ByteBuf is) throws IOException { long size = 0; int sign = 1; int read = is.readByte(); if (read == '-') { read = is.readByte(); sign = -1; } do { if (read == CR) { if (is.readByte() == LF) { break; } } int value = read - ZERO; if (value >= 0 && value < 10) { size *= 10; size += value; } else { throw new IOException("Invalid character in integer"); } read = is.readByte(); } while (true); return size * sign; } public Reply receive(final ByteBuf is) throws IOException { if (reply != null) { return decodeMultiBulkReply(is); } return readReply(is); } public Reply readReply(ByteBuf is) throws IOException { int code = is.readByte(); switch (code) { case StatusReply.MARKER: { String status = is.readBytes(is.bytesBefore((byte) '\r')).toString(Charsets.UTF_8); is.skipBytes(2); return new StatusReply(status); } case ErrorReply.MARKER: { String error = is.readBytes(is.bytesBefore((byte) '\r')).toString(Charsets.UTF_8); is.skipBytes(2); return new ErrorReply(error); } case IntegerReply.MARKER: { return new IntegerReply(readLong(is)); } case BulkReply.MARKER: { return new BulkReply(readBytes(is)); } case MultiBulkReply.MARKER: { if (reply == null) { return decodeMultiBulkReply(is); } else { return new RedisReplyDecoder(false).decodeMultiBulkReply(is); } } default: { throw new IOException("Unexpected character in stream: " + code); } } } @Override public void checkpoint() { if (checkpointEnabled) { super.checkpoint(); } } public MultiBulkReply decodeMultiBulkReply(ByteBuf is) throws IOException { try { if (reply == null) { reply = new MultiBulkReply(); checkpoint(); } reply.read(this, is); return reply; } finally { reply = null; } } }