/*
* 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.config;
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.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.config.BucketConfigRequest;
import com.couchbase.client.core.message.config.BucketConfigResponse;
import com.couchbase.client.core.message.config.BucketStreamingRequest;
import com.couchbase.client.core.message.config.BucketStreamingResponse;
import com.couchbase.client.core.message.config.BucketsConfigRequest;
import com.couchbase.client.core.message.config.BucketsConfigResponse;
import com.couchbase.client.core.message.config.ClusterConfigRequest;
import com.couchbase.client.core.message.config.ClusterConfigResponse;
import com.couchbase.client.core.message.config.ConfigRequest;
import com.couchbase.client.core.message.config.FlushRequest;
import com.couchbase.client.core.message.config.FlushResponse;
import com.couchbase.client.core.message.config.GetDesignDocumentsRequest;
import com.couchbase.client.core.message.config.GetDesignDocumentsResponse;
import com.couchbase.client.core.message.config.GetUsersRequest;
import com.couchbase.client.core.message.config.GetUsersResponse;
import com.couchbase.client.core.message.config.InsertBucketRequest;
import com.couchbase.client.core.message.config.InsertBucketResponse;
import com.couchbase.client.core.message.config.RemoveBucketRequest;
import com.couchbase.client.core.message.config.RemoveBucketResponse;
import com.couchbase.client.core.message.config.RemoveUserRequest;
import com.couchbase.client.core.message.config.RemoveUserResponse;
import com.couchbase.client.core.message.config.RestApiRequest;
import com.couchbase.client.core.message.config.RestApiResponse;
import com.couchbase.client.core.message.config.UpdateBucketRequest;
import com.couchbase.client.core.message.config.UpdateBucketResponse;
import com.couchbase.client.core.message.config.UpsertUserRequest;
import com.couchbase.client.core.message.config.UpsertUserResponse;
import com.couchbase.client.core.service.ServiceType;
import com.lmax.disruptor.EventSink;
import com.lmax.disruptor.RingBuffer;
import io.netty.buffer.ByteBuf;
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.Observable;
import rx.subjects.BehaviorSubject;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.RejectedExecutionException;
/**
* The {@link ConfigHandler} is responsible for encoding {@link ConfigRequest}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 ConfigHandler extends AbstractGenericHandler<HttpObject, HttpRequest, ConfigRequest> {
/**
* The logger used.
*/
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(ConfigHandler.class);
/**
* 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 BehaviorSubject<String> streamingConfigObservable;
/**
* Creates a new {@link ConfigHandler} with the default queue for requests.
*
* @param endpoint the {@link AbstractEndpoint} to coordinate with.
* @param responseBuffer the {@link RingBuffer} to push responses into.
*/
public ConfigHandler(AbstractEndpoint endpoint, EventSink<ResponseEvent> responseBuffer, boolean isTransient,
final boolean pipeline) {
super(endpoint, responseBuffer, isTransient, pipeline);
}
/**
* Creates a new {@link ConfigHandler} 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.
*/
ConfigHandler(AbstractEndpoint endpoint, EventSink<ResponseEvent> responseBuffer, Queue<ConfigRequest> queue,
boolean isTransient, final boolean pipeline) {
super(endpoint, responseBuffer, queue, isTransient, pipeline);
}
@Override
protected HttpRequest encodeRequest(final ChannelHandlerContext ctx, final ConfigRequest msg) throws Exception {
if (msg instanceof RestApiRequest) {
return encodeRestApiRequest(ctx, (RestApiRequest) msg);
}
HttpMethod httpMethod = HttpMethod.GET;
if (msg instanceof FlushRequest || msg instanceof InsertBucketRequest
|| msg instanceof UpdateBucketRequest) {
httpMethod = HttpMethod.POST;
} else if (msg instanceof UpsertUserRequest) {
httpMethod = HttpMethod.PUT;
} else if (msg instanceof RemoveBucketRequest || msg instanceof RemoveUserRequest) {
httpMethod = HttpMethod.DELETE;
}
ByteBuf content;
if (msg instanceof InsertBucketRequest) {
content = Unpooled.copiedBuffer(((InsertBucketRequest) msg).payload(), CharsetUtil.UTF_8);
} else if (msg instanceof UpdateBucketRequest) {
content = Unpooled.copiedBuffer(((UpdateBucketRequest) msg).payload(), CharsetUtil.UTF_8);
} else if (msg instanceof UpsertUserRequest) {
content = Unpooled.copiedBuffer(((UpsertUserRequest) msg).payload(), CharsetUtil.UTF_8);
} else {
content = Unpooled.EMPTY_BUFFER;
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, httpMethod, msg.path(), content);
request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent());
if (msg instanceof InsertBucketRequest || msg instanceof UpdateBucketRequest || msg instanceof UpsertUserRequest) {
request.headers().set(HttpHeaders.Names.ACCEPT, "*/*");
request.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/x-www-form-urlencoded");
}
request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, content.readableBytes());
request.headers().set(HttpHeaders.Names.HOST, remoteHttpHost(ctx));
addHttpBasicAuth(ctx, request, msg.username(), msg.password());
return request;
}
private HttpRequest encodeRestApiRequest(ChannelHandlerContext ctx, RestApiRequest msg) {
HttpMethod httpMethod = msg.method();
ByteBuf content = Unpooled.copiedBuffer(msg.body(), CharsetUtil.UTF_8);
String path = msg.pathWithParameters();
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, httpMethod, path, content);
//these headers COULD be overridden
request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent());
request.headers().set(HttpHeaders.Names.HOST, remoteHttpHost(ctx));
for (Map.Entry<String, Object> header : msg.headers().entrySet()) {
request.headers().set(header.getKey(), header.getValue());
}
//these headers should always be computed from the msg
request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, content.readableBytes());
addHttpBasicAuth(ctx, request, msg.username(), msg.password());
return request;
}
@Override
protected CouchbaseResponse decodeResponse(final ChannelHandlerContext ctx, final HttpObject msg) throws Exception {
ConfigRequest request = currentRequest();
CouchbaseResponse response = null;
if (msg instanceof HttpResponse) {
responseHeader = (HttpResponse) msg;
if (request instanceof BucketStreamingRequest) {
response = handleBucketStreamingResponse(ctx, responseHeader);
}
if (responseContent != null) {
responseContent.clear();
} else {
responseContent = ctx.alloc().buffer();
}
}
if (msg instanceof HttpContent) {
responseContent.writeBytes(((HttpContent) msg).content());
if (streamingConfigObservable != null) {
maybePushConfigChunk();
}
}
if (msg instanceof LastHttpContent) {
if (request instanceof BucketStreamingRequest) {
if (streamingConfigObservable != null) {
streamingConfigObservable.onCompleted();
streamingConfigObservable = null;
}
finishedDecoding();
return null;
}
ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code());
String body = responseContent.readableBytes() > 0
? responseContent.toString(CHARSET) : responseHeader.getStatus().reasonPhrase();
if (request instanceof BucketConfigRequest) {
response = new BucketConfigResponse(body, status);
} else if (request instanceof ClusterConfigRequest) {
response = new ClusterConfigResponse(body, status);
} else if (request instanceof BucketsConfigRequest) {
response = new BucketsConfigResponse(body, status);
} else if (request instanceof GetDesignDocumentsRequest) {
response = new GetDesignDocumentsResponse(body, status, request);
} else if (request instanceof RemoveBucketRequest) {
response = new RemoveBucketResponse(status);
} else if (request instanceof InsertBucketRequest) {
response = new InsertBucketResponse(body, status);
} else if (request instanceof UpdateBucketRequest) {
response = new UpdateBucketResponse(body, status);
} else if (request instanceof FlushRequest) {
boolean done = responseHeader.getStatus().code() != 201;
response = new FlushResponse(done, body, status);
} else if (request instanceof GetUsersRequest) {
response = new GetUsersResponse(body, status, request);
} else if (request instanceof UpsertUserRequest) {
response = new UpsertUserResponse(body, status);
} else if (request instanceof RemoveUserRequest) {
response = new RemoveUserResponse(status);
} else if (request instanceof RestApiRequest) {
response = new RestApiResponse((RestApiRequest) request, responseHeader.getStatus(),
responseHeader.headers(), body);
}
finishedDecoding();
}
return response;
}
/**
* Decodes a {@link BucketStreamingResponse}.
*
* @param ctx the handler context.
* @param header the received header.
* @return a initialized {@link CouchbaseResponse}.
*/
private CouchbaseResponse handleBucketStreamingResponse(final ChannelHandlerContext ctx,
final HttpResponse header) {
SocketAddress addr = ctx.channel().remoteAddress();
String host = addr instanceof InetSocketAddress ? ((InetSocketAddress) addr).getHostName() : addr.toString();
ResponseStatus status = ResponseStatusConverter.fromHttp(header.getStatus().code());
Observable<String> scheduledObservable = null;
if (status.isSuccess()) {
streamingConfigObservable = BehaviorSubject.create();
scheduledObservable = streamingConfigObservable.onBackpressureBuffer().observeOn(env().scheduler());
}
return new BucketStreamingResponse(
scheduledObservable,
host,
status,
currentRequest()
);
}
/**
* Push a config chunk into the streaming observable.
*/
private void maybePushConfigChunk() {
String currentChunk = responseContent.toString(CHARSET);
int separatorIndex = currentChunk.indexOf("\n\n\n\n");
if (separatorIndex > 0) {
String content = currentChunk.substring(0, separatorIndex);
streamingConfigObservable.onNext(content.trim());
responseContent.clear();
responseContent.writeBytes(currentChunk.substring(separatorIndex + 4).getBytes(CHARSET));
}
}
/**
* If it is still present and open, release the content buffer. Also set it
* to null so that next decoding can take a new buffer from the pool.
*/
private void releaseResponseContent() {
if (responseContent != null) {
if (responseContent.refCnt() > 0) {
responseContent.release();
}
responseContent = null;
}
}
@Override
protected void finishedDecoding() {
super.finishedDecoding();
releaseResponseContent();
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
if (streamingConfigObservable != null) {
try {
streamingConfigObservable.onCompleted();
} catch (RejectedExecutionException ex) {
// this can happen during shutdown, so log it but don't let it
// bubble up the event loop.
LOGGER.info(logIdent(ctx, endpoint()) + "Could not complete config stream, scheduler shut "
+ "down already.");
}
}
super.handlerRemoved(ctx);
releaseResponseContent();
}
@Override
protected ServiceType serviceType() {
return ServiceType.CONFIG;
}
}