/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.nifi.amqp.processors; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.net.ssl.SSLContext; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.lifecycle.OnStopped; import org.apache.nifi.authentication.exception.ProviderCreationException; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.ssl.SSLContextService; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * Base processor that uses RabbitMQ client API * (https://www.rabbitmq.com/api-guide.html) to rendezvous with AMQP-based * messaging systems version 0.9.1 * * @param <T> the type of {@link AMQPWorker}. Please see {@link AMQPPublisher} * and {@link AMQPConsumer} */ abstract class AbstractAMQPProcessor<T extends AMQPWorker> extends AbstractProcessor { public static final PropertyDescriptor HOST = new PropertyDescriptor.Builder() .name("Host Name") .description("Network address of AMQP broker (e.g., localhost)") .required(true) .defaultValue("localhost") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor PORT = new PropertyDescriptor.Builder() .name("Port") .description("Numeric value identifying Port of AMQP broker (e.g., 5671)") .required(true) .defaultValue("5672") .addValidator(StandardValidators.PORT_VALIDATOR) .build(); public static final PropertyDescriptor V_HOST = new PropertyDescriptor.Builder() .name("Virtual Host") .description("Virtual Host name which segregates AMQP system for enhanced security.") .required(false) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor USER = new PropertyDescriptor.Builder() .name("User Name") .description("User Name used for authentication and authorization.") .required(true) .defaultValue("guest") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder() .name("Password") .description("Password used for authentication and authorization.") .required(true) .defaultValue("guest") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor AMQP_VERSION = new PropertyDescriptor.Builder() .name("AMQP Version") .description("AMQP Version. Currently only supports AMQP v0.9.1.") .required(true) .allowableValues("0.9.1") .defaultValue("0.9.1") .build(); public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() .name("ssl-context-service") .displayName("SSL Context Service") .description("The SSL Context Service used to provide client certificate information for TLS/SSL connections.") .required(false) .identifiesControllerService(SSLContextService.class) .build(); public static final PropertyDescriptor CLIENT_AUTH = new PropertyDescriptor.Builder() .name("ssl-client-auth") .displayName("Client Auth") .description("Client authentication policy when connecting to secure (TLS/SSL) AMQP broker. " + "Possible values are REQUIRED, WANT, NONE. This property is only used when an SSL Context " + "has been defined and enabled.") .required(false) .allowableValues(SSLContextService.ClientAuth.values()) .defaultValue("REQUIRED") .build(); static List<PropertyDescriptor> descriptors = new ArrayList<>(); /* * Will ensure that list of PropertyDescriptors is build only once, since * all other lifecycle methods are invoked multiple times. */ static { descriptors.add(HOST); descriptors.add(PORT); descriptors.add(V_HOST); descriptors.add(USER); descriptors.add(PASSWORD); descriptors.add(AMQP_VERSION); descriptors.add(SSL_CONTEXT_SERVICE); descriptors.add(CLIENT_AUTH); } protected volatile Connection amqpConnection; protected volatile T targetResource; /** * Will builds target resource ({@link AMQPPublisher} or * {@link AMQPConsumer}) upon first invocation and will delegate to the * implementation of * {@link #rendezvousWithAmqp(ProcessContext, ProcessSession)} method for * further processing. */ @Override public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { synchronized (this) { this.buildTargetResource(context); } this.rendezvousWithAmqp(context, session); } /** * Will close current AMQP connection. */ @OnStopped public void close() { try { if (this.targetResource != null) { this.targetResource.close(); } } catch (Exception e) { this.getLogger().warn("Failure while closing target resource " + this.targetResource, e); } try { if (this.amqpConnection != null) { this.amqpConnection.close(); } } catch (IOException e) { this.getLogger().warn("Failure while closing connection", e); } this.amqpConnection = null; } /** * Delegate method to supplement * {@link #onTrigger(ProcessContext, ProcessSession)}. It is implemented by * sub-classes to perform {@link Processor} specific functionality. * * @param context * instance of {@link ProcessContext} * @param session * instance of {@link ProcessSession} */ protected abstract void rendezvousWithAmqp(ProcessContext context, ProcessSession session) throws ProcessException; /** * Delegate method to supplement building of target {@link AMQPWorker} (see * {@link AMQPPublisher} or {@link AMQPConsumer}) and is implemented by * sub-classes. * * @param context * instance of {@link ProcessContext} * @return new instance of {@link AMQPWorker} */ protected abstract T finishBuildingTargetResource(ProcessContext context); /** * Builds target resource ({@link AMQPPublisher} or {@link AMQPConsumer}). * It does so by making a {@link Connection} and then delegating to the * implementation of {@link #finishBuildingTargetResource(ProcessContext)} * which will build {@link AMQPWorker} (see {@link AMQPPublisher} or * {@link AMQPConsumer}). */ private void buildTargetResource(ProcessContext context) { if (this.amqpConnection == null || !this.amqpConnection.isOpen()) { this.amqpConnection = this.createConnection(context); this.targetResource = this.finishBuildingTargetResource(context); } } /** * Creates {@link Connection} to AMQP system. */ private Connection createConnection(ProcessContext context) { ConnectionFactory cf = new ConnectionFactory(); cf.setHost(context.getProperty(HOST).getValue()); cf.setPort(Integer.parseInt(context.getProperty(PORT).getValue())); cf.setUsername(context.getProperty(USER).getValue()); cf.setPassword(context.getProperty(PASSWORD).getValue()); String vHost = context.getProperty(V_HOST).getValue(); if (vHost != null) { cf.setVirtualHost(vHost); } // handles TLS/SSL aspects final SSLContextService sslService = context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); final String rawClientAuth = context.getProperty(CLIENT_AUTH).getValue(); final SSLContext sslContext; if (sslService != null) { final SSLContextService.ClientAuth clientAuth; if (StringUtils.isBlank(rawClientAuth)) { clientAuth = SSLContextService.ClientAuth.REQUIRED; } else { try { clientAuth = SSLContextService.ClientAuth.valueOf(rawClientAuth); } catch (final IllegalArgumentException iae) { throw new ProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", rawClientAuth, StringUtils.join(SslContextFactory.ClientAuth.values(), ", "))); } } sslContext = sslService.createSSLContext(clientAuth); } else { sslContext = null; } // check if the ssl context is set and add it to the factory if so if (sslContext != null) { cf.useSslProtocol(sslContext); } try { Connection connection = cf.newConnection(); return connection; } catch (Exception e) { throw new IllegalStateException("Failed to establish connection with AMQP Broker: " + cf.toString(), e); } } }