/**
*Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)]
*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
*/
/**
*Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)]
*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 net.rubyeye.xmemcached.command.text;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import net.rubyeye.xmemcached.command.AssocCommandAware;
import net.rubyeye.xmemcached.command.Command;
import net.rubyeye.xmemcached.command.CommandType;
import net.rubyeye.xmemcached.command.MapReturnValueAware;
import net.rubyeye.xmemcached.command.MergeCommandsAware;
import net.rubyeye.xmemcached.impl.MemcachedTCPSession;
import net.rubyeye.xmemcached.monitor.Constants;
import net.rubyeye.xmemcached.transcoders.CachedData;
import net.rubyeye.xmemcached.utils.ByteUtils;
import com.google.code.yanf4j.buffer.IoBuffer;
/**
* Abstract get command for text protocol
*
* @author dennis
*
*/
public abstract class TextGetCommand extends Command implements
MergeCommandsAware, AssocCommandAware, MapReturnValueAware {
protected Map<String, CachedData> returnValues;
private String currentReturnKey;
private int offset;
/**
*When MemcachedClient merge get commands,those commans which have the same
* key will be merged into one get command.The result command's
* assocCommands contains all these commands with the same key.
*/
protected List<Command> assocCommands;
protected Map<Object, Command> mergeCommands;
public final Map<Object, Command> getMergeCommands() {
return this.mergeCommands;
}
public final void setMergeCommands(Map<Object, Command> mergeCommands) {
this.mergeCommands = mergeCommands;
}
public final List<Command> getAssocCommands() {
return this.assocCommands;
}
public final void setAssocCommands(List<Command> assocCommands) {
this.assocCommands = assocCommands;
}
public TextGetCommand(String key, byte[] keyBytes, CommandType cmdType,
CountDownLatch latch) {
super(key, keyBytes, cmdType, latch);
this.returnValues = new HashMap<String, CachedData>(32);
}
private ParseStatus parseStatus = ParseStatus.NULL;
public static enum ParseStatus {
NULL, VALUE, KEY, FLAG, DATA_LEN, DATA_LEN_DONE, CAS, CAS_DONE, DATA, END
}
protected boolean wasFirst = true;
public ParseStatus getParseStatus() {
return this.parseStatus;
}
public void setParseStatus(ParseStatus parseStatus) {
this.parseStatus = parseStatus;
}
@Override
public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) {
while (true) {
if (buffer == null || !buffer.hasRemaining()) {
return false;
}
switch (this.parseStatus) {
case NULL:
if (buffer.remaining() < 2)
return false;
int pos = buffer.position();
byte first = buffer.get(pos);
byte second = buffer.get(pos + 1);
if (first == 'E' && second == 'N') {
this.parseStatus = ParseStatus.END;
// dispatch result
dispatch();
this.currentReturnKey = null;
continue;
} else if (first == 'V') {
this.parseStatus = ParseStatus.VALUE;
this.wasFirst = false;
continue;
} else {
return decodeError(session, buffer);
}
case END:
// END\r\n
return ByteUtils.stepBuffer(buffer, 5);
case VALUE:
// VALUE[SPACE]
if (ByteUtils.stepBuffer(buffer, 6)) {
this.parseStatus = ParseStatus.KEY;
continue;
} else {
return false;
}
case KEY:
String item = getItem(buffer, ' ');
if (item == null) {
return false;
} else {
this.currentReturnKey = item;
this.returnValues.put(this.currentReturnKey,
new CachedData());
this.parseStatus = ParseStatus.FLAG;
continue;
}
case FLAG:
item = getItem(buffer, ' ');
if (item == null) {
return false;
} else {
final CachedData cachedData = this.returnValues
.get(this.currentReturnKey);
cachedData.setFlag(Integer.parseInt(item));
this.parseStatus = ParseStatus.DATA_LEN;
continue;
}
case DATA_LEN:
item = getItem(buffer, '\r', ' ');
if (item == null) {
return false;
} else {
final CachedData cachedData = this.returnValues
.get(this.currentReturnKey);
cachedData.setCapacity(Integer.parseInt(item));
assert (cachedData.getCapacity() >= 0);
cachedData.setData(new byte[cachedData.getCapacity()]);
this.parseStatus = ParseStatus.DATA_LEN_DONE;
continue;
}
case DATA_LEN_DONE:
if (buffer.remaining() < 1) {
return false;
} else {
pos = buffer.position();
first = buffer.get(pos);
// check if buffer has cas value
if (first == '\n') {
// skip '\n'
buffer.position(pos + 1);
this.parseStatus = ParseStatus.DATA;
continue;
} else {
this.parseStatus = ParseStatus.CAS;
continue;
}
}
case CAS:
// has cas value
item = getItem(buffer, '\r');
if (item == null) {
return false;
} else {
final CachedData cachedData = this.returnValues
.get(this.currentReturnKey);
cachedData.setCas(Long.parseLong(item));
this.parseStatus = ParseStatus.CAS_DONE;
continue;
}
case CAS_DONE:
if (buffer.remaining() < 1) {
return false;
} else {
this.parseStatus = ParseStatus.DATA;
// skip '\n'
buffer.position(buffer.position() + 1);
continue;
}
case DATA:
final CachedData value = this.returnValues
.get(this.currentReturnKey);
int remaining = buffer.remaining();
int remainingCapacity = value.remainingCapacity();
assert (remainingCapacity >= 0);
// Data is not enough,return false
if (remaining < remainingCapacity + 2) {
int length = remaining > remainingCapacity ? remainingCapacity
: remaining;
value.fillData(buffer, length);
return false;
} else if (remainingCapacity > 0) {
value.fillData(buffer, remainingCapacity);
}
assert (value.remainingCapacity() == 0);
buffer
.position(buffer.position()
+ ByteUtils.SPLIT.remaining());
Map<Object, Command> mergetCommands = getMergeCommands();
if (mergetCommands != null) {
final TextGetCommand command = (TextGetCommand) mergetCommands
.remove(this.currentReturnKey);
if (command != null) {
command.setResult(value);
command.countDownLatch();
this.mergeCount--;
if (command.getAssocCommands() != null) {
for (Command assocCommand : command
.getAssocCommands()) {
assocCommand.setResult(value);
assocCommand.countDownLatch();
this.mergeCount--;
}
}
}
}
this.currentReturnKey = null;
this.parseStatus = ParseStatus.NULL;
continue;
default:
return decodeError(session, buffer);
}
}
}
private String getItem(ByteBuffer buffer, char token, char... others) {
int pos = buffer.position() + this.offset;
final int limit = buffer.limit();
for (; pos < limit; pos++) {
final byte b = buffer.get(pos);
if (b == token || isIn(b, others)) {
byte[] keyBytes = new byte[pos - buffer.position()];
buffer.get(keyBytes);
this.offset = 0;
assert (pos == buffer.position());
// skip token
buffer.position(pos + 1);
return getString(keyBytes);
}
}
this.offset = pos - buffer.position();
return null;
}
private boolean isIn(byte b, char[] others) {
for (int i = 0; i < others.length; i++) {
if (b == others[i]) {
return true;
}
}
return false;
}
private String getString(byte[] keyBytes) {
try {
return new String(keyBytes, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/*
* (non-Javadoc)
*
* @see
* net.rubyeye.xmemcached.command.text.MapReturnValueAware#getReturnValues()
*/
public final Map<String, CachedData> getReturnValues() {
return this.returnValues;
}
public final void setReturnValues(Map<String, CachedData> returnValues) {
this.returnValues = returnValues;
}
public abstract void dispatch();
@Override
public void encode() {
byte[] cmdBytes = this.commandType == CommandType.GET_ONE
|| this.commandType == CommandType.GET_MANY ? Constants.GET
: Constants.GETS;
this.ioBuffer = IoBuffer.allocate(cmdBytes.length
+ Constants.CRLF.length + 1 + this.keyBytes.length);
ByteUtils.setArguments(this.ioBuffer, cmdBytes, this.keyBytes);
this.ioBuffer.flip();
}
}