org.eclipse.hono.connection.ConnectionFactoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.connection.ConnectionFactoryImpl.java

Source

/**
 * Copyright (c) 2016 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.connection;

import java.util.Objects;
import java.util.UUID;

import org.eclipse.hono.config.ClientConfigProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.core.net.TrustOptions;
import io.vertx.proton.ProtonClient;
import io.vertx.proton.ProtonClientOptions;
import io.vertx.proton.ProtonConnection;
import io.vertx.proton.sasl.impl.ProtonSaslExternalImpl;
import io.vertx.proton.sasl.impl.ProtonSaslPlainImpl;

/**
 * A <em>vertx-proton</em> based connection factory.
 */
public class ConnectionFactoryImpl implements ConnectionFactory {

    private static final Logger logger = LoggerFactory.getLogger(ConnectionFactoryImpl.class);
    private final Vertx vertx;
    private final ClientConfigProperties config;

    /**
     * Constructor with the Vert.x instance to use and the configuration 
     * parameters for connecting to the AMQP server as parameters.
     * <p>
     * The <em>name</em> property of the configuration is used as the basis
     * for the local container name which is then appended with a UUID.
     * 
     * @param vertx The Vert.x instance.
     * @param config The configuration parameters.
     * @throws NullPointerException if the parameters are {@code null}.
     */
    public ConnectionFactoryImpl(final Vertx vertx, final ClientConfigProperties config) {
        this.vertx = Objects.requireNonNull(vertx);
        this.config = Objects.requireNonNull(config);
    }

    @Override
    public String getName() {
        return config.getName();
    }

    @Override
    public String getHost() {
        return config.getHost();
    }

    @Override
    public int getPort() {
        return config.getPort();
    }

    @Override
    public String getPathSeparator() {
        return config.getPathSeparator();
    }

    @Override
    public void connect(final ProtonClientOptions options,
            final Handler<AsyncResult<ProtonConnection>> closeHandler,
            final Handler<ProtonConnection> disconnectHandler,
            final Handler<AsyncResult<ProtonConnection>> connectionResultHandler) {
        connect(options, null, null, closeHandler, disconnectHandler, connectionResultHandler);
    }

    @Override
    public void connect(final ProtonClientOptions options, final String username, final String password,
            final Handler<AsyncResult<ProtonConnection>> closeHandler,
            final Handler<ProtonConnection> disconnectHandler,
            final Handler<AsyncResult<ProtonConnection>> connectionResultHandler) {

        if (vertx == null) {
            throw new IllegalStateException("Vert.x instance must be set");
        } else if (config == null) {
            throw new IllegalStateException("Client configuration must be set");
        }

        Objects.requireNonNull(connectionResultHandler);
        final ProtonClientOptions clientOptions = options != null ? options : new ProtonClientOptions();
        final String effectiveUsername = username == null ? config.getUsername() : username;
        final String effectivePassword = password == null ? config.getPassword() : password;
        addOptions(clientOptions, effectiveUsername, effectivePassword);

        final ProtonClient client = ProtonClient.create(vertx);
        logger.debug("connecting to AMQP 1.0 container [{}://{}:{}]", clientOptions.isSsl() ? "amqps" : "amqp",
                config.getHost(), config.getPort());
        client.connect(clientOptions, config.getHost(), config.getPort(), effectiveUsername, effectivePassword,
                conAttempt -> handleConnectionAttemptResult(conAttempt, clientOptions, closeHandler,
                        disconnectHandler, connectionResultHandler));
    }

    private void handleConnectionAttemptResult(final AsyncResult<ProtonConnection> conAttempt,
            final ProtonClientOptions clientOptions, final Handler<AsyncResult<ProtonConnection>> closeHandler,
            final Handler<ProtonConnection> disconnectHandler,
            final Handler<AsyncResult<ProtonConnection>> connectionResultHandler) {

        if (conAttempt.failed()) {

            logger.debug("can't connect to AMQP 1.0 container [{}://{}:{}]: {}",
                    clientOptions.isSsl() ? "amqps" : "amqp", config.getHost(), config.getPort(),
                    conAttempt.cause().getMessage());
            connectionResultHandler.handle(Future.failedFuture(conAttempt.cause()));

        } else {

            // at this point the SASL exchange has completed successfully
            logger.debug("connected to AMQP 1.0 container [{}://{}:{}], opening connection ...",
                    clientOptions.isSsl() ? "amqps" : "amqp", config.getHost(), config.getPort());
            ProtonConnection downstreamConnection = conAttempt.result();
            downstreamConnection.setContainer(String.format("%s-%s", config.getName(), UUID.randomUUID()))
                    .setHostname(config.getAmqpHostname()).openHandler(openCon -> {
                        if (openCon.succeeded()) {
                            logger.debug("connection to container [{}] at [{}://{}:{}] open",
                                    downstreamConnection.getRemoteContainer(),
                                    clientOptions.isSsl() ? "amqps" : "amqp", config.getHost(), config.getPort());
                            downstreamConnection.disconnectHandler(disconnectHandler);
                            downstreamConnection.closeHandler(closeHandler);
                            connectionResultHandler.handle(Future.succeededFuture(downstreamConnection));
                        } else {
                            logger.warn("can't open connection to container [{}] at [{}://{}:{}]",
                                    downstreamConnection.getRemoteContainer(),
                                    clientOptions.isSsl() ? "amqps" : "amqp", config.getHost(), config.getPort(),
                                    openCon.cause());
                            connectionResultHandler.handle(Future.failedFuture(openCon.cause()));
                        }
                    }).open();
        }
    }

