/** * Copyright 2008 ThimbleWare Inc. * * 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 com.thimbleware.jmemcached; import static com.thimbleware.jmemcached.CommandDecoder.SessionState.ERROR; import static com.thimbleware.jmemcached.CommandDecoder.SessionState.READY; import static com.thimbleware.jmemcached.CommandDecoder.SessionState.WAITING_FOR_DATA; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.ArrayList; import java.util.List; import org.apache.mina.common.ByteBuffer; import org.apache.mina.common.IoSession; import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.demux.MessageDecoderAdapter; import org.apache.mina.filter.codec.demux.MessageDecoderResult; /** * MINA MessageDecoderAdapter responsible for parsing inbound lines from the memcached protocol session. */ public final class CommandDecoder extends MessageDecoderAdapter { private final static int THIRTY_DAYS = 60 * 60 * 24 * 30; public static CharsetDecoder DECODER = Charset.forName("US-ASCII").newDecoder(); private static final int WORD_BUFFER_INIT_SIZE = 16; private static final String SESSION_STATUS = "sessionStatus"; /** * Possible states that the current session is in. */ enum SessionState { ERROR, WAITING_FOR_DATA, READY } /** * Object for holding the current session status. */ final class SessionStatus implements Serializable { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; // the state the session is in public SessionState state; // if we are waiting for more data, how much? public int bytesNeeded; // the current working command public CommandMessage cmd; SessionStatus(SessionState state) { this.state = state; } SessionStatus(SessionState state, int bytesNeeded, CommandMessage cmd) { this.state = state; this.bytesNeeded = bytesNeeded; this.cmd = cmd; } } /** * Checks the specified buffer is decodable by this decoder. * * In our case checks the session state to see if we are waiting for data. If we are, make sure * that we actually have all the data we need. * * @return {@link #OK} if this decoder can decode the specified buffer. * {@link #NOT_OK} if this decoder cannot decode the specified buffer. * {@link #NEED_DATA} if more data is required to determine if the * specified buffer is decodable ({@link #OK}) or not decodable * {@link #NOT_OK}. */ public final MessageDecoderResult decodable(IoSession session, ByteBuffer in) { // ask the session for its state, SessionStatus sessionStatus = (SessionStatus) session.getAttribute(SESSION_STATUS); if (sessionStatus != null && sessionStatus.state == WAITING_FOR_DATA) { if (in.remaining() < sessionStatus.bytesNeeded + 2) return MessageDecoderResult.NEED_DATA; } return MessageDecoderResult.OK; } /** * Actually decodes inbound data from the memcached protocol session. * * MINA invokes {@link #decode(IoSession, ByteBuffer, ProtocolDecoderOutput)} * method with read data, and then the decoder implementation puts decoded * messages into {@link ProtocolDecoderOutput}. * * @return {@link #OK} if finished decoding messages successfully. * {@link #NEED_DATA} if you need more data to finish decoding current message. * {@link #NOT_OK} if you cannot decode current message due to protocol specification violation. * * @throws Exception if the read data violated protocol specification */ public final MessageDecoderResult decode(IoSession session, ByteBuffer in, ProtocolDecoderOutput out) throws Exception { SessionStatus sessionStatus = (SessionStatus) session.getAttribute(SESSION_STATUS); SessionStatus returnedSessionStatus; if (sessionStatus != null && sessionStatus.state == WAITING_FOR_DATA) { if (in.remaining() < sessionStatus.bytesNeeded + 2) { return MessageDecoderResult.NEED_DATA; } // get the bytes we want, and that's it byte[] buffer = new byte[sessionStatus.bytesNeeded]; in.get(buffer); // eat crlf at end String crlf = in.getString(2, DECODER); if (crlf.equals("\r\n")) returnedSessionStatus = continueSet(session, out, sessionStatus, buffer); else { session.setAttribute(SESSION_STATUS, new SessionStatus(READY)); return MessageDecoderResult.NOT_OK; } } else { // retrieve the first line of the input, if there isn't a full one, request more StringBuffer wordBuffer = new StringBuffer(WORD_BUFFER_INIT_SIZE); ArrayList<String> words = new ArrayList<String>(8); in.mark(); boolean completed = false; for (int i = 0; in.hasRemaining();) { char c = (char) in.get(); if (c == ' ') { words.add(wordBuffer.toString()); wordBuffer = new StringBuffer(WORD_BUFFER_INIT_SIZE); i++; } else if (c == '\r' && in.hasRemaining() && in.get() == (byte) '\n') { if (wordBuffer.length() != 0) words.add(wordBuffer.toString()); completed = true; break; } else { wordBuffer.append(c); i++; } } if (!completed) { in.reset(); return MessageDecoderResult.NEED_DATA; } returnedSessionStatus = processLine(words, session, out); } if (returnedSessionStatus.state != ERROR) { session.setAttribute(SESSION_STATUS, returnedSessionStatus); return MessageDecoderResult.OK; } else return MessageDecoderResult.NOT_OK; } /** * Process an individual completel 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 session the MINA IoSession * @param out the MINA protocol decoder output to pass our command on to * @return the session status we want to set the session to */ private SessionStatus processLine(List<String> parts, IoSession session, ProtocolDecoderOutput out) { CommandMessage cmd = new CommandMessage(parts.get(0).toUpperCase().intern()); if (cmd.cmd == Commands.ADD || cmd.cmd == Commands.SET || cmd.cmd == Commands.REPLACE || cmd.cmd == Commands.CAS || cmd.cmd == Commands.APPEND || cmd.cmd == Commands.PREPEND) { // if we don't have all the parts, it's malformed if (parts.size() < 5) { return new SessionStatus(ERROR); } int size = Integer.parseInt(parts.get(4)); cmd.element = new MCElement(); cmd.element.keystring = parts.get(1); cmd.element.flags = parts.get(2); cmd.element.expire = Integer.parseInt(parts.get(3)); if (cmd.element.expire != 0 && cmd.element.expire <= THIRTY_DAYS) { cmd.element.expire += Now(); } cmd.element.data_length = size; // look for cas and "noreply" elements if (parts.size() > 5) { int noreply = cmd.cmd == Commands.CAS ? 6 : 5; if (cmd.cmd == Commands.CAS) { cmd.cas_key = Long.valueOf(parts.get(5)); } if (parts.size() == noreply + 1 && parts.get(noreply).equalsIgnoreCase("noreply")) cmd.noreply = true; } return new SessionStatus(WAITING_FOR_DATA, size, cmd); } else if (cmd.cmd == Commands.GET || cmd.cmd == Commands.GETS || cmd.cmd == Commands.STATS || cmd.cmd == Commands.QUIT || cmd.cmd == Commands.VERSION) { // CMD <options>* cmd.keys.addAll(parts.subList(1, parts.size())); out.write(cmd); } else if (cmd.cmd == Commands.INCR || cmd.cmd == Commands.DECR) { if (parts.size() < 2 || parts.size() > 3) return new SessionStatus(ERROR); cmd.keys.add(parts.get(1)); cmd.keys.add(parts.get(2)); if (parts.size() == 3 && parts.get(2).equalsIgnoreCase("noreply")) { cmd.noreply = true; } out.write(cmd); } else if (cmd.cmd == Commands.DELETE) { cmd.keys.add(parts.get(1)); if (parts.size() >= 2) { if (parts.get(parts.size() - 1).equalsIgnoreCase("noreply")) { cmd.noreply = true; if (parts.size() == 4) cmd.time = Integer.valueOf(parts.get(2)); } else if (parts.size() == 3) cmd.time = Integer.valueOf(parts.get(2)); } out.write(cmd); } else if (cmd.cmd == Commands.FLUSH_ALL) { if (parts.size() >= 1) { if (parts.get(parts.size() - 1).equalsIgnoreCase("noreply")) { cmd.noreply = true; if (parts.size() == 3) cmd.time = Integer.valueOf(parts.get(1)); } else if (parts.size() == 2) cmd.time = Integer.valueOf(parts.get(1)); } out.write(cmd); } else { return new SessionStatus(ERROR); } return new SessionStatus(READY); } /** * Handles the continuation of a SET/ADD/REPLACE command with the data it was waiting for. * * @param session the MINA IoSession * @param out the MINA protocol decoder output which we signal with the completed command * @param state the current session status (unused) * @param remainder the bytes picked up * @return the new status to set the session to */ private SessionStatus continueSet(IoSession session, ProtocolDecoderOutput out, SessionStatus state, byte[] remainder) { state.cmd.element.data = remainder; out.write(state.cmd); return new SessionStatus(READY); } /** * @return the current time in seconds */ public final int Now() { return (int) (System.currentTimeMillis() / 1000); } }