org.eclipse.hono.service.amqp.AmqpServiceBase.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.service.amqp.AmqpServiceBase.java

Source

/**
 * Copyright (c) 2017, 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.service.amqp;

import java.lang.ref.WeakReference;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.transport.AmqpError;
import org.apache.qpid.proton.amqp.transport.Source;
import org.eclipse.hono.auth.Activity;
import org.eclipse.hono.auth.HonoUser;
import org.eclipse.hono.config.ServiceConfigProperties;
import org.eclipse.hono.service.AbstractServiceBase;
import org.eclipse.hono.service.auth.AuthorizationService;
import org.eclipse.hono.service.auth.ClaimsBasedAuthorizationService;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.ResourceIdentifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import io.vertx.core.AsyncResult;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.ext.healthchecks.HealthCheckHandler;
import io.vertx.proton.ProtonConnection;
import io.vertx.proton.ProtonHelper;
import io.vertx.proton.ProtonLink;
import io.vertx.proton.ProtonReceiver;
import io.vertx.proton.ProtonSender;
import io.vertx.proton.ProtonServer;
import io.vertx.proton.ProtonServerOptions;
import io.vertx.proton.ProtonSession;
import io.vertx.proton.sasl.ProtonSaslAuthenticatorFactory;

/**
 * A base class for implementing services using AMQP 1.0 as the transport protocol.
 * <p>
 * This class provides support for implementing an AMQP 1.0 container hosting arbitrary
 * API {@link AmqpEndpoint}s. An endpoint can be used to handle messages being sent to and/or
 * received from an AMQP <em>node</em> represented by an address prefix.
 * 
 * @param <T> The type of configuration properties this service uses.
 */
public abstract class AmqpServiceBase<T extends ServiceConfigProperties> extends AbstractServiceBase<T> {

    private final Map<String, AmqpEndpoint> endpoints = new HashMap<>();

    private ProtonServer server;
    private ProtonServer insecureServer;
    private ProtonSaslAuthenticatorFactory saslAuthenticatorFactory;
    private AuthorizationService authorizationService;

    /**
     * Gets the name of the service, that may be used for the container name on amqp connections e.g.
     * @return The name of the service.
     */
    protected abstract String getServiceName();

    @Autowired
    @Qualifier(Constants.QUALIFIER_AMQP)
    @Override
    public void setConfig(final T configuration) {
        setSpecificConfig(configuration);
    }

    /**
     * Gets the default port number of the secure AMQP port.
     * 
     * @return {@link Constants#PORT_AMQPS}
     */
    @Override
    public int getPortDefaultValue() {
        return Constants.PORT_AMQPS;
    }

    /**
     * Gets the default port number of the non-secure AMQP port.
     * 
     * @return {@link Constants#PORT_AMQP}
     */
    @Override
    public int getInsecurePortDefaultValue() {
        return Constants.PORT_AMQP;
    }

    /**
     * Adds multiple endpoints to this server.
     *
     * @param definedEndpoints The endpoints.
     */
    @Autowired(required = false)
    public final void addEndpoints(final List<AmqpEndpoint> definedEndpoints) {
        Objects.requireNonNull(definedEndpoints);
        for (final AmqpEndpoint ep : definedEndpoints) {
            addEndpoint(ep);
        }
    }

    /**
     * Adds an endpoint to this server.
     *
     * @param ep The endpoint.
     */
    public final void addEndpoint(final AmqpEndpoint ep) {
        if (endpoints.putIfAbsent(ep.getName(), ep) != null) {
            LOG.warn("multiple endpoints defined with name [{}]", ep.getName());
        } else {
            LOG.debug("registering endpoint [{}]", ep.getName());
        }
    }

    /**
     * Gets the endpoint registered for handling a specific target address.
     * 
     * @param targetAddress The address.
     * @return The endpoint for handling the address or {@code null} if no endpoint is registered
     *         for the target address.
     */
    protected final AmqpEndpoint getEndpoint(final ResourceIdentifier targetAddress) {
        return getEndpoint(targetAddress.getEndpoint());
    }

    /**
     * Gets the endpoint registered for a given name.
     * 
     * @param endpointName The name.
     * @return The endpoint registered under the given name or {@code null} if no endpoint has been registered
     *         under the name.
     */
    protected final AmqpEndpoint getEndpoint(final String endpointName) {
        return endpoints.get(endpointName);
    }

