/* * Copyright (c) 2017 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.query.parser; import com.couchbase.client.core.logging.CouchbaseLogger; import com.couchbase.client.core.logging.CouchbaseLoggerFactory; import com.couchbase.client.core.message.CouchbaseRequest; import com.couchbase.client.core.message.ResponseStatus; import com.couchbase.client.core.message.query.GenericQueryResponse; import com.couchbase.client.core.utils.UnicastAutoReleaseSubject; import com.couchbase.client.core.utils.yasjl.ByteBufJsonParser; import com.couchbase.client.core.utils.yasjl.Callbacks.JsonPointerCB1; import com.couchbase.client.core.utils.yasjl.JsonPointer; import java.io.EOFException; import io.netty.buffer.ByteBuf; import io.netty.util.CharsetUtil; import rx.Scheduler; import rx.subjects.AsyncSubject; import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; /** * yasjl based query response parser * * @author Subhashni Balakrishnan * @since 1.4.3 */ public class YasjlQueryResponseParser { private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(YasjlQueryResponseParser.class); private ByteBufJsonParser parser; private String requestID; private String clientContextID; private boolean sentResponse; protected static final Charset CHARSET = CharsetUtil.UTF_8; protected ByteBuf responseContent; /** * Represents an observable that sends result chunks. */ protected UnicastAutoReleaseSubject<ByteBuf> queryRowObservable; /** * Represents an observable that has the signature of the N1QL results if there are any. */ protected UnicastAutoReleaseSubject<ByteBuf> querySignatureObservable; /** * Represents an observable that sends errors and warnings if any during query execution. */ protected UnicastAutoReleaseSubject<ByteBuf> queryErrorObservable; /** * Represent an observable that has the final execution status of the query, once all result rows and/or * errors/warnings have been sent. */ protected AsyncSubject<String> queryStatusObservable; /** * Represents an observable containing metrics on a terminated query. */ protected UnicastAutoReleaseSubject<ByteBuf> queryInfoObservable; /** * Represents an observable containing profile info on a terminated query. */ protected UnicastAutoReleaseSubject<ByteBuf> queryProfileInfoObservable; /** * Represents the current request */ protected CouchbaseRequest currentRequest; /** * Scheduler for query response */ protected Scheduler scheduler; /** * TTL for response observables */ protected long ttl; /** * Should complete callback on Io thread */ protected boolean callbacksOnIoPool; /** * Response status */ protected ResponseStatus status; /** * Flag to indicate if the parser is initialized */ protected boolean initialized; /** * Response that should be returned on parse call */ protected GenericQueryResponse response; public YasjlQueryResponseParser(Scheduler scheduler, long ttl, boolean callbacksOnIoPool) { this.scheduler = scheduler; this.ttl = ttl; this.response = null; this.callbacksOnIoPool = callbacksOnIoPool; JsonPointer[] jsonPointers = { new JsonPointer("/requestID", new JsonPointerCB1() { public void call(ByteBuf buf) { requestID = buf.toString(CHARSET); requestID = requestID.substring(1, requestID.length() - 1); buf.release(); if (queryRowObservable != null) { queryRowObservable.withTraceIdentifier("queryRow." + requestID); } if (queryErrorObservable != null) { queryErrorObservable.withTraceIdentifier("queryError." + requestID); } if (queryInfoObservable != null) { queryInfoObservable.withTraceIdentifier("queryInfo." + requestID); } if (querySignatureObservable != null) { querySignatureObservable.withTraceIdentifier("querySignature." + requestID); } if (queryProfileInfoObservable != null) { queryProfileInfoObservable.withTraceIdentifier("queryProfileInfo." + requestID); } } }), new JsonPointer("/clientContextID", new JsonPointerCB1() { public void call(ByteBuf buf) { clientContextID = buf.toString(CHARSET); clientContextID = clientContextID.substring(1, clientContextID.length() - 1); buf.release(); } }), new JsonPointer("/signature", new JsonPointerCB1() { public void call(ByteBuf buf) { if (querySignatureObservable != null) { querySignatureObservable.onNext(buf); } } }), new JsonPointer("/status", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryStatusObservable != null) { String statusStr = buf.toString(CHARSET); buf.release(); statusStr = statusStr.substring(1, statusStr.length() - 1); if (!statusStr.equals("success")) { status = ResponseStatus.FAILURE; } queryStatusObservable.onNext(statusStr); //overwrite existing response object if streamed in status if (!sentResponse) { createResponse(); LOGGER.trace("Received status for requestId {}", requestID); } } } }), new JsonPointer("/metrics", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryInfoObservable != null) { queryInfoObservable.onNext(buf); } } }), new JsonPointer("/results/-", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryRowObservable != null) { queryRowObservable.onNext(buf); if (response == null) { createResponse(); LOGGER.trace("Started receiving results for requestId {}", requestID); } } } }), new JsonPointer("/errors/-", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryErrorObservable != null) { queryErrorObservable.onNext(buf); if (response == null) { createResponse(); LOGGER.trace("Started receiving errors for requestId {}", requestID); } } } }), new JsonPointer("/warnings/-", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryErrorObservable != null) { queryErrorObservable.onNext(buf); if (response == null) { createResponse(); LOGGER.trace("Started receiving warnings for requestId {}", requestID); } } } }), new JsonPointer("/profile", new JsonPointerCB1() { public void call(ByteBuf buf) { if (queryProfileInfoObservable != null) { queryProfileInfoObservable.onNext(buf); } } }), }; this.parser = new ByteBufJsonParser(jsonPointers); } public boolean isInitialized() { return this.initialized; } public void initialize(ByteBuf responseContent, final ResponseStatus responseStatus) { this.requestID = ""; this.clientContextID = ""; //initialize to empty strings instead of null as we may not receive context id sometimes this.sentResponse = false; this.response = null; this.status = responseStatus; this.responseContent = responseContent; queryRowObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryErrorObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryStatusObservable = AsyncSubject.create(); queryInfoObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); querySignatureObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryProfileInfoObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryErrorObservable.onBackpressureBuffer(); queryRowObservable.onBackpressureBuffer(); querySignatureObservable.onBackpressureBuffer(); queryStatusObservable.onBackpressureBuffer(); queryInfoObservable.onBackpressureBuffer(); queryProfileInfoObservable.onBackpressureBuffer(); if (!this.callbacksOnIoPool) { queryErrorObservable.observeOn(scheduler); queryRowObservable.observeOn(scheduler); querySignatureObservable.observeOn(scheduler); queryStatusObservable.observeOn(scheduler); queryInfoObservable.observeOn(scheduler); queryProfileInfoObservable.observeOn(scheduler); } parser.initialize(responseContent); initialized = true; } private void createResponse() { //when streaming results/errors/status starts, build out the response response = new GenericQueryResponse( queryErrorObservable, queryRowObservable, querySignatureObservable, queryStatusObservable, queryInfoObservable, queryProfileInfoObservable, currentRequest, status, requestID, clientContextID); } //parses the response content public GenericQueryResponse parse(boolean lastChunk) throws Exception { try { parser.parse(); //discard only if EOF is not thrown responseContent.discardSomeReadBytes(); LOGGER.trace("Received last chunk and completed parsing for requestId {}", requestID); } catch (EOFException ex) { //ignore as we expect chunked responses LOGGER.trace("Still expecting more data for requestId {}", requestID); } //return back response only once if (!this.sentResponse && this.response != null) { this.sentResponse = true; return this.response; } return null; } public void finishParsingAndReset() { if (queryRowObservable != null) { queryRowObservable.onCompleted(); } if (queryInfoObservable != null) { queryInfoObservable.onCompleted(); } if (queryErrorObservable != null) { queryErrorObservable.onCompleted(); } if (queryStatusObservable != null) { queryStatusObservable.onCompleted(); } if (querySignatureObservable != null) { querySignatureObservable.onCompleted(); } if (queryProfileInfoObservable != null) { queryProfileInfoObservable.onCompleted(); } queryInfoObservable = null; queryRowObservable = null; queryErrorObservable = null; queryStatusObservable = null; querySignatureObservable = null; queryProfileInfoObservable = null; this.initialized = false; } }