org.eclipse.hono.adapter.http.AbstractVertxBasedHttpProtocolAdapter.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.adapter.http.AbstractVertxBasedHttpProtocolAdapter.java

Source

/**
 * Copyright (c) 2016, 2018 Bosch Software Innovations GmbH.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bosch Software Innovations GmbH - initial creation
 */

package org.eclipse.hono.adapter.http;

import java.net.HttpURLConnection;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;

import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.proton.ProtonDelivery;
import org.apache.qpid.proton.message.Message;
import org.eclipse.hono.client.ClientErrorException;
import org.eclipse.hono.client.MessageConsumer;
import org.eclipse.hono.client.MessageSender;
import org.eclipse.hono.service.AbstractProtocolAdapterBase;
import org.eclipse.hono.service.auth.device.Device;
import org.eclipse.hono.service.command.CommandResponseSender;
import org.eclipse.hono.service.http.HttpUtils;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.EventConstants;
import org.eclipse.hono.util.MessageHelper;
import org.eclipse.hono.util.ResourceIdentifier;
import org.eclipse.hono.util.TelemetryConstants;
import org.eclipse.hono.util.TenantObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

/**
 * Base class for a Vert.x based Hono protocol adapter that uses the HTTP protocol.
 * It provides access to the Telemetry and Event API.
 * 
 * @param <T> The type of configuration properties used by this service.
 */
public abstract class AbstractVertxBasedHttpProtocolAdapter<T extends HttpProtocolAdapterProperties>
        extends AbstractProtocolAdapterBase<T> {

    /**
     * Default file uploads directory used by Vert.x Web.
     */
    protected static final String DEFAULT_UPLOADS_DIRECTORY = "/tmp";

    private static final Logger LOG = LoggerFactory.getLogger(AbstractVertxBasedHttpProtocolAdapter.class);

    private static final int AT_LEAST_ONCE = 1;
    private static final int HEADER_QOS_INVALID = -1;

    private HttpServer server;
    private HttpServer insecureServer;
    private HttpAdapterMetrics metrics;

    /**
     * Sets the metrics for this service.
     *
     * @param metrics The metrics
     */
    @Autowired
    public final void setMetrics(final HttpAdapterMetrics metrics) {
        this.metrics = metrics;
    }

    /**
     * @return 8443
     */
    @Override
    public final int getPortDefaultValue() {
        return 8443;
    }

    /**
     * @return 8080
     */
    @Override
    public final int getInsecurePortDefaultValue() {
        return 8080;
    }

    @Override
    protected final int getActualPort() {
        return server != null ? server.actualPort() : Constants.PORT_UNCONFIGURED;
    }

    @Override
    protected final int getActualInsecurePort() {
        return insecureServer != null ? insecureServer.actualPort() : Constants.PORT_UNCONFIGURED;
    }

    /**
     * Sets the http server instance configured to serve requests over a TLS secured socket.
     * <p>
     * If no server is set using this method, then a server instance is created during
     * startup of this adapter based on the <em>config</em> properties and the server options
     * returned by {@link #getHttpServerOptions()}.
     * 
     * @param server The http server.
     * @throws NullPointerException if server is {@code null}.
     * @throws IllegalArgumentException if the server is already started and listening on an address/port.
     */
    @Autowired(required = false)
    public final void setHttpServer(final HttpServer server) {
        Objects.requireNonNull(server);
        if (server.actualPort() > 0) {
            throw new IllegalArgumentException("http server must not be started already");
        } else {
            this.server = server;
        }
    }

    /**
     * Sets the http server instance configured to serve requests over a plain socket.
     * <p>
     * If no server is set using this method, then a server instance is created during
     * startup of this adapter based on the <em>config</em> properties and the server options
     * returned by {@link #getInsecureHttpServerOptions()}.
     * 
     * @param server The http server.
     * @throws NullPointerException if server is {@code null}.
     * @throws IllegalArgumentException if the server is already started and listening on an address/port.
     */
    @Autowired(required = false)
    public final void setInsecureHttpServer(final HttpServer server) {
        Objects.requireNonNull(server);
        if (server.actualPort() > 0) {
            throw new IllegalArgumentException("http server must not be started already");
        } else {
            this.insecureServer = server;
        }
    }

    @Override
    public final void doStart(final Future<Void> startFuture) {

        checkPortConfiguration().compose(s -> preStartup()).compose(s -> {
            if (metrics == null) {
                // use default implementation
                // which simply discards all reported metrics
                metrics = new HttpAdapterMetrics();
            }
            final Router router = createRouter();
            if (router == null) {
                return Future.failedFuture("no router configured");
            } else {
                addRoutes(router);
                return CompositeFuture.all(bindSecureHttpServer(router), bindInsecureHttpServer(router));
            }
        }).compose(s -> {
            try {
                onStartupSuccess();
                startFuture.complete();
            } catch (Exception e) {
                LOG.error("error in onStartupSuccess", e);
                startFuture.fail(e);
            }
        }, startFuture);
    }

    /**
     * Invoked before the http server is started.
     * <p>
     * May be overridden by sub-classes to provide additional startup handling.
     * 
     * @return A future indicating the outcome of the operation. The start up process fails if the returned future fails.
     */
    protected Future<Void> preStartup() {

        return Future.succeededFuture();
    }

    /**
     * Invoked after this adapter has started up successfully.
     * <p>
     * May be overridden by sub-classes.
     */
    protected void onStartupSuccess() {
        // empty
    }

    /**
     * Creates the router for handling requests.
     * <p>
     * This method creates a router instance with the following routes:
     * <ol>
     * <li>A default route limiting the body size of requests to the maximum payload size set in the <em>config</em> properties.</li>
     * </ol>
     * 
     * @return The newly created router (never {@code null}).
     */
    protected Router createRouter() {

        final Router router = Router.router(vertx);
        LOG.info("limiting size of inbound request body to {} bytes", getConfig().getMaxPayloadSize());
        router.route().handler(
                BodyHandler.create(DEFAULT_UPLOADS_DIRECTORY).setBodyLimit(getConfig().getMaxPayloadSize()));

        return router;
    }

    /**
     * Adds custom routes for handling requests.
     * <p>
     * This method is invoked right before the http server is started with the value returned by
     * {@link AbstractVertxBasedHttpProtocolAdapter#createRouter()}.
     * 
     * @param router The router to add the custom routes to.
     */
    protected abstract void addRoutes(Router router);

    /**
     * Gets the options to use for creating the TLS secured http server.
     * <p>
     * Subclasses may override this method in order to customize the server.
     * <p>
     * This method returns default options with the host and port being set to the corresponding values
     * from the <em>config</em> properties and using a maximum chunk size of 4096 bytes.
     * 
     * @return The http server options.
     */
    protected HttpServerOptions getHttpServerOptions() {

        final HttpServerOptions options = new HttpServerOptions();
        options.setHost(getConfig().getBindAddress()).setPort(getConfig().getPort(getPortDefaultValue()))
                .setMaxChunkSize(4096);
        addTlsKeyCertOptions(options);
        addTlsTrustOptions(options);
        return options;
    }

    /**
     * Gets the options to use for creating the insecure http server.
     * <p>
     * Subclasses may override this method in order to customize the server.
     * <p>
     * This method returns default options with the host and port being set to the corresponding values
     * from the <em>config</em> properties and using a maximum chunk size of 4096 bytes.
     * 
     * @return The http server options.
     */
    protected HttpServerOptions getInsecureHttpServerOptions() {

        final HttpServerOptions options = new HttpServerOptions();
        options.setHost(getConfig().getInsecurePortBindAddress())
                .setPort(getConfig().getInsecurePort(getInsecurePortDefaultValue())).setMaxChunkSize(4096);
        return options;
    }

    /**
     * Invoked before the message is sent to the downstream peer.
     * <p>
     * Subclasses may override this method in order to customize the message
     * before it is sent, e.g. adding custom properties.
     * 
     * @param downstreamMessage The message that will be sent downstream.
     * @param ctx The routing context.
     */
    protected void customizeDownstreamMessage(final Message downstreamMessage, final RoutingContext ctx) {
        // this default implementation does nothing
    }

    /**
     * Gets the authenticated device identity from the routing context.
     * 
     * @param ctx The routing context.
     * @return The device or {@code null} if the device has not been authenticated.
     */
    protected final Device getAuthenticatedDevice(final RoutingContext ctx) {

        return Optional.ofNullable(ctx.user()).map(user -> {
            if (Device.class.isInstance(user)) {
                return (Device) user;
            } else {
                return null;
            }
        }).orElse(null);
    }

    private Future<HttpServer> bindSecureHttpServer(final Router router) {

        if (isSecurePortEnabled()) {
            final Future<HttpServer> result = Future.future();
            final String bindAddress = server == null ? getConfig().getBindAddress() : "?";
            if (server == null) {
                server = vertx.createHttpServer(getHttpServerOptions());
            }
            server.requestHandler(router::accept).listen(done -> {
                if (done.succeeded()) {
                    LOG.info("secure http server listening on {}:{}", bindAddress, server.actualPort());
                    result.complete(done.result());
                } else {
                    LOG.error("error while starting up secure http server", done.cause());
                    result.fail(done.cause());
                }
            });
            return result;
        } else {
            return Future.succeededFuture();
        }
    }

    private Future<HttpServer> bindInsecureHttpServer(final Router router) {

        if (isInsecurePortEnabled()) {
            final Future<HttpServer> result = Future.future();
            final String bindAddress = insecureServer == null ? getConfig().getInsecurePortBindAddress() : "?";
            if (insecureServer == null) {
                insecureServer = vertx.createHttpServer(getInsecureHttpServerOptions());
            }
            insecureServer.requestHandler(router::accept).listen(done -> {
                if (done.succeeded()) {
                    LOG.info("insecure http server listening on {}:{}", bindAddress, insecureServer.actualPort());
                    result.complete(done.result());
                } else {
                    LOG.error("error while starting up insecure http server", done.cause());
                    result.fail(done.cause());
                }
            });
            return result;
        } else {
            return Future.succeededFuture();
        }
    }

    @Override
    public final void doStop(final Future<Void> stopFuture) {

        try {
            preShutdown();
        } catch (Exception e) {
            LOG.error("error in preShutdown", e);
        }

        final Future<Void> serverStopTracker = Future.future();
        if (server != null) {
            server.close(serverStopTracker.completer());
        } else {
            serverStopTracker.complete();
        }

        final Future<Void> insecureServerStopTracker = Future.future();
        if (insecureServer != null) {
            insecureServer.close(insecureServerStopTracker.completer());
        } else {
            insecureServerStopTracker.complete();
        }

        CompositeFuture.all(serverStopTracker, insecureServerStopTracker).compose(v -> postShutdown())
                .compose(s -> stopFuture.complete(), stopFuture);
    }

    /**
     * Invoked before the Http server is shut down.
     * May be overridden by sub-classes.
     */
    protected void preShutdown() {
        // empty
    }

    /**
     * Invoked after the Adapter has been shutdown successfully.
     * May be overridden by sub-classes to provide further shutdown handling.
     * 
     * @return A future that has to be completed when this operation is finished.
     */
    protected Future<Void> postShutdown() {
        return Future.succeededFuture();
    }

    /**
     * Uploads the body of an HTTP request as a telemetry message to the Hono server.
     * <p>
     * This method simply invokes {@link #uploadTelemetryMessage(RoutingContext, String, String, Buffer, String)}
     * with objects retrieved from the routing context.
     *
     * @param ctx The context to retrieve the message payload and content type from.
     * @param tenant The tenant of the device that has produced the data.
     * @param deviceId The id of the device that has produced the data.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public final void uploadTelemetryMessage(final RoutingContext ctx, final String tenant, final String deviceId) {

        uploadTelemetryMessage(Objects.requireNonNull(ctx), Objects.requireNonNull(tenant),
                Objects.requireNonNull(deviceId), ctx.getBody(), HttpUtils.getContentType(ctx));
    }

    /**
     * Uploads a telemetry message to the Hono server.
     * <p>
     * Depending on the outcome of the attempt to upload the message to Hono, the HTTP response's code is
     * set as follows:
     * <ul>
     * <li>202 (Accepted) - if the telemetry message has been sent to the Hono server.</li>
     * <li>400 (Bad Request) - if the message payload is {@code null} or empty or if the content type is {@code null}.</li>
     * <li>503 (Service Unavailable) - if the message could not be sent to the Hono server, e.g. due to lack of connection or credit.</li>
     * </ul>
     * 
     * @param ctx The context to retrieve cookies and the HTTP response from.
     * @param tenant The tenant of the device that has produced the data.
     * @param deviceId The id of the device that has produced the data.
     * @param payload The message payload to send.
     * @param contentType The content type of the message payload.
     * @throws NullPointerException if any of response, tenant or device ID is {@code null}.
     */
    public final void uploadTelemetryMessage(final RoutingContext ctx, final String tenant, final String deviceId,
            final Buffer payload, final String contentType) {

        doUploadMessage(Objects.requireNonNull(ctx), Objects.requireNonNull(tenant),
                Objects.requireNonNull(deviceId), payload, contentType, getTelemetrySender(tenant),
                TelemetryConstants.TELEMETRY_ENDPOINT);
    }

    /**
     * Uploads the body of an HTTP request as an event message to the Hono server.
     * <p>
     * This method simply invokes {@link #uploadEventMessage(RoutingContext, String, String, Buffer, String)}
     * with objects retrieved from the routing context.
     *
     * @param ctx The context to retrieve the message payload and content type from.
     * @param tenant The tenant of the device that has produced the data.
     * @param deviceId The id of the device that has produced the data.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public final void uploadEventMessage(final RoutingContext ctx, final String tenant, final String deviceId) {

        uploadEventMessage(Objects.requireNonNull(ctx), Objects.requireNonNull(tenant),
                Objects.requireNonNull(deviceId), ctx.getBody(), HttpUtils.getContentType(ctx));
    }

    /**
     * Uploads an event message to the Hono server.
     * <p>
     * Depending on the outcome of the attempt to upload the message to Hono, the HTTP response's code is
     * set as follows:
     * <ul>
     * <li>202 (Accepted) - if the telemetry message has been sent to the Hono server.</li>
     * <li>400 (Bad Request) - if the message payload is {@code null} or empty or if the content type is {@code null}.</li>
     * <li>503 (Service Unavailable) - if the message could not be sent to the Hono server, e.g. due to lack of connection or credit.</li>
     * </ul>
     * 
     * @param ctx The context to retrieve cookies and the HTTP response from.
     * @param tenant The tenant of the device that has produced the data.
     * @param deviceId The id of the device that has produced the data.
     * @param payload The message payload to send.
     * @param contentType The content type of the message payload.
     * @throws NullPointerException if any of response, tenant or device ID is {@code null}.
     */
    public final void uploadEventMessage(final RoutingContext ctx, final String tenant, final String deviceId,
            final Buffer payload, final String contentType) {

        doUploadMessage(Objects.requireNonNull(ctx), Objects.requireNonNull(tenant),
                Objects.requireNonNull(deviceId), payload, contentType, getEventSender(tenant),
                EventConstants.EVENT_ENDPOINT);
    }

    private void doUploadMessage(final RoutingContext ctx, final String tenant, final String deviceId,
            final Buffer payload, final String contentType, final Future<MessageSender> senderTracker,
            final String endpointName) {

        if (!isPayloadOfIndicatedType(payload, contentType)) {
            HttpUtils.badRequest(ctx,
                    String.format("Content-Type %s does not match with the payload", contentType));
        } else {
            final Integer qosHeader = getQoSLevel(ctx.request().getHeader(Constants.HEADER_QOS_LEVEL));
            if (contentType == null) {
                HttpUtils.badRequest(ctx, String.format("%s header is missing", HttpHeaders.CONTENT_TYPE));
            } else if (qosHeader != null && qosHeader == HEADER_QOS_INVALID) {
                HttpUtils.badRequest(ctx, "Bad QoS Header Value");
            } else {

                final Device authenticatedDevice = getAuthenticatedDevice(ctx);
                final Future<JsonObject> tokenTracker = getRegistrationAssertion(tenant, deviceId,
                        authenticatedDevice);
                final Future<TenantObject> tenantConfigTracker = getTenantConfiguration(tenant);

                // AtomicBoolean to control if the downstream message was sent successfully
                final AtomicBoolean downstreamMessageSent = new AtomicBoolean(false);
                // AtomicReference to a Handler to be called to close an open command receiver link.
                final AtomicReference<Handler<Void>> closeLinkAndTimerHandlerRef = new AtomicReference<>();

                // Handler to be called with a received command. If the timer expired, null is provided as command.
                final Handler<Message> commandReceivedHandler = commandMessage -> {
                    // reset the closeHandler reference, since it is not valid anymore at this time.
                    closeLinkAndTimerHandlerRef.set(null);
                    if (downstreamMessageSent.get()) {
                        // finish the request, since the response is now complete (command was added)
                        if (!ctx.response().closed()) {
                            ctx.response().end();
                        }
                    }
                };

                CompositeFuture.all(tokenTracker, tenantConfigTracker, senderTracker).compose(ok -> {

                    if (tenantConfigTracker.result().isAdapterEnabled(getTypeName())) {
                        final MessageSender sender = senderTracker.result();
                        final Message downstreamMessage = newMessage(
                                ResourceIdentifier.from(endpointName, tenant, deviceId),
                                sender.isRegistrationAssertionRequired(), ctx.request().uri(), contentType, payload,
                                tokenTracker.result(), HttpUtils.getTimeTilDisconnect(ctx));
                        customizeDownstreamMessage(downstreamMessage, ctx);

                        // first open the command receiver link (if needed)
                        return openCommandReceiverLink(ctx, tenant, deviceId, commandReceivedHandler)
                                .compose(closeLinkAndTimerHandler -> {
                                    closeLinkAndTimerHandlerRef.set(closeLinkAndTimerHandler);

                                    if (qosHeader == null) {
                                        return sender.send(downstreamMessage);
                                    } else {
                                        return sender.sendAndWaitForOutcome(downstreamMessage);
                                    }
                                });
                    } else {
                        // this adapter is not enabled for the tenant
                        return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_FORBIDDEN));
                    }
                }).compose(delivery -> {
                    LOG.trace(
                            "successfully processed message for device [tenantId: {}, deviceId: {}, endpoint: {}]",
                            tenant, deviceId, endpointName);
                    metrics.incrementProcessedHttpMessages(endpointName, tenant);
                    ctx.response().setStatusCode(HttpURLConnection.HTTP_ACCEPTED);
                    downstreamMessageSent.set(true);

                    // if no command timer was created, the request now can be responded
                    if (closeLinkAndTimerHandlerRef.get() == null) {
                        ctx.response().end();
                    }

                    return Future.succeededFuture();

                }).recover(t -> {

                    LOG.debug("cannot process message for device [tenantId: {}, deviceId: {}, endpoint: {}]",
                            tenant, deviceId, endpointName, t);

                    cancelResponseTimer(closeLinkAndTimerHandlerRef);

                    if (ClientErrorException.class.isInstance(t)) {
                        final ClientErrorException e = (ClientErrorException) t;
                        ctx.fail(e.getErrorCode());
                    } else {
                        metrics.incrementUndeliverableHttpMessages(endpointName, tenant);
                        HttpUtils.serviceUnavailable(ctx, 2);
                    }
                    return Future.failedFuture(t);
                });
            }
        }
    }

    private void cancelResponseTimer(final AtomicReference<Handler<Void>> cancelTimerHandlerRef) {
        Optional.ofNullable(cancelTimerHandlerRef.get()).map(cancelTimerHandler -> {
            cancelTimerHandler.handle(null);
            return null;
        });
    }

    private void closeCommandReceiverLink(final AtomicReference<MessageConsumer> messageConsumerRef) {
        Optional.ofNullable(messageConsumerRef.get()).map(messageConsumer -> {
            messageConsumer.close(v2 -> {
            });
            return null;
        });
    }

    /**
     * Create a consumer for a command message that can be used by the command and control consumer.
     *
     * @param ctx The routing context of the HTTP request.
     * @param tenant The tenant of the device for that a command may be received.
     * @param deviceId The id of the device for that a command may be received.
     * @param commandReceivedHandler A handler that is invoked after a command message was received.
     * @return The BiConsumer to pass to the command and control consumer.
     */
    private BiConsumer<ProtonDelivery, Message> createCommandMessageConsumer(final RoutingContext ctx,
            final String tenant, final String deviceId, final Handler<Message> commandReceivedHandler) {
        return (delivery, commandMessage) -> {

            final Optional<String> commandSubject = Optional
                    .ofNullable(commandMessage.getProperties().getSubject());
            final Optional<String> commandRequestId = validateAndGenerateCommandRequestId(tenant, deviceId,
                    commandMessage);

            if (commandSubject.isPresent() && commandRequestId.isPresent()) {

                ctx.response().putHeader(Constants.HEADER_COMMAND, commandSubject.get());
                ctx.response().putHeader(Constants.HEADER_COMMAND_REQUEST_ID, commandRequestId.get());

                HttpUtils.setResponseBody(ctx.response(), MessageHelper.getPayload(commandMessage));

                commandReceivedHandler.handle(commandMessage);

            } else {
                LOG.info(
                        "Received command with invalid reply-to endpoint for device [tenantId: {}, deviceId: {}] - ignoring.",
                        tenant, deviceId);
            }
        };

    }

    /**
     * Opens a command receiver link for a device by creating a command and control consumer.
     *
     * @param ctx The routing context of the HTTP request.
     * @param tenant The tenant of the device for that a command may be received.
     * @param deviceId The id of the device for that a command may be received.
     * @param commandReceivedHandler Handler to be called after a command was received or the timer expired.
     *                               The link was closed at this time already.
     *                               If the timer expired, the passed command message is null, otherwise the received command message is passed.
     *
     * @return Optional An optional handler that cancels a timer that might have been started to close the receiver link again.
     */
    private Future<Handler<Void>> openCommandReceiverLink(final RoutingContext ctx, final String tenant,
            final String deviceId, final Handler<Message> commandReceivedHandler) {

        final Future<Handler<Void>> resultWithLinkCloseHandler = Future.future();

        final AtomicReference<MessageConsumer> messageConsumerRef = new AtomicReference<>(null);

        final Integer timeToDelayResponse = Optional.ofNullable(HttpUtils.getTimeTilDisconnect(ctx)).orElse(-1);

        if (timeToDelayResponse > 0) {
            // create a handler for being invoked if a command was received
            final Handler<Message> commandHandler = commandMessage -> {
                // if a link close handler was set, invoke it
                Optional.ofNullable(resultWithLinkCloseHandler.result()).map(linkCloseHandler -> {
                    linkCloseHandler.handle(null);
                    return null;
                });

                // if desired, now invoke the passed commandReceivedHandler and pass the message
                Optional.ofNullable(commandReceivedHandler).map(h -> {
                    h.handle(commandMessage);
                    return null;
                });

                final Optional<String> replyIdOpt = getReplyToIdFromCommand(tenant, deviceId, commandMessage);
                if (!replyIdOpt.isPresent()) {
                    // from Java 9 on: switch to opt.ifPresentOrElse
                    LOG.debug(
                            "Received command without valid replyId for device [tenantId: {}, deviceId: {}] - no reply will be sent to the application",
                            tenant, deviceId);
                } else {
                    replyIdOpt.map(replyId -> {
                        // send answer to caller via sender link
                        final Future<CommandResponseSender> responseSender = createCommandResponseSender(tenant,
                                deviceId, replyId);
                        responseSender.compose(commandResponseSender -> commandResponseSender.sendCommandResponse(
                                getCorrelationIdFromMessage(commandMessage), null, null, HttpURLConnection.HTTP_OK))
                                .map(delivery -> {
                                    LOG.debug("acknowledged command [message-id: {}]",
                                            commandMessage.getMessageId());
                                    responseSender.result().close(v -> {
                                    });
                                    return null;
                                }).otherwise(t -> {
                                    LOG.debug("could not acknowledge command [message-id: {}]",
                                            commandMessage.getMessageId(), t);
                                    return Optional.ofNullable(responseSender.result()).map(r -> {
                                        r.close(v -> {
                                        });
                                        return null;
                                    }).orElse(null);
                                });

                        return replyId;
                    });

                }
            };

            // create the commandMessageConsumer that handles an incoming command message
            final BiConsumer<ProtonDelivery, Message> commandMessageConsumer = createCommandMessageConsumer(ctx,
                    tenant, deviceId, commandHandler);

            createCommandConsumer(tenant, deviceId, commandMessageConsumer,
                    v -> onCloseCommandConsumer(tenant, deviceId, commandMessageConsumer)).map(messageConsumer -> {
                        // remember message consumer for later usage
                        messageConsumerRef.set(messageConsumer);
                        // let only one command reach the adapter (may change in the future)
                        messageConsumer.flow(1);

                        // create a timer that is invoked if no command was received until timeToDelayResponse is expired
                        final long timerId = getVertx().setTimer(timeToDelayResponse * 1000L, delay -> {
                            closeCommandReceiverLink(messageConsumerRef);
                            // command finished, invoke handler
                            Optional.ofNullable(commandReceivedHandler).map(h -> {
                                h.handle(null);
                                return null;
                            });
                        });
                        // define the cancel code as closure
                        resultWithLinkCloseHandler.complete(v -> {
                            getVertx().cancelTimer(timerId);
                            closeCommandReceiverLink(messageConsumerRef);
                        });

                        return messageConsumer;
                    }).recover(t -> {
                        closeCommandReceiverLink(messageConsumerRef);
                        resultWithLinkCloseHandler.fail(t);
                        return Future.failedFuture(t);
                    });

        } else {
            resultWithLinkCloseHandler.complete();
        }

        return resultWithLinkCloseHandler;
    }

    private static Integer getQoSLevel(final String qosValue) {
        try {
            if (qosValue == null) {
                return null;
            } else {
                return Integer.parseInt(qosValue) != AT_LEAST_ONCE ? HEADER_QOS_INVALID : AT_LEAST_ONCE;
            }
        } catch (NumberFormatException e) {
            return HEADER_QOS_INVALID;
        }
    }
}