/* * Copyright (c) 2014-2015 Spotify AB * * 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.spotify.folsom.client.ascii; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.List; public class AsciiMemcacheDecoder extends ByteToMessageDecoder { private static final int MAX_RESPONSE_LINE = 500; private final ByteBuffer line = ByteBuffer.allocate(MAX_RESPONSE_LINE); private final ByteBuffer token = ByteBuffer.allocate(MAX_RESPONSE_LINE); private final Charset charset; private boolean valueMode = false; private ValueAsciiResponse valueResponse = new ValueAsciiResponse(); private boolean consumed; private byte[] key = null; private byte[] value = null; private long cas = 0; private int valueOffset; public AsciiMemcacheDecoder(Charset charset) { this.charset = charset; } @Override protected void decode(final ChannelHandlerContext ctx, final ByteBuf buf, final List<Object> out) throws Exception { while (true) { int readableBytes = buf.readableBytes(); if (readableBytes == 0) { return; } if (key != null) { final int toCopy = Math.min(value.length - valueOffset, readableBytes); if (toCopy > 0) { buf.readBytes(value, valueOffset, toCopy); readableBytes -= toCopy; valueOffset += toCopy; if (valueOffset < value.length) { return; } } final ByteBuffer line = readLine(buf, readableBytes); if (line == null) { return; } if (line.remaining() > 0) { throw new IOException(String.format("Unexpected end of data block: %s", lineToString())); } valueResponse.addGetResult(key, value, cas); key = null; value = null; cas = 0; } else { final ByteBuffer line = readLine(buf, readableBytes); if (line == null) { return; } readNextToken(); int tokenLength = token.remaining(); if (tokenLength < 1) { throw fail(); } final byte firstChar = token.get(); if (Character.isDigit(firstChar)) { try { long numeric = parseLong(firstChar, token); out.add(new NumericAsciiResponse(numeric)); } catch (NumberFormatException e) { throw new IOException("Unexpected line: " + line, e); } } else if (tokenLength == 3) { expect(firstChar, "END"); out.add(valueResponse); valueResponse = new ValueAsciiResponse(); valueMode = false; return; } else if (tokenLength == 5) { expect(firstChar, "VALUE"); valueMode = true; // VALUE <key> <flags> <bytes> [<cas unique>]\r\n // key readNextToken(); int keyLen = token.remaining(); if (keyLen <= 0) { throw fail(); } byte[] key = new byte[token.remaining()]; token.get(key); // flags readNextToken(); int flagLen = token.remaining(); if (flagLen <= 0) { throw fail(); } // size readNextToken(); int sizeLen = token.remaining(); if (sizeLen <= 0) { throw fail(); } final int size = (int) parseLong(token.get(), token); // cas readNextToken(); int casLen = token.remaining(); long cas = 0; if (casLen > 0) { cas = parseLong(token.get(), token); } this.key = key; this.value = new byte[size]; this.valueOffset = 0; this.cas = cas; } else if (valueMode) { // when in valueMode, the only valid responses are "END" and "VALUE" throw fail(); } else if (tokenLength == 6) { if (firstChar == 'S') { expect(firstChar, "STORED"); out.add(AsciiResponse.STORED); return; } else { expect(firstChar, "EXISTS"); out.add(AsciiResponse.EXISTS); return; } } else if (tokenLength == 7) { if (firstChar == 'T') { expect(firstChar, "TOUCHED"); out.add(AsciiResponse.TOUCHED); return; } else { expect(firstChar, "DELETED"); out.add(AsciiResponse.DELETED); return; } } else if (tokenLength == 9) { expect(firstChar, "NOT_FOUND"); out.add(AsciiResponse.NOT_FOUND); return; } else if (tokenLength == 10) { expect(firstChar, "NOT_STORED"); out.add(AsciiResponse.NOT_STORED); return; } else { throw fail(); } } } } private void readNextToken() { token.clear(); while (line.hasRemaining()) { byte b = line.get(); if (b == ' ') { break; } token.put(b); } token.flip(); } private IOException fail() { return new IOException("Unexpected line: " + lineToString()); } private String lineToString() { line.rewind(); return new String(line.array(), 0, line.remaining(), charset); } private void expect(byte firstChar, final String compareTo) throws IOException { if (firstChar != compareTo.charAt(0)) { throw fail(); } final int length = compareTo.length(); if (length != token.remaining() + 1) { throw fail(); } for (int i = 1; i < length; i++) { if (token.get() != compareTo.charAt(i)) { throw fail(); } } } private long parseLong(byte firstChar, ByteBuffer token) throws IOException { // firstChar must be guarantee to be a digit. long res = firstChar - '0'; if (res < 0 || res > 9) { throw fail(); } while (token.hasRemaining()) { final int digit = token.get() - '0'; if (digit < 0 || digit > 9) { throw fail(); } res *= 10; res += digit; } return res; } private ByteBuffer readLine(final ByteBuf buf, final int available) throws IOException { if (consumed) { consumed = false; line.clear(); } for (int i = 0; i < available - 1; i++) { final byte b = buf.readByte(); if (b == '\r') { if (buf.readByte() == '\n') { consumed = true; line.flip(); return line; } throw new IOException("Expected newline, got something else"); } line.put(b); } return null; } }