/* * Copyright (c) 2015, Jurriaan Mous and contributors as indicated by the @author tags. * * 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 mousio.etcd4j.transport; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.util.concurrent.Promise; import mousio.client.exceptions.PrematureDisconnectException; import mousio.etcd4j.requests.EtcdRequest; import mousio.etcd4j.responses.EtcdAuthenticationException; import mousio.etcd4j.responses.EtcdException; import mousio.etcd4j.responses.EtcdResponseDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @author Jurriaan Mous * @author Luca Burgazzoli * * Handles etcd responses * * @param <R> Response type */ class EtcdResponseHandler<R> extends SimpleChannelInboundHandler<FullHttpResponse> { private static final Logger logger = LoggerFactory.getLogger(EtcdResponseHandler.class); private static final Map<HttpResponseStatus, EtcdResponseDecoder<? extends Throwable>> failureDecoders; static { failureDecoders = new HashMap<>(); failureDecoders.put(HttpResponseStatus.UNAUTHORIZED, EtcdAuthenticationException.DECODER); failureDecoders.put(HttpResponseStatus.NOT_FOUND, EtcdException.DECODER); failureDecoders.put(HttpResponseStatus.FORBIDDEN, EtcdException.DECODER); failureDecoders.put(HttpResponseStatus.PRECONDITION_FAILED, EtcdException.DECODER); failureDecoders.put(HttpResponseStatus.INTERNAL_SERVER_ERROR, EtcdException.DECODER); failureDecoders.put(HttpResponseStatus.BAD_REQUEST, EtcdException.DECODER); } protected final Promise<R> promise; protected final EtcdNettyClient client; protected final EtcdRequest<R> request; private boolean isRetried; /** * Constructor * * @param etcdNettyClient the client handling connections * @param etcdRequest request */ @SuppressWarnings("unchecked") public EtcdResponseHandler(EtcdNettyClient etcdNettyClient, EtcdRequest<R> etcdRequest) { this.client = etcdNettyClient; this.request = etcdRequest; this.promise = etcdRequest.getPromise().getNettyPromise(); this.isRetried = false; } /** * Set if the connection is retried. * If true the promise will not fail on un-registering this handler. * * @param retried true if request is being retried. */ public void retried(boolean retried) { this.isRetried = retried; } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { if (!isRetried && !promise.isDone()) { this.request.getPromise().handleRetry(new PrematureDisconnectException()); } super.channelUnregistered(ctx); } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception { final HttpResponseStatus status =response.status(); final HttpHeaders headers = response.headers(); final ByteBuf content = response.content(); if (logger.isDebugEnabled()) { logger.debug("Received {} for {} {}", status.code(), this.request.getMethod().name(), this.request.getUri()); } if (status.equals(HttpResponseStatus.MOVED_PERMANENTLY) || status.equals(HttpResponseStatus.TEMPORARY_REDIRECT)) { if (headers.contains(HttpHeaderNames.LOCATION)) { this.request.setUrl(headers.get(HttpHeaderNames.LOCATION)); this.client.connect(this.request); // Closing the connection which handled the previous request. ctx.close(); if (logger.isDebugEnabled()) { logger.debug("redirect for {} to {}", this.request.getHttpRequest().uri() , headers.get(HttpHeaderNames.LOCATION) ); } } else { this.promise.setFailure(new Exception("Missing Location header on redirect")); } } else { EtcdResponseDecoder<? extends Throwable> failureDecoder = failureDecoders.get(status); if(failureDecoder != null) { this.promise.setFailure(failureDecoder.decode(headers, content)); } else if (!content.isReadable()) { // If connection was accepted maybe response has to be waited for if (!status.equals(HttpResponseStatus.OK) && !status.equals(HttpResponseStatus.ACCEPTED) && !status.equals(HttpResponseStatus.CREATED)) { this.promise.setFailure(new IOException( "Content was not readable. HTTP Status: " + status)); } } else { try { this.promise.setSuccess( request.getResponseDecoder().decode(headers, content)); } catch (Exception e) { if (e instanceof EtcdException) { this.promise.setFailure(e); } else { try { // Try to be smart, if an exception is thrown, first try to decode // the content and see if it is an EtcdException, i.e. an error code // not included in failureDecoders this.promise.setFailure(EtcdException.DECODER.decode(headers, content)); } catch (Exception e1) { // if it fails again, set the original exception as failure this.promise.setFailure(e); } } } } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { this.promise.setFailure(cause); } }