    /**
     * Iterates over the endpoints registered with this service.
     * 
     * @return The endpoints.
     */
    protected final Iterable<AmqpEndpoint> endpoints() {
        return endpoints.values();
    }

    /**
     * Sets the factory to use for creating objects performing SASL based authentication of clients.
     *
     * @param factory The factory.
     * @throws NullPointerException if factory is {@code null}.
     */
    @Autowired(required = false)
    public void setSaslAuthenticatorFactory(final ProtonSaslAuthenticatorFactory factory) {
        this.saslAuthenticatorFactory = Objects.requireNonNull(factory);
    }

    /**
     * Sets the object to use for authorizing access to resources and operations.
     * 
     * @param authService The authorization service to use.
     * @throws NullPointerException if the service is {@code null}.
     */
    public final void setAuthorizationService(final AuthorizationService authService) {
        this.authorizationService = authService;
    }

    /**
     * Gets the object used for authorizing access to resources and operations.
     * 
     * @return The authorization service to use.
     */
    protected final AuthorizationService getAuthorizationService() {
        return authorizationService;
    }

    @Override
    public Future<Void> startInternal() {

        if (authorizationService == null) {
            authorizationService = new ClaimsBasedAuthorizationService();
        }
        return preStartServers().compose(s -> checkPortConfiguration()).compose(s -> startEndpoints())
                .compose(s -> startSecureServer()).compose(s -> startInsecureServer());
    }

    /**
     * Closes an expired client connection.
     * <p>
     * A connection is considered expired if the {@link HonoUser#isExpired()} method
     * of the user principal attached to the connection returns {@code true}.
     * 
     * @param con The client connection.
     */
    protected final void closeExpiredConnection(final ProtonConnection con) {

        if (!con.isDisconnected()) {
            final HonoUser clientPrincipal = Constants.getClientPrincipal(con);
            if (clientPrincipal != null) {
                LOG.debug("client's [{}] access token has expired, closing connection", clientPrincipal.getName());
                con.disconnectHandler(null);
                con.closeHandler(null);
                con.setCondition(ProtonHelper.condition(AmqpError.UNAUTHORIZED_ACCESS, "access token expired"));
                con.close();
                con.disconnect();
                publishConnectionClosedEvent(con);
            }
        }
    }

    /**
     * Invoked before binding listeners to the configured socket addresses.
     * <p>
     * Subclasses may override this method to do any kind of initialization work.
     * 
     * @return A future indicating the outcome of the operation. The listeners will not
     *         be bound if the returned future fails.
     */
    protected Future<Void> preStartServers() {
        return Future.succeededFuture();
    }

    private Future<Void> startEndpoints() {

        @SuppressWarnings("rawtypes")
        final List<Future> endpointFutures = new ArrayList<>(endpoints.size());
        for (final AmqpEndpoint ep : endpoints.values()) {
            LOG.info("starting endpoint [name: {}, class: {}]", ep.getName(), ep.getClass().getName());
            endpointFutures.add(ep.start());
        }
        final Future<Void> startFuture = Future.future();
        CompositeFuture.all(endpointFutures).setHandler(startup -> {
            if (startup.succeeded()) {
                startFuture.complete();
            } else {
                startFuture.fail(startup.cause());
            }
        });
        return startFuture;
    }

    private Future<Void> stopEndpoints() {

        @SuppressWarnings("rawtypes")
        final List<Future> endpointFutures = new ArrayList<>(endpoints.size());
        for (final AmqpEndpoint ep : endpoints.values()) {
            LOG.info("stopping endpoint [name: {}, class: {}]", ep.getName(), ep.getClass().getName());
            endpointFutures.add(ep.stop());
        }
        final Future<Void> stopFuture = Future.future();
        CompositeFuture.all(endpointFutures).setHandler(shutdown -> {
            if (shutdown.succeeded()) {
                stopFuture.complete();
            } else {
                stopFuture.fail(shutdown.cause());
            }
        });
        return stopFuture;
    }

