All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rabbitmq.jms.client.RMQMessageProducer Maven / Gradle / Ivy

The newest version!
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2013-2023 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
package com.rabbitmq.jms.client;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.jms.admin.RMQDestination;
import com.rabbitmq.jms.client.message.RMQTextMessage;
import com.rabbitmq.jms.util.RMQJMSException;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.jms.CompletionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.jms.Destination;
import jakarta.jms.InvalidDestinationException;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageProducer;
import jakarta.jms.Queue;
import jakarta.jms.QueueSender;
import jakarta.jms.Topic;
import jakarta.jms.TopicPublisher;
import java.io.IOException;
import java.util.function.BiFunction;

import static com.rabbitmq.jms.client.RMQMessage.*;

/**
 *
 */
public class RMQMessageProducer implements MessageProducer, QueueSender, TopicPublisher {

    private final Logger logger = LoggerFactory.getLogger(RMQMessageProducer.class);

    private static final String DIRECT_REPLY_TO = "amq.rabbitmq.reply-to";

    static final CompletionListener NO_OP_COMPLETION_LISTENER = new CompletionListener() {
        @Override
        public void onCompletion(Message message) {

        }

        @Override
        public void onException(Message message, Exception exception) {

        }
    };

    /**
     * The destination that we send our message to
     */
    private final RMQDestination destination;
    /**
     * The session this producer was created by
     */
    private final RMQSession session;
    /**
     * The default delivery mode used when a message is sent.
     * @see jakarta.jms.DeliveryMode
     */
    private int deliveryMode = Message.DEFAULT_DELIVERY_MODE;
    /**
     * Should we use message IDs or not.
     * In this implementation, this flag is ignored and we will
     * always use message IDs
     */
    private boolean disableMessageID = false;
    /**
     * Should we disable timestamps
     * In this implementation, this flag is ignored and we will
     * always use message timestamps
     */
    private boolean disableMessageTimestamp = false;
    /**
     * The default priority for a message
     */
    private int priority = Message.DEFAULT_PRIORITY;
    /**
     * The default TTL value is 0 (zero), meaning indefinite (no time-out).
     */
    private long ttl = Message.DEFAULT_TIME_TO_LIVE;

    private long deliveryDelay = Message.DEFAULT_DELIVERY_DELAY;

    private final SendingStrategy sendingStrategy;

    private final BiFunction amqpPropertiesCustomiser;

    private final SendingContextConsumer sendingContextConsumer;

    private final BeforePublishingCallback beforePublishingCallback;

    private final boolean keepTextMessageType;

    private final AtomicBoolean publishConfirmedEnabled = new AtomicBoolean(false);

    RMQMessageProducer(RMQSession session, RMQDestination destination, boolean preferProducerMessageProperty,
                              BiFunction amqpPropertiesCustomiser,
                              SendingContextConsumer sendingContextConsumer,
                              PublishingListener publishingListener,
                              boolean keepTextMessageType) {
        this.session = session;
        this.destination = destination;
        if (preferProducerMessageProperty) {
            sendingStrategy = new PreferMessageProducerPropertySendingStrategy();
        } else {
            sendingStrategy = new PreferMessagePropertySendingStrategy();
        }
        this.amqpPropertiesCustomiser = amqpPropertiesCustomiser == null ? (builder, message) -> builder : amqpPropertiesCustomiser;
        this.sendingContextConsumer = sendingContextConsumer == null ? ctx -> {} : sendingContextConsumer;
        if (publishingListener == null) {
            this.beforePublishingCallback = (message, completionListener, channel) -> {};
        } else {
            this.beforePublishingCallback = (message, completionListener, channel) ->
                publishingListener.publish(message, completionListener, channel.getNextPublishSeqNo());
        }
        this.keepTextMessageType = keepTextMessageType;
    }

    public RMQMessageProducer(RMQSession session, RMQDestination destination, boolean preferProducerMessageProperty,
            BiFunction amqpPropertiesCustomiser,
            SendingContextConsumer sendingContextConsumer) {
        this(session, destination, preferProducerMessageProperty, amqpPropertiesCustomiser, sendingContextConsumer, null,
            false);
    }

