/** * JBoss, Home of Professional Open Source * Copyright Red Hat, Inc., and individual contributors. * * Licensed 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.jboss.aerogear.unifiedpush.message.util; import org.jboss.aerogear.unifiedpush.message.exception.MessageDeliveryException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Resource; import javax.ejb.Stateless; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.jms.IllegalStateException; import javax.jms.JMSException; import javax.jms.MessageConsumer; import javax.jms.MessageProducer; import javax.jms.ObjectMessage; import javax.jms.Session; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; /** * Utility class for sending and receiving JMS messages */ @Stateless public class JmsClient { private static final Logger logger = LoggerFactory.getLogger(JmsClient.class); @Resource(mappedName = "java:/ConnectionFactory") private ConnectionFactory connectionFactory; @Resource(mappedName = "java:/JmsXA") private ConnectionFactory xaConnectionFactory; /** * Creates {@link JmsSender} utility that allows to specify how should be message sent and into which destination * * @param message the message to be send out * @return the new sender object */ public JmsSender send(Serializable message) { return new JmsSender(message); } /** * Creates {@link JmsReceiver} utility that allows to specify how should be message received and from which destination * * @return the new receiver object */ public JmsReceiver receive() { return new JmsReceiver(); } /** * Utility that allows to specify how should be message sent and into which destination */ public class JmsReceiver { private boolean transacted; private String selector; private Wait wait = new WaitIndefinitely(); private int acknowledgeMode = Session.AUTO_ACKNOWLEDGE; private boolean autoClose = true; private Connection connection; public JmsReceiver() { } /** * Receives the message in transaction (i.e. use JmsXA connection factory). * * @return this receiver object */ public JmsReceiver inTransaction() { this.transacted = true; return this; } /** * Specifies a selector used to query messages from a destination. * The selector can be formatted with arguments as in {@link String#format(String, Object...)}. * * @param selector specifies to query messages from a destination. * @param args argument for the selector * @return this receiver object * * @see String#format(String, Object...) */ public JmsReceiver withSelector(String selector, Object... args) { this.selector = String.format(selector, args); return this; } /** * Don't block and returns the message what is in the queue, if there is none queued, then returns null immediately. * * @return this receiver object */ public JmsReceiver noWait() { this.wait = new NoWait(); return this; } /** * Waits specific number of milliseconds for a message to eventually appear in the queue, or returns null if there was no message queued in given interval. * * @param timeout wait until * @return this receiver object */ public JmsReceiver withTimeout(long timeout) { this.wait = new WaitSpecificTime(timeout); return this; } /** * Sets the message acknowledgement mode. * * @param acknowledgeMode JMS acknowledge mode as in {@link Session} * @return this receiver object */ public JmsReceiver withAcknowledgeMode(int acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; return this; } /** * Won't close the connection automatically upon completion, allowing to reuse given connection. * * @return this receiver object */ public JmsReceiver noAutoClose() { this.autoClose = false; return this; } /** * Closes the connection. */ public void close() { try { connection.close(); } catch (JMSException e) { throw new java.lang.IllegalStateException(e); } } /** * Receives message from the given destination. * * @param destination where to receive from * @return dequeued {@link ObjectMessage} */ public ObjectMessage from(Destination destination) { try { if (transacted) { connection = xaConnectionFactory.createConnection(); } else { connection = connectionFactory.createConnection(); } Session session = connection.createSession(transacted, acknowledgeMode); MessageConsumer messageConsumer; if (selector != null) { messageConsumer = session.createConsumer(destination, selector); } else { messageConsumer = session.createConsumer(destination); } connection.start(); ObjectMessage objectMessage; if (wait instanceof WaitIndefinitely) { objectMessage = (ObjectMessage) messageConsumer.receive(); } else if (wait instanceof NoWait) { objectMessage = (ObjectMessage) messageConsumer.receiveNoWait(); } else if (wait instanceof WaitSpecificTime) { objectMessage = (ObjectMessage) messageConsumer.receive(((WaitSpecificTime) wait).getTime()); } else { throw new IllegalStateException("Unknown wait: " + wait.getClass()); } return objectMessage; } catch (JMSException e) { throw new MessageDeliveryException("Failed to queue push message for further processing", e); } finally { if (connection != null && autoClose) { try { connection.close(); } catch (JMSException e) { logger.error("Failed to close JMS connection: ", e); } } } } } /** * Utility that allows to specify how should be message received and from which destination */ public class JmsSender { private Serializable message; private boolean transacted; private Map<String, Object> properties = new LinkedHashMap<>(); private int autoAcknowledgeMode = Session.AUTO_ACKNOWLEDGE; public JmsSender(Serializable message) { this.message = message; } /** * Send the message in transaction (i.e. use JmsXA connection factory). * * @return this sender object */ public JmsSender inTransaction() { this.transacted = true; return this; } /** * Sets the property that can be later used to query message by selector. * * @param name of property * @param value of property * @return this sender object */ public JmsSender withProperty(String name, String value) { if (value == null) { throw new NullPointerException("property value"); } this.properties.put(name, value); return this; } /** * Sets the property that can be later used to query message by selector. * * @param name of property * @param value of property * @return this sender object */ public JmsSender withProperty(String name, Long value) { if (value == null) { throw new NullPointerException("property value"); } this.properties.put(name, value); return this; } /** * The message sent with given ID will be delivered exactly once. * * Any other try to sent another message with exactly same ID won't result into queing the message, * no matter what payload the another message has. * * @param duplicateDetectionId protection of ID for duplicate msgs * @return this sender object */ public JmsSender withDuplicateDetectionId(String duplicateDetectionId) { if (duplicateDetectionId == null) { throw new NullPointerException("duplicateDetectionId"); } this.properties.put("_HQ_DUPL_ID", duplicateDetectionId); this.properties.put("_AMQ_DUPL_ID", duplicateDetectionId); return this; } /** * The message sent with given ID will be scheduled to be delivered after specified number of miliseconds. * * @param delayMs the delay in milliseconds * @return this sender object */ public JmsSender withDelayedDelivery(Long delayMs) { if (delayMs == null) { throw new NullPointerException("delayMs"); } this.properties.put("_HQ_SCHED_DELIVERY", new Long(System.currentTimeMillis() + delayMs)); this.properties.put("_AMQ_SCHED_DELIVERY", new Long(System.currentTimeMillis() + delayMs)); return this; } /** * Sends the message to the destination. * * @param destination where to send */ public void to(Destination destination) { Connection connection = null; try { if (transacted) { connection = xaConnectionFactory.createConnection(); } else { connection = connectionFactory.createConnection(); } Session session = connection.createSession(transacted, autoAcknowledgeMode); MessageProducer messageProducer = session.createProducer(destination); connection.start(); ObjectMessage objectMessage = session.createObjectMessage(message); for (Entry<String, Object> property : properties.entrySet()) { final Object value = property.getValue(); if (value instanceof String) { objectMessage.setStringProperty(property.getKey(), (String) value); } else if (value instanceof Long) { objectMessage.setLongProperty(property.getKey(), (Long) value); } } messageProducer.send(objectMessage); } catch (JMSException e) { throw new MessageDeliveryException("Failed to queue push message for further processing", e); } finally { if (connection != null) { try { connection.close(); } catch (JMSException e) { logger.error("Failed to close JMS connection: ", e); } } } } } private interface Wait { } private static class WaitIndefinitely implements Wait { } private static class NoWait implements Wait { } private static class WaitSpecificTime implements Wait { private long time; WaitSpecificTime(long time) { super(); this.time = time; } public long getTime() { return time; } } }