    private Future<Void> startInsecureServer() {

        if (isInsecurePortEnabled()) {
            final int insecurePort = determineInsecurePort();
            final Future<Void> result = Future.future();
            final ProtonServerOptions options = createInsecureServerOptions();
            insecureServer = createProtonServer(options).connectHandler(this::onRemoteConnectionOpenInsecurePort)
                    .listen(insecurePort, getConfig().getInsecurePortBindAddress(), bindAttempt -> {
                        if (bindAttempt.succeeded()) {
                            if (getInsecurePort() == getInsecurePortDefaultValue()) {
                                LOG.info("server listens on standard insecure port [{}:{}]",
                                        getInsecurePortBindAddress(), getInsecurePort());
                            } else {
                                LOG.warn("server listens on non-standard insecure port [{}:{}], default is {}",
                                        getInsecurePortBindAddress(), getInsecurePort(),
                                        getInsecurePortDefaultValue());
                            }
                            result.complete();
                        } else {
                            LOG.error("cannot bind to insecure port", bindAttempt.cause());
                            result.fail(bindAttempt.cause());
                        }
                    });
            return result;
        } else {
            LOG.info("insecure port is not enabled");
            return Future.succeededFuture();
        }
    }

    private Future<Void> startSecureServer() {

        if (isSecurePortEnabled()) {
            final int securePort = determineSecurePort();
            final Future<Void> result = Future.future();
            final ProtonServerOptions options = createServerOptions();
            server = createProtonServer(options).connectHandler(this::onRemoteConnectionOpen).listen(securePort,
                    getConfig().getBindAddress(), bindAttempt -> {
                        if (bindAttempt.succeeded()) {
                            if (getPort() == getPortDefaultValue()) {
                                LOG.info("server listens on standard secure port [{}:{}]", getBindAddress(),
                                        getPort());
                            } else {
                                LOG.warn("server listens on non-standard secure port [{}:{}], default is {}",
                                        getBindAddress(), getPort(), getPortDefaultValue());
                            }
                            result.complete();
                        } else {
                            LOG.error("cannot bind to secure port", bindAttempt.cause());
                            result.fail(bindAttempt.cause());
                        }
                    });
            return result;
        } else {
            LOG.info("secure port is not enabled");
            return Future.succeededFuture();
        }
    }

    private ProtonServer createProtonServer(final ProtonServerOptions options) {
        return ProtonServer.create(vertx, options).saslAuthenticatorFactory(saslAuthenticatorFactory);
    }

    /**
     * Invoked during start up to create options for the proton server instance binding to the secure port.
     * <p>
     * Subclasses may override this method to set custom options.
     * 
     * @return The options.
     */
    protected ProtonServerOptions createServerOptions() {
        final ProtonServerOptions options = createInsecureServerOptions();
        addTlsKeyCertOptions(options);
        addTlsTrustOptions(options);
        return options;
    }

    /**
     * Invoked during start up to create options for the proton server instance binding to the insecure port.
     * <p>
     * Subclasses may override this method to set custom options.
     * 
     * @return The options.
     */
    protected ProtonServerOptions createInsecureServerOptions() {

        final ProtonServerOptions options = new ProtonServerOptions();
        options.setHeartbeat(60000); // // close idle connections after two minutes of inactivity
        options.setReceiveBufferSize(16 * 1024); // 16kb
        options.setSendBufferSize(16 * 1024); // 16kb
        options.setLogActivity(getConfig().isNetworkDebugLoggingEnabled());

        return options;
    }

    @Override
    public final Future<Void> stopInternal() {

        return CompositeFuture.all(stopServer(), stopInsecureServer()).compose(s -> stopEndpoints());
    }

    private Future<Void> stopServer() {

        final Future<Void> secureTracker = Future.future();

        if (server != null) {
            LOG.info("stopping secure AMQP server [{}:{}]", getBindAddress(), getActualPort());
            server.close(secureTracker.completer());
        } else {
            secureTracker.complete();
        }
        return secureTracker;
    }

    private Future<Void> stopInsecureServer() {

        final Future<Void> insecureTracker = Future.future();

        if (insecureServer != null) {
            LOG.info("stopping insecure AMQP server [{}:{}]", getInsecurePortBindAddress(),
                    getActualInsecurePort());
            insecureServer.close(insecureTracker.completer());
        } else {
            insecureTracker.complete();
        }
        return insecureTracker;
    }