    public RMQMessageProducer(RMQSession session, RMQDestination destination, boolean preferProducerMessageProperty,
        BiFunction amqpPropertiesCustomiser) {
        this(session, destination, preferProducerMessageProperty, amqpPropertiesCustomiser, ctx -> {});
    }

    /**
     * Create a producer of messages.
     * @param session which this producer uses
     * @param destination to which this producer sends messages.
     * @param preferProducerMessageProperty properties take precedence over respective message properties
     */
    public RMQMessageProducer(RMQSession session, RMQDestination destination, boolean preferProducerMessageProperty) {
        this(session, destination, preferProducerMessageProperty, (builder, message) -> builder);
    }

    /**
     * Create a producer of messages.
     * @param session which this producer uses
     * @param destination to which this producer sends messages.
     */
    public RMQMessageProducer(RMQSession session, RMQDestination destination) {
        this(session, destination, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDisableMessageID(boolean value) throws JMSException {
        this.disableMessageID = value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getDisableMessageID() throws JMSException {
        return this.disableMessageID;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDisableMessageTimestamp(boolean value) throws JMSException {
        this.disableMessageTimestamp = value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getDisableMessageTimestamp() throws JMSException {
        return this.disableMessageTimestamp;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDeliveryMode(int deliveryMode) throws JMSException {
        this.deliveryMode = deliveryMode;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getDeliveryMode() throws JMSException {
        return this.deliveryMode;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setPriority(int defaultPriority) throws JMSException {
        this.priority = defaultPriority;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getPriority() throws JMSException {
        return this.priority;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setTimeToLive(long timeToLive) throws JMSException {
        this.ttl = timeToLive;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getTimeToLive() throws JMSException {
        return this.ttl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Destination getDestination() throws JMSException {
        return this.destination;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws JMSException {
        logger.trace("close producer for destination '{}'", this.destination);
        this.session.removeProducer(this);
    }

    /**
     * Method called internally or by the Session
     * when system is shutting down
     */
    protected void internalClose() {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void send(Message message) throws JMSException {
        this.sendingStrategy.send(this.destination, message, NO_OP_COMPLETION_LISTENER);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void send(Message message, int deliveryMode, int priority, long timeToLive) throws JMSException {
        this.sendingStrategy.send(this.destination, message, NO_OP_COMPLETION_LISTENER,
            deliveryMode, priority, timeToLive);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void send(Destination destination, Message message) throws JMSException {
        this.checkUnidentifiedMessageProducer(destination);
        this.sendingStrategy.send(destination, message, NO_OP_COMPLETION_LISTENER);
    }

    private void checkUnidentifiedMessageProducer(Destination destination) {
        if (destination != null && this.destination != null)
            throw new UnsupportedOperationException("Must not supply a destination unless MessageProducer is unidentified.");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException {
        this.checkUnidentifiedMessageProducer(destination);
        this.sendingStrategy.send(destination, message, NO_OP_COMPLETION_LISTENER,
            deliveryMode, priority, timeToLive);
    }

    private void internalSend(RMQDestination destination, Message message, CompletionListener completionListener,
                              int deliveryMode, int priority, long timeToLiveOrExpiration,
                              MessageExpirationType messageExpirationType,
                              DeliveryTimeSource deliveryTimeSource) throws JMSException {
        logger.trace("send/publish message({}) to destination({}) with properties deliveryMode({}), priority({}), timeToLive({}), deliveryTimeSource({})",
                message, destination, deliveryMode, priority, timeToLiveOrExpiration, deliveryTimeSource);

        this.sendingContextConsumer.accept(new SendingContext(destination, message));

        if (destination == null)
            destination = this.destination;
        if (destination == null)
            throw new InvalidDestinationException("No destination supplied, or implied.");
        if (deliveryMode != jakarta.jms.DeliveryMode.PERSISTENT)
            deliveryMode = jakarta.jms.DeliveryMode.NON_PERSISTENT;

        /* Normalise message to internal form */
        RMQMessage rmqMessage = RMQMessage.normalise(message);

        /* Set known JMS message properties that need to be set during this call */
        long currentTime = System.currentTimeMillis();
        long expiration;
        long ttl;
        if (messageExpirationType == MessageExpirationType.TTL) {
            expiration = timeToLiveOrExpiration == 0L ? 0L : currentTime + timeToLiveOrExpiration;
            ttl = timeToLiveOrExpiration;
        } else {
            expiration = timeToLiveOrExpiration;
            ttl = timeToLiveOrExpiration - currentTime;
        }

        rmqMessage.setJMSDeliveryMode(deliveryMode);
        rmqMessage.setJMSPriority(priority);
        rmqMessage.setJMSExpiration(expiration);
        rmqMessage.setJMSDestination(destination);
        rmqMessage.setJMSTimestamp(currentTime);
        rmqMessage.generateInternalID();
        long deliveryDelay = getDeliveryDelayAndSetJMSDeliveryTimeIfNeeded(rmqMessage, deliveryTimeSource);

        /* Now send it */
        if (destination.isAmqp()) {
            sendAMQPMessage(destination, rmqMessage, message, completionListener,
                deliveryMode, priority, ttl, deliveryDelay);
        } else {
            sendJMSMessage(destination, rmqMessage, message, completionListener,
                deliveryMode, priority, ttl, deliveryDelay);
        }
    }
    private long getDeliveryDelayAndSetJMSDeliveryTimeIfNeeded(RMQMessage rmqMessage, DeliveryTimeSource deliveryTimeSource) throws JMSException {
        long deliveryDelay = 0L;
        long currentTime = System.currentTimeMillis();
        if (DeliveryTimeSource.MESSAGE.equals(deliveryTimeSource) || getDeliveryDelay() <= 0L) {
            deliveryDelay = rmqMessage.getJMSDeliveryTime() - currentTime;
        }else if (getDeliveryDelay() > 0L) {
            deliveryDelay = getDeliveryDelay();
            rmqMessage.setJMSDeliveryTime(currentTime + getDeliveryDelay());
        }
        return deliveryDelay;
    }

    private void sendAMQPMessage(RMQDestination destination, RMQMessage msg, Message originalMessage,
                                 CompletionListener completionListener, int deliveryMode,
                                 int priority, long timeToLive, long deliveryDelay) throws JMSException {
        if (!destination.isAmqpWritable()) {
            this.logger.error("Cannot write to AMQP destination {}", destination);
            throw new RMQJMSException("Cannot write to AMQP destination", new UnsupportedOperationException("MessageProducer.send to undefined AMQP resource"));
        }

        if (msg.isAmqpWritable()) {
            try {
                AMQP.BasicProperties.Builder bob = new AMQP.BasicProperties.Builder();
                bob.contentType("application/octet-stream");
                bob.deliveryMode(RMQMessage.rmqDeliveryMode(deliveryMode));
                bob.priority(priority);
                bob.correlationId(msg.getJMSCorrelationID());
                bob.expiration(rmqExpiration(timeToLive));
                Map messageHeaders = msg.toAmqpHeaders();
                if (this.keepTextMessageType && msg instanceof RMQTextMessage) {
                    messageHeaders.put(RMQMessage.JMS_TYPE_HEADER,
                                       RMQMessage.TEXT_MESSAGE_HEADER_VALUE);
                }
                String targetAmqpExchangeName = session.delayMessage(destination, messageHeaders, deliveryDelay);
                bob.headers(messageHeaders);

                setReplyToProperty(bob, msg);

                bob = amqpPropertiesCustomiser.apply(bob, msg);

                byte[] data = msg.toAmqpByteArray();

                this.beforePublishingCallback.beforePublishing(originalMessage, completionListener, this.session.getChannel());
                this.session.getChannel().basicPublish(targetAmqpExchangeName, destination.getAmqpRoutingKey(), bob.build(), data);
            } catch (IOException x) {
                throw new RMQJMSException(x);
            }
        } else {
            this.logger.error("Unsupported message type {} for AMQP destination {}", msg.getClass().getName(), destination);
            throw new RMQJMSException("Unsupported message type for AMQP destination", new UnsupportedOperationException("MessageProducer.send to AMQP resource: Message not Text or Bytes"));
        }
    }

    // protected for testing
    protected void sendJMSMessage(RMQDestination destination, RMQMessage msg, Message originalMessage,
                                  CompletionListener completionListener,
                                  int deliveryMode, int priority, long timeToLive, long deliveryDelay) throws JMSException {
        this.session.declareDestinationIfNecessary(destination);
        try {
            AMQP.BasicProperties.Builder bob = new AMQP.BasicProperties.Builder();
            bob.contentType("application/octet-stream");
            bob.deliveryMode(RMQMessage.rmqDeliveryMode(deliveryMode));
            bob.priority(priority);
            bob.correlationId(msg.getJMSCorrelationID());
            bob.expiration(rmqExpiration(timeToLive));
            Map headers = msg.toHeaders();
            String targetAmqpExchangeName = session.delayMessage(destination, headers, deliveryDelay);
            bob.headers(headers);

            setReplyToProperty(bob, msg);

            byte[] data = msg.toByteArray();

            this.beforePublishingCallback.beforePublishing(originalMessage, completionListener,
                this.session.getChannel());
            this.session.getChannel().basicPublish(targetAmqpExchangeName, destination.getAmqpRoutingKey(), bob.build(), data);
        } catch (IOException x) {
            throw new RMQJMSException(x);
        }
    }

    /**
     * Set AMQP reply-to property to reply-to if necessary.
     * 

*

* Set the reply-to property to amq.rabbitmq.reply-to * if the JMSReplyTo header is set to a destination with that * name and does not have a routing key (which indicates this this is a forwarded * reply to destination). *

* Set the reply-to property to the amq routing key, if the routing key * if not null. *

* For outbound RPC request. * * @param builder * @param msg * @throws JMSException * @since 1.11.0 */ private static void setReplyToProperty(AMQP.BasicProperties.Builder builder, RMQMessage msg) throws JMSException { if (msg.getJMSReplyTo() != null && msg.getJMSReplyTo() instanceof RMQDestination) { RMQDestination replyTo = (RMQDestination) msg.getJMSReplyTo(); if (DIRECT_REPLY_TO.equals(replyTo.getDestinationName()) && replyTo.getAmqpRoutingKey() == null) { builder.replyTo(DIRECT_REPLY_TO); } else { builder.replyTo(replyTo.getAmqpRoutingKey()); } } } /** This is dictated by `erlang:send_after' on which rabbitmq depends to implement TTL: *
-define(MAX_EXPIRY_TIMER, 4294967295). */ private static final long MAX_TTL = 4294967295L; /** * Convert long time-to-live to String time-to-live for amqp protocol. * Constrain to limits: 0 <= ttl <= MAX_TTL. * @param ttl JMS time-to-live long integer * @return RabbitMQ message expiration setting (null if expiration==0L) */ private static final String rmqExpiration(long ttl) { if (ttl == 0L) return null; return String.valueOf( ttl < 0L ? 0L : ttl > MAX_TTL ? MAX_TTL : ttl ); } /** * {@inheritDoc} */ @Override public Queue getQueue() throws JMSException { return this.destination; } /** * {@inheritDoc} */ @Override public void send(Queue queue, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { this.sendingStrategy.send(queue, message, NO_OP_COMPLETION_LISTENER, deliveryMode, priority, timeToLive); } /** * {@inheritDoc} */ @Override public void send(Queue queue, Message message) throws JMSException { this.sendingStrategy.send(queue, message, NO_OP_COMPLETION_LISTENER); } /** * {@inheritDoc} */ @Override public Topic getTopic() throws JMSException { return this.destination; } /** * {@inheritDoc} */ @Override public void publish(Message message) throws JMSException { this.sendingStrategy.send(this.getTopic(), message, NO_OP_COMPLETION_LISTENER); } /** * {@inheritDoc} */ @Override public void publish(Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { this.sendingStrategy.send(this.getTopic(), message, NO_OP_COMPLETION_LISTENER, deliveryMode, priority, timeToLive); } /** * {@inheritDoc} */ @Override public void publish(Topic topic, Message message) throws JMSException { this.sendingStrategy.send(topic, message, NO_OP_COMPLETION_LISTENER); } /** * {@inheritDoc} */ @Override public void publish(Topic topic, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { this.sendingStrategy.send(topic, message, NO_OP_COMPLETION_LISTENER, deliveryMode, priority, timeToLive); } /** * Strategy interface for sending messages. */ private interface SendingStrategy { void send(Destination destination, Message message, CompletionListener completionListener) throws JMSException; void send(Destination destination, Message message, CompletionListener completionListener, int deliveryMode, int priority, long timeToLive) throws JMSException; } private enum DeliveryTimeSource { MESSAGE, /** the message carries the delivery time */ PRODUCER /** the producer is configured with a delivery delay */ } /** * This implementation ignores message properties (delivery mode, priority, and expiration) * in favor of the message producer's properties. */ private class PreferMessageProducerPropertySendingStrategy implements SendingStrategy { @Override public void send(Destination destination, Message message, CompletionListener completionListener) throws JMSException { internalSend((RMQDestination) destination, message, completionListener, getDeliveryMode(), getPriority(), getTimeToLive(), MessageExpirationType.TTL, DeliveryTimeSource.PRODUCER); } @Override public void send(Destination destination, Message message, CompletionListener completionListener, int deliveryMode, int priority, long timeToLive) throws JMSException { internalSend((RMQDestination) destination, message, completionListener, deliveryMode, priority, timeToLive, MessageExpirationType.TTL, DeliveryTimeSource.PRODUCER); } } /** * This implementation uses message properties (delivery mode, priority, and expiration) * if they've been set up. It falls back to the message producer's properties. */ private class PreferMessagePropertySendingStrategy implements SendingStrategy { @Override public void send(Destination destination, Message message, CompletionListener completionListener) throws JMSException { internalSend((RMQDestination) destination, message, completionListener, message.propertyExists(JMS_MESSAGE_DELIVERY_MODE) ? message.getJMSDeliveryMode() : getDeliveryMode(), message.propertyExists(JMS_MESSAGE_PRIORITY) ? message.getJMSPriority() : getPriority(), message.propertyExists(JMS_MESSAGE_EXPIRATION) ? message.getJMSExpiration() : getTimeToLive(), message.propertyExists(JMS_MESSAGE_EXPIRATION) ? MessageExpirationType.EXPIRATION : MessageExpirationType.TTL, message.propertyExists(JMS_MESSAGE_DELIVERY_TIME) ? DeliveryTimeSource.MESSAGE : DeliveryTimeSource.PRODUCER); } @Override public void send(Destination destination, Message message, CompletionListener completionListener, int deliveryMode, int priority, long timeToLive) throws JMSException { internalSend((RMQDestination) destination, message, completionListener, deliveryMode, priority, timeToLive, MessageExpirationType.TTL, DeliveryTimeSource.MESSAGE); } } private enum MessageExpirationType { TTL, EXPIRATION } interface BeforePublishingCallback { void beforePublishing(Message message, CompletionListener completionListener, Channel channel); } @Override public void setDeliveryDelay(long deliveryDelay) { this.deliveryDelay = deliveryDelay; } @Override public long getDeliveryDelay() { return deliveryDelay; } @Override public void send(Message message, CompletionListener completionListener) throws JMSException { checkCompletionListenerNotNull(completionListener); enablePublishConfirm(); this.sendingStrategy.send(this.destination, message, completionListener); } @Override public void send(Message message, int deliveryMode, int priority, long timeToLive, CompletionListener completionListener) throws JMSException { checkCompletionListenerNotNull(completionListener); enablePublishConfirm(); this.sendingStrategy.send(this.destination, message, completionListener, deliveryMode, priority, timeToLive); } @Override public void send(Destination destination, Message message, CompletionListener completionListener) throws JMSException { this.checkUnidentifiedMessageProducer(destination); checkCompletionListenerNotNull(completionListener); enablePublishConfirm(); this.sendingStrategy.send(destination, message, completionListener); } @Override public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, CompletionListener completionListener) throws JMSException { this.checkUnidentifiedMessageProducer(destination); checkCompletionListenerNotNull(completionListener); enablePublishConfirm(); this.sendingStrategy.send(destination, message, completionListener, deliveryMode, priority, timeToLive); } private static void checkCompletionListenerNotNull(CompletionListener completionListener) { if (completionListener == null) { throw new IllegalArgumentException("The completion listener cannot be null"); } } private void enablePublishConfirm() throws JMSException { if (this.publishConfirmedEnabled.compareAndSet(false, true)) { try { this.session.enablePublishConfirmOnChannel(); } catch (IOException e) { throw new RMQJMSException(e); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy