/* * 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.jmeter.protocol.jms.client; import java.io.Closeable; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Session; import javax.jms.Topic; import javax.naming.Context; import javax.naming.NamingException; import org.apache.jmeter.protocol.jms.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Generic MessageConsumer class, which has two possible strategies. * <ul> * <li>Use MessageConsumer.receive(timeout) to fetch messages.</li> * <li>Use MessageListener.onMessage() to cache messages in a local queue.</li> * </ul> * In both cases, the {@link #getMessage(long)} method is used to return the next message, * either directly using receive(timeout) or from the queue using poll(timeout). */ public class ReceiveSubscriber implements Closeable, MessageListener { private static final Logger log = LoggerFactory.getLogger(ReceiveSubscriber.class); private final Connection connection; private final Session session; private final MessageConsumer subscriber; /* * We use a LinkedBlockingQueue (rather than a ConcurrentLinkedQueue) because it has a * poll-with-wait method that avoids the need to use a polling loop. */ private final LinkedBlockingQueue<Message> queue; /** * No need for volatile as this variable is only accessed by a single thread */ private boolean connectionStarted; /** * Constructor takes the necessary JNDI related parameters to create a * connection and prepare to begin receiving messages. <br> * The caller must then invoke {@link #start()} to enable message reception. * * @param useProps * if <code>true</code>, use <em>jndi.properties</em> instead of * <code>initialContextFactory</code>, <code>providerUrl</code>, * <code>securityPrincipal</code>, * <code>securityCredentials</code> * @param initialContextFactory * name of the initial context factory (will be ignored if * <code>useProps</code> is <code>true</code>) * @param providerUrl * url of the provider (will be ignored if <code>useProps</code> * is <code>true</code>) * @param connfactory * name of the object factory to look up in context * @param destinationName * name of the destination * @param durableSubscriptionId * id for a durable subscription (if empty or <code>null</code> * no durable subscription will be done) * @param clientId * client id to use (may be empty or <code>null</code>) * @param jmsSelector * Message Selector * @param useAuth * flag whether auth should be used (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityPrincipal * name of the principal to use for auth (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityCredentials * credentials for the principal (will be ignored if * <code>useProps</code> is <code>true</code>) * @throws JMSException * if could not create context or other problem occurred. * @throws NamingException * when lookup of context or destination fails */ public ReceiveSubscriber(boolean useProps, String initialContextFactory, String providerUrl, String connfactory, String destinationName, String durableSubscriptionId, String clientId, String jmsSelector, boolean useAuth, String securityPrincipal, String securityCredentials) throws NamingException, JMSException { this(0, useProps, initialContextFactory, providerUrl, connfactory, destinationName, durableSubscriptionId, clientId, jmsSelector, useAuth, securityPrincipal, securityCredentials, false); } /** * Constructor takes the necessary JNDI related parameters to create a * connection and create an onMessageListener to prepare to begin receiving * messages. <br> * The caller must then invoke {@link #start()} to enable message reception. * * @param queueSize * maximum queue size, where a <code>queueSize</code> <=0 * means no limit * @param useProps * if <code>true</code>, use <em>jndi.properties</em> instead of * <code>initialContextFactory</code>, <code>providerUrl</code>, * <code>securityPrincipal</code>, * <code>securityCredentials</code> * @param initialContextFactory * name of the initial context factory (will be ignored if * <code>useProps</code> is <code>true</code>) * @param providerUrl * url of the provider (will be ignored if <code>useProps</code> * is <code>true</code>) * @param connfactory * name of the object factory to look up in context * @param destinationName * name of the destination * @param durableSubscriptionId * id for a durable subscription (if empty or <code>null</code> * no durable subscription will be done) * @param clientId * client id to use (may be empty or <code>null</code>) * @param jmsSelector * Message Selector * @param useAuth * flag whether auth should be used (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityPrincipal * name of the principal to use for auth (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityCredentials * credentials for the principal (will be ignored if * <code>useProps</code> is <code>true</code>) * @throws JMSException * if could not create context or other problem occurred. * @throws NamingException * when lookup of context or destination fails */ public ReceiveSubscriber(int queueSize, boolean useProps, String initialContextFactory, String providerUrl, String connfactory, String destinationName, String durableSubscriptionId, String clientId, String jmsSelector, boolean useAuth, String securityPrincipal, String securityCredentials) throws NamingException, JMSException { this(queueSize, useProps, initialContextFactory, providerUrl, connfactory, destinationName, durableSubscriptionId, clientId, jmsSelector, useAuth, securityPrincipal, securityCredentials, true); } /** * Constructor takes the necessary JNDI related parameters to create a * connection and create an onMessageListener to prepare to begin receiving * messages. <br/> * The caller must then invoke {@link #start()} to enable message reception. * * @param queueSize * maximum queue, where a queueSize <=0 means no limit * @param useProps * if <code>true</code>, use <em>jndi.properties</em> instead of * <code>initialContextFactory</code>, <code>providerUrl</code>, * <code>securityPrincipal</code>, * <code>securityCredentials</code> * @param initialContextFactory * name of the initial context factory (will be ignored if * <code>useProps</code> is <code>true</code>) * @param providerUrl * url of the provider (will be ignored if <code>useProps</code> * is <code>true</code>) * @param connfactory * name of the object factory to look up in context * @param destinationName * name of the destination * @param durableSubscriptionId * id for a durable subscription (if empty or <code>null</code> * no durable subscription will be done) * @param clientId * client id to use (may be empty or <code>null</code>) * @param jmsSelector * Message Selector * @param useAuth * flag whether auth should be used (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityPrincipal * name of the principal to use for auth (will be ignored if * <code>useProps</code> is <code>true</code>) * @param securityCredentials * credentials for the principal (will be ignored if * <code>useProps</code> is <code>true</code>) * @param useMessageListener * if <code>true</code> create an onMessageListener to prepare to * begin receiving messages, otherwise queue will be * <code>null</code> * @throws JMSException * if could not create context or other problem occurred. * @throws NamingException * when lookup of context or destination fails */ private ReceiveSubscriber(int queueSize, boolean useProps, String initialContextFactory, String providerUrl, String connfactory, String destinationName, String durableSubscriptionId, String clientId, String jmsSelector, boolean useAuth, String securityPrincipal, String securityCredentials, boolean useMessageListener) throws NamingException, JMSException { boolean initSuccess = false; try{ Context ctx = InitialContextFactory.getContext(useProps, initialContextFactory, providerUrl, useAuth, securityPrincipal, securityCredentials); connection = Utils.getConnection(ctx, connfactory); if(!isEmpty(clientId)) { connection.setClientID(clientId); } session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination dest = Utils.lookupDestination(ctx, destinationName); subscriber = createSubscriber(session, dest, durableSubscriptionId, jmsSelector); if(useMessageListener) { if (queueSize <=0) { queue = new LinkedBlockingQueue<>(); } else { queue = new LinkedBlockingQueue<>(queueSize); } subscriber.setMessageListener(this); } else { queue = null; } log.debug("<init> complete"); initSuccess = true; } finally { if(!initSuccess) { close(); } } } /** * Return a simple MessageConsumer or a TopicSubscriber (as a durable subscription) * @param session * JMS session * @param destination * JMS destination, can be either topic or queue * @param durableSubscriptionId * If neither empty nor null, this means that a durable * subscription will be used * @param jmsSelector JMS Selector * @return the message consumer * @throws JMSException */ private MessageConsumer createSubscriber(Session session, Destination destination, String durableSubscriptionId, String jmsSelector) throws JMSException { if (isEmpty(durableSubscriptionId)) { if(isEmpty(jmsSelector)) { return session.createConsumer(destination); } else { return session.createConsumer(destination, jmsSelector); } } else { if(isEmpty(jmsSelector)) { return session.createDurableSubscriber((Topic) destination, durableSubscriptionId); } else { return session.createDurableSubscriber((Topic) destination, durableSubscriptionId, jmsSelector, false); } } } /** * Calls Connection.start() to begin receiving inbound messages. * @throws JMSException when starting the context fails */ public void start() throws JMSException { log.debug("start()"); connection.start(); connectionStarted=true; } /** * Calls Connection.stop() to stop receiving inbound messages. * @throws JMSException when stopping the context fails */ public void stop() throws JMSException { log.debug("stop()"); connection.stop(); connectionStarted=false; } /** * Get the next message or <code>null</code>. * <p> * Never blocks for longer than the specified timeout. * * @param timeout in milliseconds * @return the next message or <code>null</code> * * @throws JMSException when receiving the message fails */ public Message getMessage(long timeout) throws JMSException { Message message = null; if (queue != null) { // Using onMessage Listener try { if (timeout < 10) { // Allow for short/negative times message = queue.poll(); } else { message = queue.poll(timeout, TimeUnit.MILLISECONDS); } } catch (InterruptedException e) { // Ignored Thread.currentThread().interrupt(); } return message; } if (timeout < 10) { // Allow for short/negative times message = subscriber.receiveNoWait(); } else { message = subscriber.receive(timeout); } return message; } /** * close() will stop the connection first. * Then it closes the subscriber, session and connection. */ @Override public void close() { // called by SubscriberSampler#threadFinished() log.debug("close()"); try { if(connection != null && connectionStarted) { connection.stop(); connectionStarted = false; } } catch (JMSException e) { log.warn("Stopping connection throws exception, message: {}", e.getMessage(), e); } Utils.close(subscriber, log); Utils.close(session, log); Utils.close(connection, log); } /** * {@inheritDoc} */ @Override public void onMessage(Message message) { if (!queue.offer(message)){ log.warn("Could not add message to queue"); } } /** * Checks whether string is empty * * @param s1 * @return True if input is null, an empty string, or a white space-only string */ private boolean isEmpty(String s1) { return s1 == null || s1.trim().isEmpty(); } }