    @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;
    }

    /**
     * Invoked when an AMQP <em>open</em> frame is received on the secure port.
     * <p>
     * This method
     * <ol>
     * <li>sets the container name</li>
     * <li>invokes {@link #setRemoteConnectionOpenHandler(ProtonConnection)}</li>
     * </ol>
     * <p>
     * Subclasses may override this method to set custom handlers.
     *
     * @param connection The AMQP connection that the frame is supposed to establish.
     */
    protected void onRemoteConnectionOpen(final ProtonConnection connection) {
        connection.setContainer(String.format("%s-%s:%d", getServiceName(), getBindAddress(), getPort()));
        setRemoteConnectionOpenHandler(connection);
    }

    /**
     * Invoked when an AMQP <em>open</em> frame is received on the insecure port.
     * <p>
     * This method
     * <ol>
     * <li>sets the container name</li>
     * <li>invokes {@link #setRemoteConnectionOpenHandler(ProtonConnection)}</li>
     * </ol>
     * <p>
     * Subclasses may override this method to set custom handlers.
     *
     * @param connection The AMQP connection that the frame is supposed to establish.
     * @throws NullPointerException if connection is {@code null}.
     */
    protected void onRemoteConnectionOpenInsecurePort(final ProtonConnection connection) {
        connection.setContainer(
                String.format("%s-%s:%d", getServiceName(), getInsecurePortBindAddress(), getInsecurePort()));
        setRemoteConnectionOpenHandler(connection);
    }

    /**
     * Closes a link for an unknown target address.
     * <p>
     * The link is closed with AMQP error code <em>amqp:not-found</em>.
     * 
     * @param con The connection that the link belongs to.
     * @param link The link.
     * @param address The unknown target address.
     */
    protected final void handleUnknownEndpoint(final ProtonConnection con, final ProtonLink<?> link,
            final ResourceIdentifier address) {
        LOG.info("client [container: {}] wants to establish link for unknown endpoint [address: {}]",
                con.getRemoteContainer(), address);
        link.setCondition(ProtonHelper.condition(AmqpError.NOT_FOUND,
                String.format("no endpoint registered for address %s", address)));
        link.close();
    }

    /**
     * Creates a resource identifier for a given address.
     * 
     * @param address The address. If this service is configured for
     *         a single tenant only then the address is assumed to <em>not</em> contain a tenant
     *         component.
     * @return The identifier representing the address.
     * @throws IllegalArgumentException if the given address does not represent a valid resource identifier.
     */
    protected final ResourceIdentifier getResourceIdentifier(final String address) {
        if (getConfig().isSingleTenant()) {
            return ResourceIdentifier.fromStringAssumingDefaultTenant(address);
        } else {
            return ResourceIdentifier.fromString(address);
        }
    }

    /**
     * Handles a request from a client to establish a link for sending messages to this server.
     * The already established connection must have an authenticated user as principal for doing the authorization check.
     *
     * @param con the connection to the client.
     * @param receiver the receiver created for the link.
     */
    protected void handleReceiverOpen(final ProtonConnection con, final ProtonReceiver receiver) {
        if (receiver.getRemoteTarget().getAddress() == null) {
            LOG.debug(
                    "client [container: {}] wants to open an anonymous link for sending messages to arbitrary addresses, closing link ...",
                    con.getRemoteContainer());
            receiver.setCondition(ProtonHelper.condition(AmqpError.NOT_ALLOWED, "anonymous relay not supported"));
            receiver.close();
        } else {
            LOG.debug("client [container: {}] wants to open a link [address: {}] for sending messages",
                    con.getRemoteContainer(), receiver.getRemoteTarget());
            try {
                final ResourceIdentifier targetResource = getResourceIdentifier(
                        receiver.getRemoteTarget().getAddress());
                final AmqpEndpoint endpoint = getEndpoint(targetResource);
                if (endpoint == null) {
                    handleUnknownEndpoint(con, receiver, targetResource);
                } else {
                    final HonoUser user = Constants.getClientPrincipal(con);
                    getAuthorizationService().isAuthorized(user, targetResource, Activity.WRITE)
                            .setHandler(authAttempt -> {
                                if (authAttempt.succeeded() && authAttempt.result()) {
                                    Constants.copyProperties(con, receiver);
                                    receiver.setTarget(receiver.getRemoteTarget());
                                    endpoint.onLinkAttach(con, receiver, targetResource);
                                } else {
                                    LOG.debug("subject [{}] is not authorized to WRITE to [{}]", user.getName(),
                                            targetResource);
                                    receiver.setCondition(ProtonHelper
                                            .condition(AmqpError.UNAUTHORIZED_ACCESS.toString(), "unauthorized"));
                                    receiver.close();
                                }
                            });
                }
            } catch (final IllegalArgumentException e) {
                LOG.debug("client has provided invalid resource identifier as target address", e);
                receiver.setCondition(ProtonHelper.condition(AmqpError.NOT_FOUND, "no such address"));
                receiver.close();
            }
        }
    }

    /**
     * Handles a request from a client to establish a link for receiving messages from this server.
     *
     * @param con the connection to the client.
     * @param sender the sender created for the link.
     */
    protected void handleSenderOpen(final ProtonConnection con, final ProtonSender sender) {
        final Source remoteSource = sender.getRemoteSource();
        LOG.debug("client [container: {}] wants to open a link [address: {}] for receiving messages",
                con.getRemoteContainer(), remoteSource);
        try {
            final ResourceIdentifier targetResource = getResourceIdentifier(remoteSource.getAddress());
            final AmqpEndpoint endpoint = getEndpoint(targetResource);
            if (endpoint == null) {
                handleUnknownEndpoint(con, sender, targetResource);
            } else {
                final HonoUser user = Constants.getClientPrincipal(con);
                getAuthorizationService().isAuthorized(user, targetResource, Activity.READ)
                        .setHandler(authAttempt -> {
                            if (authAttempt.succeeded() && authAttempt.result()) {
                                Constants.copyProperties(con, sender);
                                sender.setSource(sender.getRemoteSource());
                                endpoint.onLinkAttach(con, sender, targetResource);
                            } else {
                                LOG.debug("subject [{}] is not authorized to READ from [{}]", user.getName(),
                                        targetResource);
                                sender.setCondition(ProtonHelper.condition(AmqpError.UNAUTHORIZED_ACCESS.toString(),
                                        "unauthorized"));
                                sender.close();
                            }
                        });
            }
        } catch (final IllegalArgumentException e) {
            LOG.debug("client has provided invalid resource identifier as target address", e);
            sender.setCondition(ProtonHelper.condition(AmqpError.NOT_FOUND, "no such address"));
            sender.close();
        }
    }

    /**
     * Sets default handlers on a connection that has been opened
     * by a peer.
     * <p>
     * This method registers the following handlers
     * <ul>
     * <li>sessionOpenHandler - {@link #handleSessionOpen(ProtonConnection, ProtonSession)}</li>
     * <li>receiverOpenHandler - {@link #handleReceiverOpen(ProtonConnection, ProtonReceiver)}</li>
     * <li>senderOpenHandler - {@link #handleSenderOpen(ProtonConnection, ProtonSender)}</li>
     * <li>disconnectHandler - {@link #handleRemoteDisconnect(ProtonConnection)}</li>
     * <li>closeHandler - {@link #handleRemoteConnectionClose(ProtonConnection, AsyncResult)}</li>
     * <li>openHandler - {@link #processRemoteOpen(ProtonConnection)}</li>
     * </ul>
     * <p>
     * Subclasses should override this method in order to register service
     * specific handlers and/or to prevent registration of default handlers.
     * 
     * @param connection The connection.
     */
    protected void setRemoteConnectionOpenHandler(final ProtonConnection connection) {
        connection.sessionOpenHandler(remoteOpenSession -> handleSessionOpen(connection, remoteOpenSession));
        connection.receiverOpenHandler(remoteOpenReceiver -> handleReceiverOpen(connection, remoteOpenReceiver));
        connection.senderOpenHandler(remoteOpenSender -> handleSenderOpen(connection, remoteOpenSender));
        connection.disconnectHandler(this::handleRemoteDisconnect);
        connection.closeHandler(remoteClose -> handleRemoteConnectionClose(connection, remoteClose));
        connection.openHandler(remoteOpen -> {
            if (remoteOpen.failed()) {
                LOG.debug("ignoring peer's open frame containing error", remoteOpen.cause());
            } else {
                processRemoteOpen(remoteOpen.result());
            }
        });
    }

    /**
     * Processes a peer's AMQP <em>open</em> frame.
     * <p>
     * This default implementation
     * <ol>
     * <li>adds a unique connection identifier to the connection's attachments
     * under key {@link Constants#KEY_CONNECTION_ID}</li>
     * <li>invokes {@link #processDesiredCapabilities(ProtonConnection, Symbol[])}</li>
     * <li>sets a timer that closes the connection once the client's token
     * has expired</li>
     * <li>sends the AMQP <em>open</em> frame to the peer</li>
     * </ol>
     * 
     * @param connection The connection to open.
     */
    protected void processRemoteOpen(final ProtonConnection connection) {

        final HonoUser clientPrincipal = Constants.getClientPrincipal(connection);
        LOG.debug("client [container: {}, user: {}] connected", connection.getRemoteContainer(),
                clientPrincipal.getName());
        // attach an ID so that we can later inform downstream components when connection is closed
        connection.attachments().set(Constants.KEY_CONNECTION_ID, String.class, UUID.randomUUID().toString());
        processDesiredCapabilities(connection, connection.getRemoteDesiredCapabilities());
        final Duration delay = Duration.between(Instant.now(), clientPrincipal.getExpirationTime());
        final WeakReference<ProtonConnection> conRef = new WeakReference<>(connection);
        vertx.setTimer(delay.toMillis(), timerId -> {
            if (conRef.get() != null) {
                closeExpiredConnection(conRef.get());
            }
        });
        connection.open();
    }

    /**
     * Processes the capabilities desired by a client in its
     * AMQP <em>open</em> frame.
     * <p>
     * This default implementation does nothing.
     * 
     * @param connection The connection being opened by the client.
     * @param desiredCapabilities The capabilities.
     */
    protected void processDesiredCapabilities(final ProtonConnection connection,
            final Symbol[] desiredCapabilities) {
    }

    /**
     * Invoked when a client initiates a session (which is then opened in this method).
     * <p>
     * Subclasses should override this method if other behaviour shall be implemented on session open.
     *
     * @param con The connection of the session.
     * @param session The session that is initiated.
     */
    protected void handleSessionOpen(final ProtonConnection con, final ProtonSession session) {
        LOG.debug("opening new session with client [container: {}]", con.getRemoteContainer());
        session.closeHandler(sessionResult -> {
            session.close();
        });
        session.open();
    }

    /**
     * Is called whenever a proton connection was closed. The implementation is intentionally empty.
     * <p>
     * Subclasses should override this method to publish this as an event on the vertx bus if desired.
     *
     * @param con The connection that was closed.
     */
    protected void publishConnectionClosedEvent(final ProtonConnection con) {
    }

    /**
     * Invoked when a client closes the connection with this server.
     * <p>
     * This implementation closes and disconnects the connection.
     *
     * @param con The connection to close.
     * @param res The client's close frame.
     */
    protected void handleRemoteConnectionClose(final ProtonConnection con,
            final AsyncResult<ProtonConnection> res) {
        if (res.succeeded()) {
            LOG.debug("client [container: {}] closed connection", con.getRemoteContainer());
        } else {
            LOG.debug("client [container: {}] closed connection with error", con.getRemoteContainer(), res.cause());
        }
        con.close();
        con.disconnect();
        publishConnectionClosedEvent(con);
    }

    /**
     * Invoked when the client's transport connection is disconnected from this server.
     *
     * @param con The connection that was disconnected.
     */
    protected void handleRemoteDisconnect(final ProtonConnection con) {
        LOG.debug("client [container: {}] disconnected", con.getRemoteContainer());
        con.disconnect();
        publishConnectionClosedEvent(con);
    }

    /**
     * Registers this service's endpoints' readiness checks.
     * <p>
     * This default implementation invokes {@link AmqpEndpoint#registerReadinessChecks(HealthCheckHandler)}
     * for all registered endpoints.
     * <p>
     * Subclasses should override this method to register more specific checks.
     * 
     * @param handler The health check handler to register the checks with.
     */
    @Override
    public void registerReadinessChecks(final HealthCheckHandler handler) {

        for (final AmqpEndpoint ep : endpoints()) {
            ep.registerReadinessChecks(handler);
        }
    }

    /**
     * Registers this service's endpoints' liveness checks.
     * <p>
     * This default implementation invokes {@link AmqpEndpoint#registerLivenessChecks(HealthCheckHandler)}
     * for all registered endpoints.
     * <p>
     * Subclasses should override this method to register more specific checks.
     * 
     * @param handler The health check handler to register the checks with.
     */
    @Override
    public void registerLivenessChecks(final HealthCheckHandler handler) {
        for (final AmqpEndpoint ep : endpoints()) {
            ep.registerLivenessChecks(handler);
        }
    }
}