    private void addOptions(final ProtonClientOptions clientOptions, final String username, final String password) {

        addTlsTrustOptions(clientOptions);
        if (username != null && password != null) {
            clientOptions.addEnabledSaslMechanism(ProtonSaslPlainImpl.MECH_NAME);
        } else {
            addTlsKeyCertOptions(clientOptions);
        }
    }

    private void addTlsTrustOptions(final ProtonClientOptions clientOptions) {

        if (clientOptions.getTrustOptions() == null) {
            TrustOptions trustOptions = config.getTrustOptions();
            if (trustOptions != null) {
                clientOptions.setSsl(true).setTrustOptions(trustOptions);
                if (config.isHostnameVerificationRequired()) {
                    clientOptions.setHostnameVerificationAlgorithm("HTTPS");
                } else {
                    clientOptions.setHostnameVerificationAlgorithm("");
                }
            }
        }
    }

    private void addTlsKeyCertOptions(final ProtonClientOptions clientOptions) {

        if (clientOptions.getKeyCertOptions() == null) {
            KeyCertOptions keyCertOptions = config.getKeyCertOptions();
            if (keyCertOptions != null) {
                clientOptions.setSsl(true).setKeyCertOptions(keyCertOptions);
                clientOptions.addEnabledSaslMechanism(ProtonSaslExternalImpl.MECH_NAME);
            }
        }
    }

    /**
     * Builder for ConnectionFactory instances.
     */
    public static final class ConnectionFactoryBuilder {

        private Vertx vertx;
        private final ClientConfigProperties properties;

        private ConnectionFactoryBuilder(final ClientConfigProperties properties) {
            this.properties = properties;
        }

        /**
         * Creates a new builder using default configuration values.
         * 
         * @return The builder.
         */
        public static ConnectionFactoryBuilder newBuilder() {
            return new ConnectionFactoryBuilder(new ClientConfigProperties());
        }

        /**
         * Creates a new builder using given configuration values.
         * 
         * @param config The configuration to use.
         * @return The builder.
         */
        public static ConnectionFactoryBuilder newBuilder(final ClientConfigProperties config) {
            return new ConnectionFactoryBuilder(config);
        }

        /**
         * Sets the name the client should use as its container name when connecting to the
         * server.
         * 
         * @param name The client's container name.
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder name(final String name) {
            this.properties.setName(name);
            return this;
        }

        /**
         * Sets the vert.x instance to use for running the AMQP protocol.
         * 
         * @param vertx The vert.x instance to use. A new instance will be created if not set or {@code null}.
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder vertx(final Vertx vertx) {
            this.vertx = vertx;
            return this;
        }

        /**
         * @param host the Hono host
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder host(final String host) {
            this.properties.setHost(host);
            return this;
        }

        /**
         * @param port the Hono port
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder port(final int port) {
            this.properties.setPort(port);
            return this;
        }

        /**
         * @param user username used to authenticate
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder user(final String user) {
            this.properties.setUsername(user);
            return this;
        }

        /**
         * @param password the secret used to authenticate
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder password(final String password) {
            this.properties.setPassword(password);
            return this;
        }

        /**
         * @param pathSeparator the character to use to separate the segments of message addresses.
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder pathSeparator(final String pathSeparator) {
            this.properties.setPathSeparator(pathSeparator);
            return this;
        }

        /**
         * Sets the path to the PKCS12 key store to load certificates of trusted CAs from.
         * 
         * @param path The absolute path to the key store.
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder trustStorePath(final String path) {
            this.properties.setTrustStorePath(path);
            return this;
        }

        /**
         * Sets the password for accessing the PKCS12 key store containing the certificates
         * of trusted CAs.
         * 
         * @param password The password to set.
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder trustStorePassword(final String password) {
            this.properties.setTrustStorePassword(password);
            return this;
        }

        /**
         * Disables matching of the <em>host</em> property against the distinguished name
         * asserted by the server's certificate when connecting using TLS.
         * <p>
         * Verification is enabled by default.
         * 
         * @return This builder for command chaining.
         */
        public ConnectionFactoryBuilder disableHostnameVerification() {
            this.properties.setHostnameVerificationRequired(false);
            return this;
        }

        /**
         * Creates a new client based on this builder's configuration properties.
         * 
         * @return The client instance.
         * @throws IllegalStateException if any of the mandatory properties are not set.
         */
        public ConnectionFactory build() {
            if (vertx == null) {
                vertx = Vertx.vertx();
            }
            return new ConnectionFactoryImpl(vertx, properties);
        }
    }
}