/*
* Copyright (c) 2016 Couchbase, 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.couchbase.client.core.endpoint.view;
import com.couchbase.client.core.ResponseEvent;
import com.couchbase.client.core.endpoint.AbstractEndpoint;
import com.couchbase.client.core.endpoint.AbstractGenericHandler;
import com.couchbase.client.core.endpoint.ResponseStatusConverter;
import com.couchbase.client.core.endpoint.util.ClosingPositionBufProcessor;
import com.couchbase.client.core.message.AbstractCouchbaseRequest;
import com.couchbase.client.core.message.AbstractCouchbaseResponse;
import com.couchbase.client.core.message.CouchbaseRequest;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.KeepAlive;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.view.GetDesignDocumentRequest;
import com.couchbase.client.core.message.view.GetDesignDocumentResponse;
import com.couchbase.client.core.message.view.RemoveDesignDocumentRequest;
import com.couchbase.client.core.message.view.RemoveDesignDocumentResponse;
import com.couchbase.client.core.message.view.UpsertDesignDocumentRequest;
import com.couchbase.client.core.message.view.UpsertDesignDocumentResponse;
import com.couchbase.client.core.message.view.ViewQueryRequest;
import com.couchbase.client.core.message.view.ViewQueryResponse;
import com.couchbase.client.core.message.view.ViewRequest;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.utils.UnicastAutoReleaseSubject;
import com.lmax.disruptor.RingBuffer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufProcessor;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.CharsetUtil;
import rx.Scheduler;
import rx.subjects.AsyncSubject;
import java.net.URLEncoder;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
/**
* The {@link ViewHandler} is responsible for encoding {@link ViewRequest}s into lower level
* {@link HttpRequest}s as well as decoding {@link HttpObject}s into
* {@link CouchbaseResponse}s.
*
* @author Michael Nitschinger
* @since 1.0
*/
public class ViewHandler extends AbstractGenericHandler<HttpObject, HttpRequest, ViewRequest> {
private static final int MAX_GET_LENGTH = 2048;
private static final byte QUERY_STATE_INITIAL = 0;
private static final byte QUERY_STATE_ROWS = 1;
private static final byte QUERY_STATE_INFO = 2;
private static final byte QUERY_STATE_ERROR = 3;
private static final byte QUERY_STATE_DONE = 4;
/**
* Contains the current pending response header if set.
*/
private HttpResponse responseHeader;
/**
* Contains the accumulating buffer for the response content.
*/
private ByteBuf responseContent;
/**
* Represents a observable that sends config chunks if instructed.
*/
private UnicastAutoReleaseSubject<ByteBuf> viewRowObservable;
/**
* Contains info-level data about the view response.
*/
private UnicastAutoReleaseSubject<ByteBuf> viewInfoObservable;
/**
* Contains optional errors that happened during execution.
*/
private AsyncSubject<String> viewErrorObservable;
/**
* Represents the current query parsing state.
*/
private byte viewParsingState = QUERY_STATE_INITIAL;
/**
* Creates a new {@link ViewHandler} with the default queue for requests.
*
* @param endpoint the {@link AbstractEndpoint} to coordinate with.
* @param responseBuffer the {@link RingBuffer} to push responses into.
*/
public ViewHandler(AbstractEndpoint endpoint, RingBuffer<ResponseEvent> responseBuffer, boolean isTransient,
final boolean pipeline) {
super(endpoint, responseBuffer, isTransient, pipeline);
}
/**
* Creates a new {@link ViewHandler} with a custom queue for requests (suitable for tests).
*
* @param endpoint the {@link AbstractEndpoint} to coordinate with.
* @param responseBuffer the {@link RingBuffer} to push responses into.
* @param queue the queue which holds all outstanding open requests.
*/
ViewHandler(AbstractEndpoint endpoint, RingBuffer<ResponseEvent> responseBuffer, Queue<ViewRequest> queue,
boolean isTransient, final boolean pipeline) {
super(endpoint, responseBuffer, queue, isTransient, pipeline);
}
@Override
protected HttpRequest encodeRequest(final ChannelHandlerContext ctx, final ViewRequest msg) throws Exception {
if (msg instanceof KeepAliveRequest) {
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, "/",
Unpooled.EMPTY_BUFFER);
request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent());
request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, 0);
return request;
}
StringBuilder path = new StringBuilder();
HttpMethod method = HttpMethod.GET;
ByteBuf content = null;
if (msg instanceof ViewQueryRequest) {
ViewQueryRequest queryMsg = (ViewQueryRequest) msg;
path.append("/").append(msg.bucket()).append("/_design/");
path.append(queryMsg.development() ? "dev_" + queryMsg.design() : queryMsg.design());
if (queryMsg.spatial()) {
path.append("/_spatial/");
} else {
path.append("/_view/");
}
path.append(queryMsg.view());
int queryLength = queryMsg.query() == null ? 0 : queryMsg.query().length();
int keysLength = queryMsg.keys() == null ? 0 : queryMsg.keys().length();
boolean hasQuery = queryLength > 0;
boolean hasKeys = keysLength > 0;
if (hasQuery || hasKeys) {
if (queryLength + keysLength < MAX_GET_LENGTH) {
//the query is short enough for GET
//it has query, query+keys or keys only
if (hasQuery) {
path.append("?").append(queryMsg.query());
if (hasKeys) {
path.append("&keys=").append(encodeKeysGet(queryMsg.keys()));
}
} else {
//it surely has keys if not query
path.append("?keys=").append(encodeKeysGet(queryMsg.keys()));
}
} else {
//the query is too long for GET, use the keys as JSON body
if (hasQuery) {
path.append("?").append(queryMsg.query());
}
String keysContent = encodeKeysPost(queryMsg.keys());
//switch to POST
method = HttpMethod.POST;
//body is "keys" but in JSON
content = ctx.alloc().buffer(keysContent.length());
content.writeBytes(keysContent.getBytes(CHARSET));
}
}
} else if (msg instanceof GetDesignDocumentRequest) {
GetDesignDocumentRequest queryMsg = (GetDesignDocumentRequest) msg;
path.append("/").append(msg.bucket()).append("/_design/");
path.append(queryMsg.development() ? "dev_" + queryMsg.name() : queryMsg.name());
} else if (msg instanceof UpsertDesignDocumentRequest) {
method = HttpMethod.PUT;
UpsertDesignDocumentRequest queryMsg = (UpsertDesignDocumentRequest) msg;
path.append("/").append(msg.bucket()).append("/_design/");
path.append(queryMsg.development() ? "dev_" + queryMsg.name() : queryMsg.name());
content = Unpooled.copiedBuffer(queryMsg.body(), CHARSET);
} else if (msg instanceof RemoveDesignDocumentRequest) {
method = HttpMethod.DELETE;
RemoveDesignDocumentRequest queryMsg = (RemoveDesignDocumentRequest) msg;
path.append("/").append(msg.bucket()).append("/_design/");
path.append(queryMsg.development() ? "dev_" + queryMsg.name() : queryMsg.name());
} else {
throw new IllegalArgumentException("Unknown incoming ViewRequest type "
+ msg.getClass());
}
if (content == null) {
content = Unpooled.buffer(0);
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, path.toString(), content);
request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent());
request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, content.readableBytes());
request.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
request.headers().set(HttpHeaders.Names.HOST, remoteHttpHost(ctx));
addHttpBasicAuth(ctx, request, msg.username(), msg.password());
return request;
}
/**
* Encodes the "keys" JSON array into a JSON object suitable for a POST body on query service.
*/
private String encodeKeysPost(String keys) {
return "{\"keys\":" + keys + "}";
}
/**
* Encodes the "keys" JSON array into an URL-encoded form suitable for a GET on query service.
*/
private String encodeKeysGet(String keys) {
try {
return URLEncoder.encode(keys, "UTF-8");
} catch(Exception ex) {
throw new RuntimeException("Could not prepare view argument: " + ex);
}
}
@Override
protected CouchbaseResponse decodeResponse(final ChannelHandlerContext ctx, final HttpObject msg) throws Exception {
ViewRequest request = currentRequest();
CouchbaseResponse response = null;
if (msg instanceof HttpResponse) {
responseHeader = (HttpResponse) msg;
if (responseContent != null) {
responseContent.clear();
} else {
responseContent = ctx.alloc().buffer();
}
}
if (request instanceof KeepAliveRequest) {
response = new KeepAliveResponse(ResponseStatusConverter.fromHttp(responseHeader.getStatus().code()), request);
responseContent.clear();
responseContent.discardReadBytes();
} else if (msg instanceof HttpContent) {
responseContent.writeBytes(((HttpContent) msg).content());
if (currentRequest() instanceof ViewQueryRequest) {
if (viewRowObservable == null) {
response = handleViewQueryResponse();
}
parseQueryResponse(msg instanceof LastHttpContent);
}
}
if (msg instanceof LastHttpContent) {
if (request instanceof GetDesignDocumentRequest) {
response = handleGetDesignDocumentResponse((GetDesignDocumentRequest) request);
finishedDecoding();
} else if (request instanceof UpsertDesignDocumentRequest) {
response = handleUpsertDesignDocumentResponse((UpsertDesignDocumentRequest) request);
finishedDecoding();
} else if (request instanceof RemoveDesignDocumentRequest) {
response = handleRemoveDesignDocumentResponse((RemoveDesignDocumentRequest) request);
finishedDecoding();
} else if (request instanceof KeepAliveRequest) {
finishedDecoding();
}
}
return response;
}
/**
* Creates a {@link GetDesignDocumentResponse} from its request based on the returned info.
*
* @param request the outgoing request.
* @return the parsed response.
*/
private CouchbaseResponse handleGetDesignDocumentResponse(final GetDesignDocumentRequest request) {
ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code());
return new GetDesignDocumentResponse(request.name(), request.development(), responseContent.copy(), status,
request);
}
private CouchbaseResponse handleUpsertDesignDocumentResponse(final UpsertDesignDocumentRequest request) {
ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code());
return new UpsertDesignDocumentResponse(status, responseContent.copy(), request);
}
private CouchbaseResponse handleRemoveDesignDocumentResponse(final RemoveDesignDocumentRequest request) {
ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code());
return new RemoveDesignDocumentResponse(status, responseContent.copy(), request);
}
/**
* Creates a {@link ViewQueryResponse} from its request based on the returned info.
*
* Note that observables are attached to this response which are completed later in the response cycle.
*
* @return the initial response.
*/
private CouchbaseResponse handleViewQueryResponse() {
int code = responseHeader.getStatus().code();
String phrase = responseHeader.getStatus().reasonPhrase();
ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code());
Scheduler scheduler = env().scheduler();
long ttl = env().autoreleaseAfter();
viewRowObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler);
viewInfoObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler);
viewErrorObservable = AsyncSubject.create();
//set up trace ids on all these UnicastAutoReleaseSubjects, so that if they get in a bad state
// (multiple subscribers or subscriber coming in too late) we can trace back to here
viewRowObservable.withTraceIdentifier("viewRow");
viewInfoObservable.withTraceIdentifier("viewInfo");
return new ViewQueryResponse(
viewRowObservable.onBackpressureBuffer().observeOn(scheduler),
viewInfoObservable.onBackpressureBuffer().observeOn(scheduler),
viewErrorObservable.observeOn(scheduler),
code,
phrase,
status,
currentRequest()
);
}
/**
* Main dispatch method for a query parse cycle.
*
* @param last if the given content chunk is the last one.
*/
private void parseQueryResponse(boolean last) {
if (viewParsingState == QUERY_STATE_INITIAL) {
parseViewInitial();
}
if (viewParsingState == QUERY_STATE_INFO) {
parseViewInfo();
}
if (viewParsingState == QUERY_STATE_ROWS) {
parseViewRows(last);
}
if (viewParsingState == QUERY_STATE_ERROR) {
parseViewError(last);
}
if (viewParsingState == QUERY_STATE_DONE) {
cleanupViewStates();
}
}
/**
* Clean up the query states after all rows have been consumed.
*/
private void cleanupViewStates() {
finishedDecoding();
viewInfoObservable = null;
viewRowObservable = null;
viewErrorObservable = null;
viewParsingState = QUERY_STATE_INITIAL;
}
/**
* Parse the initial view query state.
*/
private void parseViewInitial() {
switch (responseHeader.getStatus().code()) {
case 200:
viewParsingState = QUERY_STATE_INFO;
break;
default:
viewInfoObservable.onCompleted();
viewRowObservable.onCompleted();
viewParsingState = QUERY_STATE_ERROR;
}
}
/**
* The query response is an error, parse it and attache it to the observable.
*
* @param last if the given content chunk is the last one.
*/
private void parseViewError(boolean last) {
if (!last) {
return;
}
if (responseHeader.getStatus().code() == 200) {
int openBracketPos = responseContent.bytesBefore((byte) '[') + responseContent.readerIndex();
int closeBracketLength = findSectionClosingPosition(responseContent, '[', ']') - openBracketPos + 1;
ByteBuf slice = responseContent.slice(openBracketPos, closeBracketLength);
viewErrorObservable.onNext("{\"errors\":" + slice.toString(CharsetUtil.UTF_8) + "}");
} else {
viewErrorObservable.onNext("{\"errors\":[" + responseContent.toString(CharsetUtil.UTF_8) + "]}");
}
viewErrorObservable.onCompleted();
viewParsingState = QUERY_STATE_DONE;
responseContent.discardReadBytes();
}
/**
* Parse out the info portion from the header part of the query response.
*
* This includes the total rows, but also debug info if attached.
*/
private void parseViewInfo() {
int rowsStart = -1;
for (int i = responseContent.readerIndex(); i < responseContent.writerIndex() - 2; i++) {
byte curr = responseContent.getByte(i);
byte f1 = responseContent.getByte(i + 1);
byte f2 = responseContent.getByte(i + 2);
if (curr == '"' && f1 == 'r' && f2 == 'o') {
rowsStart = i;
break;
}
}
if (rowsStart == -1) {
return;
}
ByteBuf info = responseContent.readBytes(rowsStart - responseContent.readerIndex());
int closingPointer = info.forEachByteDesc(new ByteBufProcessor() {
@Override
public boolean process(byte value) throws Exception {
return value != ',';
}
});
if (closingPointer > 0) {
info.setByte(closingPointer, '}');
viewInfoObservable.onNext(info);
} else {
//JVMCBC-360 don't forget to release the now unused info ByteBuf
info.release();
viewInfoObservable.onNext(Unpooled.EMPTY_BUFFER);
}
viewInfoObservable.onCompleted();
viewParsingState = QUERY_STATE_ROWS;
}
/**
* Streaming parse the actual rows from the response and pass to the underlying observable.
*
* @param last if the given content chunk is the last one.
*/
private void parseViewRows(boolean last) {
while (true) {
int openBracketPos = responseContent.bytesBefore((byte) '{');
int errorBlockPosition = findErrorBlockPosition(openBracketPos);
if (errorBlockPosition > 0 && errorBlockPosition < openBracketPos) {
responseContent.readerIndex(errorBlockPosition + responseContent.readerIndex());
viewRowObservable.onCompleted();
viewParsingState = QUERY_STATE_ERROR;
return;
}
int closeBracketPos = findSectionClosingPosition(responseContent, '{', '}');
if (closeBracketPos == -1) {
break;
}
int from = responseContent.readerIndex() + openBracketPos;
int to = closeBracketPos - openBracketPos - responseContent.readerIndex() + 1;
viewRowObservable.onNext(responseContent.slice(from, to).copy());
responseContent.readerIndex(closeBracketPos);
responseContent.discardReadBytes();
}
if (last) {
viewRowObservable.onCompleted();
viewErrorObservable.onCompleted();
viewParsingState = QUERY_STATE_DONE;
}
}
private int findErrorBlockPosition(int openBracketPos) {
int errorPosition = -1;
int readerIndex = responseContent.readerIndex();
for (int i = readerIndex; i < readerIndex + openBracketPos - 2; i++) {
byte curr = responseContent.getByte(i);
byte f1 = responseContent.getByte(i + 1);
byte f2 = responseContent.getByte(i + 2);
if (curr == '"' && f1 == 'e' && f2 == 'r') {
errorPosition = i;
break;
}
}
return errorPosition > -1 ? errorPosition - responseContent.readerIndex() : errorPosition;
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
if (viewRowObservable != null) {
viewRowObservable.onCompleted();
viewRowObservable = null;
}
if (viewInfoObservable != null) {
viewInfoObservable.onCompleted();
viewInfoObservable = null;
}
if (viewErrorObservable != null) {
viewErrorObservable.onCompleted();
viewErrorObservable = null;
}
cleanupViewStates();
if (responseContent != null && responseContent.refCnt() > 0) {
responseContent.release();
}
super.handlerRemoved(ctx);
}
/**
* Finds the position of the correct closing character, taking into account the fact that before the correct one,
* other sub section with same opening and closing characters can be encountered.
*
* @param buf the {@link ByteBuf} where to search for the end of a section enclosed in openingChar and closingChar.
* @param openingChar the section opening char, used to detect a sub-section.
* @param closingChar the section closing char, used to detect the end of a sub-section / this section.
* @return
*/
private static int findSectionClosingPosition(ByteBuf buf, char openingChar, char closingChar) {
return buf.forEachByte(new ClosingPositionBufProcessor(openingChar, closingChar, true));
}
@Override
protected CouchbaseRequest createKeepAliveRequest() {
return new KeepAliveRequest();
}
protected static class KeepAliveRequest extends AbstractCouchbaseRequest implements ViewRequest, KeepAlive {
protected KeepAliveRequest() {
super(null, null);
}
}
protected static class KeepAliveResponse extends AbstractCouchbaseResponse {
protected KeepAliveResponse(ResponseStatus status, CouchbaseRequest request) {
super(status, request);
}
}
@Override
protected ServiceType serviceType() {
return ServiceType.VIEW;
}
}