//CHECKSTYLE:OFF /** * This file is adopted from https://code.google.com/p/jmemcache-daemon/ in order to fix a ascii * fragmentation issue: https://code.google.com/p/jmemcache-daemon/issues/detail?id=32 */ package com.thimbleware.jmemcached.protocol.text; import com.thimbleware.jmemcached.CacheElement; import com.thimbleware.jmemcached.Key; import com.thimbleware.jmemcached.LocalCacheElement; import com.thimbleware.jmemcached.protocol.CommandMessage; import com.thimbleware.jmemcached.protocol.Op; import com.thimbleware.jmemcached.protocol.SessionStatus; import com.thimbleware.jmemcached.protocol.exceptions.IncorrectlyTerminatedPayloadException; import com.thimbleware.jmemcached.protocol.exceptions.InvalidProtocolStateException; import com.thimbleware.jmemcached.protocol.exceptions.MalformedCommandException; import com.thimbleware.jmemcached.protocol.exceptions.UnknownCommandException; import com.thimbleware.jmemcached.util.BufferUtils; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferIndexFinder; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.*; import org.jboss.netty.handler.codec.frame.FrameDecoder; import java.util.ArrayList; import java.util.List; /** * The MemcachedCommandDecoder is responsible for taking lines from the MemcachedFrameDecoder and parsing them * into CommandMessage instances for handling by the MemcachedCommandHandler * <p/> * Protocol status is held in the SessionStatus instance which is shared between each of the decoders in the pipeline. */ public final class MemcachedCommandDecoder extends FrameDecoder { private static final int MIN_BYTES_LINE = 2; private SessionStatus status; private static final ChannelBuffer NOREPLY = ChannelBuffers.wrappedBuffer("noreply".getBytes()); public MemcachedCommandDecoder(SessionStatus status) { this.status = status; } /** * Index finder which locates a byte which is neither a {@code CR ('\r')} * nor a {@code LF ('\n')}. */ static ChannelBufferIndexFinder CRLF_OR_WS = new ChannelBufferIndexFinder() { public final boolean find(ChannelBuffer buffer, int guessedIndex) { byte b = buffer.getByte(guessedIndex); return b == ' ' || b == '\r' || b == '\n'; } }; static boolean eol(int pos, ChannelBuffer buffer) { return buffer.readableBytes() - pos >= MIN_BYTES_LINE && buffer.getByte(buffer.readerIndex() + pos) == '\r' && buffer.getByte(buffer.readerIndex() + pos+1) == '\n'; } @Override protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception { if (status.state == SessionStatus.State.READY) { ChannelBuffer in = buffer.slice(); // split into pieces List<ChannelBuffer> pieces = new ArrayList<ChannelBuffer>(6); if (in.readableBytes() < MIN_BYTES_LINE) return null; int pos = in.bytesBefore(CRLF_OR_WS); boolean eol = false; do { if (pos != -1) { eol = eol(pos, in); int skip = eol ? MIN_BYTES_LINE : 1; ChannelBuffer slice = in.slice(in.readerIndex(), pos); slice.readerIndex(0); pieces.add(slice); in.skipBytes(pos + skip); if (eol) break; } } while ((pos = in.bytesBefore(CRLF_OR_WS)) != -1); if (eol) { buffer.skipBytes(in.readerIndex()); return processLine(pieces, channel, ctx); } if (status.state != SessionStatus.State.WAITING_FOR_DATA) status.ready(); } else if (status.state == SessionStatus.State.WAITING_FOR_DATA) { if (buffer.readableBytes() >= status.bytesNeeded + MemcachedResponseEncoder.CRLF.capacity()) { // verify delimiter matches at the right location ChannelBuffer dest = buffer.slice(buffer.readerIndex() + status.bytesNeeded, MIN_BYTES_LINE); if (!dest.equals(MemcachedResponseEncoder.CRLF)) { // before we throw error... we're ready for the next command status.ready(); // error, no delimiter at end of payload throw new IncorrectlyTerminatedPayloadException("payload not terminated correctly"); } else { status.processingMultiline(); // There's enough bytes in the buffer and the delimiter is at the end. Read it. ChannelBuffer result = buffer.copy(buffer.readerIndex(), status.bytesNeeded); buffer.skipBytes(status.bytesNeeded + MemcachedResponseEncoder.CRLF.capacity()); CommandMessage commandMessage = continueSet(channel, status, result, ctx); if (status.state != SessionStatus.State.WAITING_FOR_DATA) status.ready(); return commandMessage; } } } else { throw new InvalidProtocolStateException("invalid protocol state"); } return null; } /** * Process an individual complete protocol line and either passes the command for processing by the * session handler, or (in the case of SET-type commands) partially parses the command and sets the session into * a state to wait for additional data. * * @param parts the (originally space separated) parts of the command * @param channel the netty channel to operate on * @param channelHandlerContext the netty channel handler context * @throws com.thimbleware.jmemcached.protocol.exceptions.MalformedCommandException * @throws com.thimbleware.jmemcached.protocol.exceptions.UnknownCommandException */ private Object processLine(List<ChannelBuffer> parts, Channel channel, ChannelHandlerContext channelHandlerContext) throws UnknownCommandException, MalformedCommandException { final int numParts = parts.size(); // Turn the command into an enum for matching on Op op; try { op = Op.FindOp(parts.get(0)); if (op == null) throw new IllegalArgumentException("unknown operation: " + parts.get(0).toString()); } catch (IllegalArgumentException e) { throw new UnknownCommandException("unknown operation: " + parts.get(0).toString()); } // Produce the initial command message, for filling in later CommandMessage cmd = CommandMessage.command(op); switch (op) { case DELETE: cmd.setKey(parts.get(1)); if (numParts >= MIN_BYTES_LINE) { if (parts.get(numParts - 1).equals(NOREPLY)) { cmd.noreply = true; if (numParts == 4) cmd.time = BufferUtils.atoi(parts.get(MIN_BYTES_LINE)); } else if (numParts == 3) cmd.time = BufferUtils.atoi(parts.get(MIN_BYTES_LINE)); } return cmd; case DECR: case INCR: // Malformed if (numParts < MIN_BYTES_LINE || numParts > 3) throw new MalformedCommandException("invalid increment command"); cmd.setKey(parts.get(1)); cmd.incrAmount = BufferUtils.atoi(parts.get(MIN_BYTES_LINE)); if (numParts == 3 && parts.get(MIN_BYTES_LINE).equals(NOREPLY)) { cmd.noreply = true; } return cmd; case FLUSH_ALL: if (numParts >= 1) { if (parts.get(numParts - 1).equals(NOREPLY)) { cmd.noreply = true; if (numParts == 3) cmd.time = BufferUtils.atoi((parts.get(1))); } else if (numParts == MIN_BYTES_LINE) cmd.time = BufferUtils.atoi((parts.get(1))); } return cmd; case VERBOSITY: // verbosity <time> [noreply]\r\n // Malformed if (numParts < MIN_BYTES_LINE || numParts > 3) throw new MalformedCommandException("invalid verbosity command"); cmd.time = BufferUtils.atoi((parts.get(1))); // verbose level if (numParts > 1 && parts.get(MIN_BYTES_LINE).equals(NOREPLY)) cmd.noreply = true; return cmd; case APPEND: case PREPEND: case REPLACE: case ADD: case SET: case CAS: // if we don't have all the parts, it's malformed if (numParts < 5) { throw new MalformedCommandException("invalid command length"); } // Fill in all the elements of the command int size = BufferUtils.atoi(parts.get(4)); int expire = BufferUtils.atoi(parts.get(3)); int flags = BufferUtils.atoi(parts.get(MIN_BYTES_LINE)); cmd.element = new LocalCacheElement(new Key(parts.get(1).slice()), flags, expire != 0 && expire < CacheElement.THIRTY_DAYS ? LocalCacheElement.Now() + expire : expire, 0L); // look for cas and "noreply" elements if (numParts > 5) { int noreply = op == Op.CAS ? 6 : 5; if (op == Op.CAS) { cmd.cas_key = BufferUtils.atol(parts.get(5)); } if (numParts == noreply + 1 && parts.get(noreply).equals(NOREPLY)) cmd.noreply = true; } // Now indicate that we need more for this command by changing the session status's state. // This instructs the frame decoder to start collecting data for us. status.needMore(size, cmd); break; // case GET: case GETS: case STATS: case VERSION: case QUIT: // Get all the keys cmd.setKeys(parts.subList(1, numParts)); // Pass it on. return cmd; default: throw new UnknownCommandException("unknown command: " + op); } return null; } /** * Handles the continuation of a SET/ADD/REPLACE command with the data it was waiting for. * * @param channel netty channel * @param state the current session status (unused) * @param remainder the bytes picked up * @param channelHandlerContext netty channel handler context */ private CommandMessage continueSet(Channel channel, SessionStatus state, ChannelBuffer remainder, ChannelHandlerContext channelHandlerContext) { state.cmd.element.setData(remainder); return state.cmd; } }