/**
* Copyright 2011 Membase, 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 org.couchbase.mock.memcached;
import org.couchbase.mock.memcached.protocol.BinaryHelloCommand;
import org.couchbase.mock.memcached.protocol.BinaryResponse;
import org.couchbase.mock.memcached.protocol.BinaryCommand;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import org.couchbase.mock.memcached.protocol.CommandFactory;
/**
* Class representing a single <i>client</i> connection to the server
*/
public class MemcachedConnection {
private final BinaryProtocolHandler protocolHandler;
private final byte header[];
private BinaryCommand command;
private final ByteBuffer input;
private List<ByteBuffer> pending = new LinkedList<ByteBuffer>();
private boolean authenticated;
private boolean closed;
private final MutationInfoWriter miw = new MutationInfoWriter();
private boolean[] supportedFeatures = new boolean[BinaryHelloCommand.Feature.MAX.getValue()];
public MemcachedConnection(MemcachedServer server) {
closed = false;
authenticated = server.getBucket().getPassword().length() <= 0;
header = new byte[24];
input = ByteBuffer.wrap(header);
protocolHandler = server.getProtocolHandler();
}
/**
* Attempt to process a single command from the input buffer. Note this does
* not actually read from the socket.
*
* @throws IOException if the client has been closed
*/
public void step() throws IOException {
if (closed) {
throw new ClosedChannelException();
}
if (input.position() == header.length) {
if (command == null) {
command = CommandFactory.create(input);
}
if (command.complete()) {
command.process();
protocolHandler.execute(command, this);
command = null;
input.rewind();
}
}
}
/**
* Places the response into the current connection's output buffer.
* Note that the actual I/O is not performed in this method
* @param response the response to enqueue
*/
public synchronized void sendResponse(BinaryResponse response) {
if (pending == null) {
pending = new LinkedList<ByteBuffer>();
}
pending.add(response.getBuffer());
}
/**
* Determines whether this connection has pending responses to be sent
* @return true there are pending responses
*/
boolean hasOutput() {
if (pending == null) {
return false;
}
if (pending.isEmpty()) {
return false;
}
if (!pending.get(0).hasRemaining()) {
return false;
}
return true;
}
/**
* Gets the raw input buffer. This may be used to add additional request data
* @return The input buffer
*/
public ByteBuffer getInputBuffer() {
if (command == null) {
return input;
} else {
return command.getInputBuffer();
}
}
/**
* Temporarily borrow the head chunk of the output buffers. This may be used
* to efficiently send responses or perform socket/buffer manipulation.
*
* When done with the context, ensure to call {@link #returnOutputContext(OutputContext)}
* @return The output context
*/
public OutputContext borrowOutputContext() {
if (!hasOutput()) {
return null;
}
OutputContext ctx = new OutputContext(pending);
pending = null;
return ctx;
}
/**
* Re-transfer ownership of a given output buffer to the connection
* @param ctx An OutputContext previously returned by {@link #borrowOutputContext()}
*/
public void returnOutputContext(OutputContext ctx) {
List<ByteBuffer> remaining = ctx.releaseRemaining();
if (pending == null) {
pending = remaining;
} else {
List<ByteBuffer> tmp = pending;
pending = remaining;
pending.addAll(tmp);
}
}
/**
* Mark this connection has being closed. This will disallow further processing of commands
*/
void shutdown() {
closed = true;
}
/**
* Mark this connection as having been successfully authenticated
*/
void setAuthenticated() {
authenticated = true;
}
/**
* Check if this connection is authenticated
* @return true if the client has already authenticated
*/
public boolean isAuthenticated() {
return authenticated;
}
public MutationInfoWriter getMutinfoWriter() {
return miw;
}
public boolean[] getSupportedFeatures() {
return Arrays.copyOf(supportedFeatures, supportedFeatures.length);
}
/**
* Sets the supported features from a HELLO command.
*
* Note that the actual enabled features will be the ones supported by the mock
* and also supported by the client. Currently the only supported feature is
* MUTATION_SEQNO.
*
* @param input The features requested by the client.
*/
void setSupportedFeatures(boolean[] input) {
if (input.length != supportedFeatures.length) {
throw new IllegalArgumentException("Bad features length!");
}
// Scan through all other features and disable them unless they are supported
for (int i = 0; i < input.length; i++) {
BinaryHelloCommand.Feature feature = BinaryHelloCommand.Feature.valueOf(i);
if (feature == null) {
supportedFeatures[i] = false;
continue;
}
switch (feature) {
case MUTATION_SEQNO:
case XERROR:
case XATTR:
case SELECT_BUCKET:
supportedFeatures[i] = input[i];
break;
default:
supportedFeatures[i] = false;
break;
}
}
// Post-processing
if (supportedFeatures[BinaryHelloCommand.Feature.MUTATION_SEQNO.getValue()]) {
miw.setEnabled(true);
} else {
miw.setEnabled(false);
}
}
}