com.rabbitmq.jms.client.RMQMessage Maven / Gradle / Ivy
/* Copyright (c) 2013 Pivotal Software, Inc. All rights reserved. */
package com.rabbitmq.jms.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.jms.BytesMessage;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageFormatException;
import javax.jms.MessageNotWriteableException;
import javax.jms.ObjectMessage;
import javax.jms.StreamMessage;
import javax.jms.TextMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.rabbitmq.client.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.GetResponse;
import com.rabbitmq.jms.admin.RMQDestination;
import com.rabbitmq.jms.client.message.RMQBytesMessage;
import com.rabbitmq.jms.client.message.RMQMapMessage;
import com.rabbitmq.jms.client.message.RMQObjectMessage;
import com.rabbitmq.jms.client.message.RMQStreamMessage;
import com.rabbitmq.jms.client.message.RMQTextMessage;
import com.rabbitmq.jms.util.HexDisplay;
import com.rabbitmq.jms.util.IteratorEnum;
import com.rabbitmq.jms.util.RMQJMSException;
import com.rabbitmq.jms.util.Util;
/**
* Base class for RMQ*Message classes. This is abstract and cannot be instantiated independently.
*/
public abstract class RMQMessage implements Message, Cloneable {
/** Logger shared with derived classes */
protected final Logger logger = LoggerFactory.getLogger(RMQMessage.class);
protected void loggerDebugByteArray(String format, byte[] buffer, Object arg) {
if (logger.isDebugEnabled()) {
StringBuilder bufferOutput = new StringBuilder("Byte array, length ").append(buffer.length).append(" :\n");
HexDisplay.decodeByteArrayIntoStringBuilder(buffer, bufferOutput);
logger.debug(format, bufferOutput.append("end of byte array, length ").append(buffer.length).append('.'), arg);
}
}
/** Error message when the message is not readable */
protected static final String NOT_READABLE = "Message not readable";
/** Error message when the message is not writeable */
protected static final String NOT_WRITEABLE = "Message not writeable";
/** Error message used when throwing {@link javax.jms.MessageFormatException} */
protected static final String UNABLE_TO_CAST = "Unable to cast the object, %s, into the specified type %s";
/** Error message when we get an EOF exception */
protected static final String MSG_EOF = "Message EOF";
/** Properties in a JMS message can NOT be named any of these reserved words */
private static final String[] RESERVED_NAMES = {"NULL", "TRUE", "FALSE", "NOT", "AND", "OR", "BETWEEN", "LIKE", "IN",
"IS", "ESCAPE"};
/** A property in a JMS message must NOT have a name starting with any of the following characters (added period 2013-06-13) */
private static final char[] INVALID_STARTS_WITH = {'0','1','2','3','4','5','6','7','8','9','-','+','\'','"','.'};
/** A property in a JMS message must NOT contain these characters in its name (removed period 2013-06-13) */
private static final char[] MAY_NOT_CONTAIN = {'\'','"'};
/**
* When we create a message that has a byte[] as the underlying
* structure, BytesMessage and StreamMessage, this is the default size.
* Can be changed with a system property.
*/
protected static final int DEFAULT_MESSAGE_BODY_SIZE = Integer.getInteger("com.rabbitmq.jms.client.message.size", 512);
/**
* We store all the JMS hard coded values, such as {@link #setJMSMessageID(String)}, as properties instead of hard
* coded fields. This way we can create a structure later on that the rabbit MQ broker can read by just changing the
* {@link #toByteArray(RMQMessage)} and {@link #fromMessage(byte[])}.
*/
private static final String PREFIX = "rmq.";
private static final String JMS_MESSAGE_ID = PREFIX + "jms.message.id";
private static final String JMS_MESSAGE_TIMESTAMP = PREFIX + "jms.message.timestamp";
private static final String JMS_MESSAGE_CORR_ID = PREFIX + "jms.message.correlation.id";
private static final String JMS_MESSAGE_REPLY_TO = PREFIX + "jms.message.reply.to";
private static final String JMS_MESSAGE_DESTINATION = PREFIX + "jms.message.destination";
private static final String JMS_MESSAGE_DELIVERY_MODE = PREFIX + "jms.message.delivery.mode";
private static final String JMS_MESSAGE_REDELIVERED = PREFIX + "jms.message.redelivered";
private static final String JMS_MESSAGE_TYPE = PREFIX + "jms.message.type";
private static final String JMS_MESSAGE_EXPIRATION = PREFIX + "jms.message.expiration";
private static final String JMS_MESSAGE_PRIORITY = PREFIX + "jms.message.priority";
/**
* For turning {@link String}s into byte[]
and back we use this {@link Charset} instance.
* This is used for {@link RMQMessage#getJMSCorrelationIDAsBytes()}.
*/
private static final Charset CHARSET = Charset.forName("UTF-8");
/** Here we store the JMS_ properties that would have been fields */
private final Map rmqProperties = new HashMap();
/** Here we store the user’s custom JMS properties */
private final Map userJmsProperties = new HashMap();
/**
* We generate a unique message ID each time we send a message
* It is stored here. This is also used for
* {@link #hashCode()} and {@link #equals(Object)}
*/
private volatile String internalMessageID=null;
/**
* A message is read only if it has been received.
* We set this flag when we receive a message in the following method
* {@link RMQMessage#convertMessage(RMQSession, RMQDestination, com.rabbitmq.client.GetResponse)}
*/
private volatile boolean readonlyProperties=false;
private volatile boolean readonlyBody=false;
/**
* Returns true if this message body is read only
* This means that message has been received and can not
* be modified
* @return true if the message is read only
*/
public boolean isReadonlyBody() {
return this.readonlyBody;
}
public boolean isReadOnlyProperties() {
return this.readonlyProperties;
}
/**
* Sets the read only flag on this message
* @see RMQMessage#convertMessage(RMQSession, RMQDestination, com.rabbitmq.client.GetResponse)
* @param readonly
*/
protected void setReadonly(boolean readonly) {
this.readonlyBody = readonly;
this.readonlyProperties = readonly;
}
protected void setReadOnlyBody(boolean readonly) {
this.readonlyBody = readonly;
}
protected void setReadOnlyProperties(boolean readonly) {
this.readonlyProperties = readonly;
}
/**
* When a message is received, it has a delivery tag
* We use this delivery tag when we ack
* a single message
* @see RMQSession#acknowledgeMessages()
* @see RMQMessage#convertMessage(RMQSession, RMQDestination, com.rabbitmq.client.GetResponse)
*/
private long rabbitDeliveryTag = -1;
/**
* returns the delivery tag for this message
* @return
*/
public long getRabbitDeliveryTag() {
return this.rabbitDeliveryTag;
}
/**
* Sets the RabbitMQ delivery tag for this message.
* @see RMQMessage#convertMessage(RMQSession, RMQDestination, com.rabbitmq.client.GetResponse)
* @param rabbitDeliveryTag
*/
protected void setRabbitDeliveryTag(long rabbitDeliveryTag) {
this.rabbitDeliveryTag = rabbitDeliveryTag;
}
/**
* The Message must hold a reference to the session itself
* So that it can ack itself. Ack belongs to the session
* @see RMQSession#acknowledgeMessages()
*/
private volatile transient RMQSession session = null;
/**
* Returns the session this object belongs to
* @return
*/
public RMQSession getSession() {
return this.session;
}
/**
* Sets the session this object was received by
* @see RMQMessage#convertMessage(RMQSession, RMQDestination, com.rabbitmq.client.GetResponse)
* @see RMQSession#acknowledgeMessage(RMQMessage)
* @param session
*/
protected void setSession(RMQSession session) {
this.session = session;
}
/**
* Constructor for auto de-serialization
*/
public RMQMessage() {
}
/**
* {@inheritDoc}
*/
@Override
public String getJMSMessageID() throws JMSException {
return this.getStringProperty(JMS_MESSAGE_ID);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSMessageID(String id) throws JMSException {
this.setStringProperty(JMS_MESSAGE_ID, id);
}
/**
* {@inheritDoc}
*/
@Override
public long getJMSTimestamp() throws JMSException {
return this.getLongProperty(JMS_MESSAGE_TIMESTAMP);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSTimestamp(long timestamp) throws JMSException {
this.setLongProperty(JMS_MESSAGE_TIMESTAMP, timestamp);
}
/**
* {@inheritDoc}
*/
@Override
public byte[] getJMSCorrelationIDAsBytes() throws JMSException {
String id = this.getStringProperty(JMS_MESSAGE_CORR_ID);
if (id != null)
return id.getBytes(getCharset());
else
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSCorrelationIDAsBytes(byte[] correlationID) throws JMSException {
String id = correlationID != null ? new String(correlationID, getCharset()) : null;
this.setStringProperty(JMS_MESSAGE_CORR_ID, id);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSCorrelationID(String correlationID) throws JMSException {
this.setStringProperty(JMS_MESSAGE_CORR_ID, correlationID);
}
/**
* {@inheritDoc}
*/
@Override
public String getJMSCorrelationID() throws JMSException {
return this.getStringProperty(JMS_MESSAGE_CORR_ID);
}
/**
* {@inheritDoc}
*/
@Override
public Destination getJMSReplyTo() throws JMSException {
return (Destination) this.getObjectProperty(JMS_MESSAGE_REPLY_TO);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSReplyTo(Destination replyTo) throws JMSException {
this.setObjectProperty(JMS_MESSAGE_REPLY_TO, replyTo);
}
/**
* {@inheritDoc}
*/
@Override
public Destination getJMSDestination() throws JMSException {
return (Destination) this.getObjectProperty(JMS_MESSAGE_DESTINATION);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSDestination(Destination destination) throws JMSException {
this.setObjectProperty(JMS_MESSAGE_DESTINATION, destination);
}
/**
* {@inheritDoc}
*/
@Override
public int getJMSDeliveryMode() throws JMSException {
return this.getIntProperty(JMS_MESSAGE_DELIVERY_MODE);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSDeliveryMode(int deliveryMode) throws JMSException {
this.setIntProperty(JMS_MESSAGE_DELIVERY_MODE, deliveryMode);
}
/**
* {@inheritDoc}
*/
@Override
public boolean getJMSRedelivered() throws JMSException {
return this.getBooleanProperty(JMS_MESSAGE_REDELIVERED);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSRedelivered(boolean redelivered) throws JMSException {
this.setBooleanProperty(JMS_MESSAGE_REDELIVERED, redelivered);
}
/**
* {@inheritDoc}
*/
@Override
public String getJMSType() throws JMSException {
return this.getStringProperty(JMS_MESSAGE_TYPE);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSType(String type) throws JMSException {
this.setStringProperty(JMS_MESSAGE_TYPE, type);
}
/**
* {@inheritDoc}
*/
@Override
public long getJMSExpiration() throws JMSException {
return this.getLongProperty(JMS_MESSAGE_EXPIRATION);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSExpiration(long expiration) throws JMSException {
this.setLongProperty(JMS_MESSAGE_EXPIRATION, expiration);
}
/**
* {@inheritDoc}
*/
@Override
public int getJMSPriority() throws JMSException {
return this.getIntProperty(JMS_MESSAGE_PRIORITY);
}
/**
* {@inheritDoc}
*/
@Override
public void setJMSPriority(int priority) throws JMSException {
this.setIntProperty(JMS_MESSAGE_PRIORITY, priority);
}
/**
* {@inheritDoc}
*/
@Override
public final void clearProperties() throws JMSException {
this.userJmsProperties.clear();
this.setReadOnlyProperties(false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean propertyExists(String name) throws JMSException {
return this.userJmsProperties.containsKey(name) || this.rmqProperties.containsKey(name);
}
/**
* {@inheritDoc}
*/
@Override
public boolean getBooleanProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null) {
//default value for null is false
return false;
} else if (o instanceof String) {
return Boolean.parseBoolean((String) o);
} else if (o instanceof Boolean) {
Boolean b = (Boolean) o;
return b.booleanValue();
} else {
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
}
/**
* {@inheritDoc}
*/
@Override
public byte getByteProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid byte");
else if (o instanceof String) {
return Byte.parseByte((String) o);
} else if (o instanceof Byte) {
Byte b = (Byte) o;
return b.byteValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public short getShortProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid short");
else if (o instanceof String) {
return Short.parseShort((String) o);
} else if (o instanceof Byte) {
Byte b = (Byte) o;
return b.byteValue();
} else if (o instanceof Short) {
Short b = (Short) o;
return b.shortValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public int getIntProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid int");
else if (o instanceof String) {
return Integer.parseInt((String) o);
} else if (o instanceof Byte) {
Byte b = (Byte) o;
return b.byteValue();
} else if (o instanceof Short) {
Short b = (Short) o;
return b.shortValue();
} else if (o instanceof Integer) {
Integer b = (Integer) o;
return b.intValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public long getLongProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid long");
else if (o instanceof String) {
return Long.parseLong((String) o);
} else if (o instanceof Byte) {
Byte b = (Byte) o;
return b.byteValue();
} else if (o instanceof Short) {
Short b = (Short) o;
return b.shortValue();
} else if (o instanceof Integer) {
Integer b = (Integer) o;
return b.intValue();
} else if (o instanceof Long) {
Long b = (Long) o;
return b.longValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public float getFloatProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid float");
else if (o instanceof String) {
return Float.parseFloat((String) o);
} else if (o instanceof Float) {
Float b = (Float) o;
return b.floatValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public double getDoubleProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
throw new NumberFormatException("Null is not a valid double");
else if (o instanceof String) {
return Double.parseDouble((String) o);
} else if (o instanceof Float) {
Float b = (Float) o;
return b.floatValue();
} else if (o instanceof Double) {
Double b = (Double) o;
return b.doubleValue();
} else
throw new MessageFormatException(String.format("Unable to convert from class [%s]", o.getClass().getName()));
}
/**
* {@inheritDoc}
*/
@Override
public String getStringProperty(String name) throws JMSException {
Object o = this.getObjectProperty(name);
if (o == null)
return null;
else if (o instanceof String)
return (String) o;
else
return o.toString();
}
/**
* {@inheritDoc}
*/
@Override
public Object getObjectProperty(String name) throws JMSException {
if (name.startsWith(PREFIX))
return this.rmqProperties.get(name);
else
return this.userJmsProperties.get(name);
}
/**
* {@inheritDoc}
*/
@Override
public Enumeration> getPropertyNames() throws JMSException {
return new IteratorEnum(this.userJmsProperties.keySet().iterator());
}
/**
* {@inheritDoc}
*/
@Override
public void setBooleanProperty(String name, boolean value) throws JMSException {
this.setObjectProperty(name, Boolean.valueOf(value));
}
/**
* {@inheritDoc}
*/
@Override
public void setByteProperty(String name, byte value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setShortProperty(String name, short value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setIntProperty(String name, int value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setLongProperty(String name, long value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setFloatProperty(String name, float value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setDoubleProperty(String name, double value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void setStringProperty(String name, String value) throws JMSException {
this.setObjectProperty(name, value);
}
/**
* Validates that a JMS property name is correct
* @param name the name of the property
* @throws JMSException if the name contains invalid characters
* @throws IllegalArgumentException if the name parameter is null
*/
protected void checkName(String name) throws JMSException {
if (name==null || name.trim().length()==0) {
throw new IllegalArgumentException("Invalid identifier:null");
} else {
//check start letters
char c = name.charAt(0);
for (int i=0; i=0) {
throw new JMSException(String.format("Identifier may not contain character [%s]", MAY_NOT_CONTAIN[i]));
}
}
//check reserverd names
for (int i=0; i0", RMQConnectionMetaData.JMSX_GROUP_SEQ_LABEL));
}
} else if (RMQConnectionMetaData.JMSX_GROUP_ID_LABEL.equals(name)) {
/**
* Special case property must be a string
*/
if (value!=null && (!(value instanceof String))) {
throw new MessageFormatException(String.format("Property [%s] can only be of type String", RMQConnectionMetaData.JMSX_GROUP_ID_LABEL));
}
}
if (name!=null && name.startsWith(PREFIX)) {
if (value==null) {
this.rmqProperties.remove(name);
} else {
this.rmqProperties.put(name, (Serializable) value);
}
} else {
if (isReadOnlyProperties()) throw new MessageNotWriteableException(NOT_WRITEABLE);
checkName(name);
if (value==null) {
this.userJmsProperties.remove(name);
} else if (validPropertyValueType(value)) {
this.userJmsProperties.put(name, (Serializable) value);
} else {
throw new MessageFormatException(String.format("Property [%s] has incorrect value type.", name));
}
}
} catch (ClassCastException x) {
throw new RMQJMSException("Property value not serializable.", x);
}
}
/**
* Only certain types are allowed as property value types (excluding our own types).
* @param value
* @return
*/
private boolean validPropertyValueType(Object value) {
return (value instanceof String
|| value instanceof Boolean
|| value instanceof Byte
|| value instanceof Short
|| value instanceof Integer
|| value instanceof Long
|| value instanceof Float
|| value instanceof Double);
}
/**
* Although the JavaDoc implies otherwise, this call acknowledges all unacknowledged messages on the message's
* session.
*
* The JavaDoc says:
*
*
* A client may individually acknowledge each message as it is consumed, or it may choose to
* acknowledge messages as an application-defined group (which is done by calling acknowledge on the last received
* message of the group, thereby acknowledging all messages consumed by the session.)
*
*
*
* ...but there is no way to do either of these things, as is explained by the specification[1].
*
* {@inheritDoc}
*
* [1] JMS 1.1 spec Section 11.3.2.2.
*
* @see RMQSession#acknowledgeMessage(RMQMessage)
*/
@Override
public void acknowledge() throws JMSException {
getSession().acknowledgeMessage(this);
}
/**
* {@inheritDoc}
*/
@Override
public final void clearBody() throws JMSException {
setReadOnlyBody(false);
clearBodyInternal();
}
protected abstract void clearBodyInternal() throws JMSException;
/** @return the {@link Charset} used to convert a {@link TextMessage} to byte[]
*/
private static final Charset getCharset() {
return CHARSET;
}
/**
* Invoked when {@link RMQMessage#toByteArray()} is called to create
* a byte[] from a message. Each subclass must implement this, but ONLY
* write its specific body. All the properties defined in {@link Message}
* will be written by the parent class.
* @param out - the output stream to which the structured part of message body (scalar types) is written
* @param bout - the output stream to which the un-structured part of message body (explicit bytes) is written
* @throws IOException if the body can not be written
*/
protected abstract void writeBody(ObjectOutput out, ByteArrayOutputStream bout) throws IOException;
/**
* Invoked when {@link RMQMessage#toAmqpByteArray()} is called to create
* a byte[] from a message. Each subclass must implement this, but ONLY
* write its specific body.
* @param out - the output stream to which the message body is written
* @throws IOException if the body can not be written
*/
protected abstract void writeAmqpBody(ByteArrayOutputStream out) throws IOException;
/**
* Invoked when a message is being deserialized to read and decode the message body.
* The implementing class should only read its body from this stream.
* If any exception is thrown, the message will not have been delivered.
*
* @param inputStream - the stream to read its body from
* @param bin - the underlying byte input stream
* @throws IOException if a read error occurs on the input stream
* @throws ClassNotFoundException if the object class cannot be found
*/
protected abstract void readBody(ObjectInput inputStream, ByteArrayInputStream bin) throws IOException, ClassNotFoundException;
/**
* Invoked when an AMQP message is being transformed into a RMQMessage
* The implementing class should only read its body by this method
*
* @param barr - the byte array payload of the AMQP message
*/
protected abstract void readAmqpBody(byte[] barr);
/**
* Generate the headers for this JMS message; these are the properties used in selection.
*
* We attach some JMS properties as headers on the message. This is for the
* purposes of message selection.
*
*
* The headers are:
*
*
* Header Field Set By
* JMSDestination send or publish method
* JMSDeliveryMode send or publish method
* JMSExpiration send or publish method
* JMSPriority send or publish method
* JMSMessageID send or publish method
* JMSTimestamp send or publish method
* JMSCorrelationID Client
* JMSReplyTo Client
* JMSType Client
* JMSRedelivered JMS provider
*
*
* From the JMS 1.1 spec:
*
Message header field references are restricted to JMSDeliveryMode
,
* JMSPriority
, JMSMessageID
, JMSTimestamp
, JMSCorrelationID
,
* and JMSType
.
* [Why not JMSExpiration
? ]
*
*
* JMSMessageID
, JMSCorrelationID
, and JMSType
values may be
* null
and if so are treated as a NULL value.
*
*
*/
Map toHeaders() throws IOException, JMSException {
Map hdrs = new HashMap();
// set non-null user properties
for (Map.Entry e : this.userJmsProperties.entrySet()) {
putIfNotNull(hdrs, e.getKey(), e.getValue());
}
// set (overwrite?) selectable JMS properties
hdrs.put("JMSDeliveryMode", (this.getJMSDeliveryMode()==DeliveryMode.PERSISTENT ? "PERSISTENT": "NON_PERSISTENT"));
putIfNotNull(hdrs, "JMSMessageID", this.getJMSMessageID());
hdrs.put("JMSTimestamp", this.getJMSTimestamp());
hdrs.put("JMSPriority", this.getJMSPriority());
putIfNotNull(hdrs, "JMSCorrelationID", this.getJMSCorrelationID());
putIfNotNull(hdrs, "JMSType", this.getJMSType());
return hdrs;
}
/**
* Converts a {@link GetResponse} to a {@link RMQMessage}
*
* @param response - the message information from RabbitMQ {@link Channel#basicGet} or via a {@link Consumer}.
* @return the JMS message corresponding to the RabbitMQ message
* @throws JMSException
*/
static RMQMessage convertMessage(RMQSession session, RMQDestination dest, GetResponse response) throws JMSException {
if (response == null) /* return null if the response is null */
return null;
if (dest.isAmqp()) {
return convertAmqpMessage(session, dest, response);
} else {
return convertJmsMessage(session, dest, response);
}
}
private static RMQMessage convertJmsMessage(RMQSession session, RMQDestination dest, GetResponse response) throws JMSException {
RMQMessage message = RMQMessage.fromMessage(response.getBody()); // Deserialize the message payload from the byte[] body
message.setSession(session); // Insert session in received message for Message.acknowledge
message.setJMSRedelivered(response.getEnvelope().isRedeliver()); // Set the redelivered flag
message.setRabbitDeliveryTag(response.getEnvelope().getDeliveryTag()); // Insert delivery tag in received message for Message.acknowledge
// message.setJMSDestination(dest); // DO NOT set the destination bug#57214768
// JMSProperties already set
message.setReadonly(true); // Set readOnly - mandatory for received messages
return message;
}
private static RMQMessage convertAmqpMessage(RMQSession session, RMQDestination dest, GetResponse response) throws JMSException {
try {
BasicProperties props = response.getProps();
RMQMessage message = RMQMessage.isAmqpTextMessage(props.getHeaders()) ? new RMQTextMessage() : new RMQBytesMessage();
message = RMQMessage.fromAmqpMessage(response.getBody(), message); // Deserialize the message payload from the byte[] body
message.setSession(session); // Insert session in received message for Message.acknowledge
message.setJMSRedelivered(response.getEnvelope().isRedeliver()); // Set the redelivered flag
message.setRabbitDeliveryTag(response.getEnvelope().getDeliveryTag()); // Insert delivery tag in received message for Message.acknowledge
message.setJMSDestination(dest); // We cannot know the original destination, so set local one
message.setJMSPropertiesFromAmqpProperties(props);
message.setReadonly(true); // Set readOnly - mandatory for received messages
return message;
} catch (IOException x) {
throw new RMQJMSException(x);
}
}
/**
* Generate the headers for an AMQP message.
*
* We attach some JMS properties as headers on the message. This is so the client can see them at the other end.
*
*
* The headers we code are user headers (if they are type convertible) and:
*
*
*
* Header Field Set By
* JMSDeliveryMode send or publish method
* JMSPriority send or publish method
* JMSMessageID send or publish method
* JMSTimestamp send or publish method
* JMSType send or publish method (not client, as we aren't the client).
*
*
* But (from the JMS 1.1 spec):
*
Message header field references are restricted to JMSDeliveryMode
,
* JMSPriority
, JMSMessageID
, JMSTimestamp
, JMSCorrelationID
,
* and JMSType
.
*
* JMSMessageID
, JMSCorrelationID
, and JMSType
values may be
* null
and if so are treated as a NULL value.
*
*
*/
Map toAmqpHeaders() throws IOException, JMSException {
Map hdrs = new HashMap();
// set non-null user properties
for (Map.Entry e : this.userJmsProperties.entrySet()) {
putIfNotNullAndAmqpType(hdrs, e.getKey(), e.getValue());
}
// set (overwrite?) selectable JMS properties
hdrs.put("JMSDeliveryMode", (this.getJMSDeliveryMode()==DeliveryMode.PERSISTENT ? "PERSISTENT": "NON_PERSISTENT"));
putIfNotNull(hdrs, "JMSMessageID", this.getJMSMessageID());
hdrs.put("JMSTimestamp", this.getJMSTimestamp());
hdrs.put("JMSPriority", this.getJMSPriority());
putIfNotNull(hdrs, "JMSCorrelationID", this.getJMSCorrelationID());
putIfNotNull(hdrs, "JMSType", this.getJMSType());
return hdrs;
}
private static void putIfNotNullAndAmqpType(Map hdrs, String key, Object val) {
if (val!=null)
if ( val instanceof String
|| val instanceof Integer
|| val instanceof Float
|| val instanceof Double
|| val instanceof Long
|| val instanceof Short
|| val instanceof Byte
)
hdrs.put(key, val);
}
private static void putIfNotNull(Map hdrs, String key, Object val) {
if (val!=null) hdrs.put(key, val);
}
/**
* Generates an AMQP byte array body for this message.
* This method invokes the {@link #writeAmqpBody(ObjectOutput)} method
* on the message subclass.
* @return the body in a byte array
* @throws IOException if conversion fails
*/
byte[] toAmqpByteArray() throws IOException, JMSException {
ByteArrayOutputStream bout = new ByteArrayOutputStream(DEFAULT_MESSAGE_BODY_SIZE);
//invoke write body
this.writeAmqpBody(bout);
//flush and return
bout.flush();
return bout.toByteArray();
}
/**
* Generates a JMS byte array body for this message.
* This method invokes the {@link #writeBody(ObjectOutput)} method
* on the class that is being serialized
* @return the body in a byte array
* @throws IOException if serialization fails
*/
byte[] toByteArray() throws IOException, JMSException {
ByteArrayOutputStream bout = new ByteArrayOutputStream(DEFAULT_MESSAGE_BODY_SIZE);
ObjectOutputStream out = new ObjectOutputStream(bout);
//write the class of the message so we can instantiate on the other end
out.writeUTF(this.getClass().getName());
//write out message id
out.writeUTF(this.internalMessageID);
//write our JMS properties
out.writeInt(this.rmqProperties.size());
for (Map.Entry entry : this.rmqProperties.entrySet()) {
out.writeUTF(entry.getKey());
writePrimitive(entry.getValue(), out, true);
}
//write custom properties
out.writeInt(this.userJmsProperties.size());
for (Map.Entry entry : this.userJmsProperties.entrySet()) {
out.writeUTF(entry.getKey());
writePrimitive(entry.getValue(), out, true);
}
out.flush(); // ensure structured part written to byte stream
this.writeBody(out, bout);
out.flush(); // force any more structured data to byte stream
return bout.toByteArray();
}
/**
* Deserializes a {@link RMQMessage} from a JMS generated byte array
* This method invokes the {@link #readBody(ObjectInput)} method
* on the deserialized class
* @param b - the message bytes
* @return a RMQMessage object
* @throws ClassNotFoundException - if the class to be deserialized wasn't found
* @throws IOException if an exception occurs during deserialization
* @throws IllegalAccessException if an exception occurs during class instantiation
* @throws InstantiationException if an exception occurs during class instantiation
* @throws RMQJMSException if RJMS class-related errors occur
*/
static RMQMessage fromMessage(byte[] b) throws RMQJMSException {
/* If we don't recognise the message format this throws an exception */
try {
ByteArrayInputStream bin = new ByteArrayInputStream(b);
ObjectInputStream in = new ObjectInputStream(bin);
//read the classname from the stream
String clazz = in.readUTF();
//instantiate the message object with the thread context classloader
RMQMessage msg = (RMQMessage) Class.forName(clazz, true, Thread.currentThread().getContextClassLoader()).newInstance();
//read the message id
msg.internalMessageID = in.readUTF();
//read JMS properties
int propsize = in.readInt();
for (int i = 0; i < propsize; i++) {
String name = in.readUTF();
Object value = readPrimitive(in);
msg.rmqProperties.put(name, (Serializable) value);
}
//read custom properties
propsize = in.readInt();
for (int i = 0; i < propsize; i++) {
String name = in.readUTF();
Object value = readPrimitive(in);
msg.userJmsProperties.put(name, (Serializable) value);
}
// read the body of the message
msg.readBody(in, bin);
return msg;
} catch (IOException x) {
throw new RMQJMSException(x);
} catch (ClassNotFoundException x) {
throw new RMQJMSException(x);
} catch (IllegalAccessException x) {
throw new RMQJMSException(x);
} catch (InstantiationException x) {
throw new RMQJMSException(x);
}
}
/**
* Deserializes a {@link RMQBytesMessage} from an AMQP generated byte array
* into a pre-created message object.
* @param b - the message bytes
* @param msg - a pre-created skeleton message object
* @return a RMQMessage object, with the correct body
* @throws ClassNotFoundException - if the class to be deserialized wasn't found
* @throws IOException if an exception occurs during deserialization
* @throws IllegalAccessException if an exception occurs during class instantiation
* @throws InstantiationException if an exception occurs during class instantiation
*/
static RMQMessage fromAmqpMessage(byte[] b, RMQMessage msg) throws IOException {
msg.readAmqpBody(b);
return msg;
}
/**
* @param deliveryMode JMS delivery mode value
* @return RabbitMQ delivery mode value
*/
static final int rmqDeliveryMode(int deliveryMode) {
return (deliveryMode == javax.jms.DeliveryMode.PERSISTENT ? 2 : 1);
}
/**
* @param rmqDeliveryMode rabbitmq delivery mode value
* @return JMS delivery mode String
*/
static final int jmsDeliveryMode(Integer rmqDeliveryMode) {
return ( ( rmqDeliveryMode != null
&& rmqDeliveryMode == 2
) ? javax.jms.DeliveryMode.PERSISTENT
: javax.jms.DeliveryMode.NON_PERSISTENT);
}
/**
* @param rmqExpiration rabbitmq expiration value
* @return JMS expiration long value
*/
static final long jmsExpiration(String rmqExpiration, Date da) {
if (null==da) da = new Date(); // assume now -- wrong nearly always
if (null==rmqExpiration)
return (0l);
try {
return da.getTime() + Long.valueOf(rmqExpiration);
} catch (NumberFormatException e) { // ignore it if conversion problems */ }
return (0l);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.internalMessageID == null) ? 0 : this.internalMessageID.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RMQMessage other = (RMQMessage) obj;
if (this.internalMessageID == null) {
if (other.internalMessageID != null)
return false;
} else if (!this.internalMessageID.equals(other.internalMessageID))
return false;
return true;
}
/**
* Returns the unique ID for this message.
* This will return null unless the message has been sent on the wire
* @return
*/
public String getInternalID() {
return this.internalMessageID;
}
/**
* Called when a message is sent so that each message is unique
*/
public void generateInternalID() {
this.internalMessageID = Util.generateUUID("");
this.rmqProperties.put(JMS_MESSAGE_ID, "ID:" + this.internalMessageID);
}
/**
* Utility method used to be able to write primitives and objects to a data
* stream without keeping track of order and type.
*
* This also allows any Object to be written.
*
*
* The purpose of this method is to optimise the writing of a primitive that
* is represented as an object by only writing the type and the primitive
* value to the stream.
*
*
* @param s
* the primitive to be written
* @param out
* the stream to write the primitive to.
* @throws IOException
* if an IOException occurs.
*/
public static void writePrimitive(Object s, ObjectOutput out) throws IOException, MessageFormatException {
writePrimitive(s, out, false);
}
public static void writePrimitive(Object s, ObjectOutput out, boolean allowSerializable) throws IOException, MessageFormatException {
if (s == null) {
out.writeByte(-1);
} else if (s instanceof Boolean) {
out.writeByte(1);
out.writeBoolean(((Boolean) s).booleanValue());
} else if (s instanceof Byte) {
out.writeByte(2);
out.writeByte(((Byte) s).byteValue());
} else if (s instanceof Short) {
out.writeByte(3);
out.writeShort((((Short) s).shortValue()));
} else if (s instanceof Integer) {
out.writeByte(4);
out.writeInt(((Integer) s).intValue());
} else if (s instanceof Long) {
out.writeByte(5);
out.writeLong(((Long) s).longValue());
} else if (s instanceof Float) {
out.writeByte(6);
out.writeFloat(((Float) s).floatValue());
} else if (s instanceof Double) {
out.writeByte(7);
out.writeDouble(((Double) s).doubleValue());
} else if (s instanceof String) {
out.writeByte(8);
out.writeUTF((String) s);
} else if (s instanceof Character) {
out.writeByte(9);
out.writeChar(((Character) s).charValue());
} else if (s instanceof byte[]) {
out.writeByte(10);
out.writeInt(((byte[]) s).length);
out.write(((byte[]) s));
} else if (allowSerializable && s instanceof Serializable){
out.writeByte(Byte.MAX_VALUE);
out.writeObject(s);
} else {
throw new MessageFormatException(s + " is not a recognized primitive type.");
}
}
/**
* Utility method to read objects from a stream. These objects must have
* been written with the method {@link #writePrimitive(Object, ObjectOutput)} otherwise
* deserialization will fail and an IOException will be thrown
* @param in the stream to read from
* @return the Object read
* @throws IOException
* @throws ClassNotFoundException
*/
public static Object readPrimitive(ObjectInput in) throws IOException, ClassNotFoundException {
byte b = in.readByte();
switch (b) {
case -1:
return null;
case 1:
return in.readBoolean();
case 2:
return in.readByte();
case 3:
return in.readShort();
case 4:
return in.readInt();
case 5:
return in.readLong();
case 6:
return in.readFloat();
case 7:
return in.readDouble();
case 8:
return in.readUTF();
case 9:
return in.readChar();
case 10: {
int length = in.readInt();
byte[] buf = new byte[length];
in.read(buf);
return buf;
}
default:
return in.readObject();
}
}
/**
* {@inheritDoc}
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
void setJMSPropertiesFromAmqpProperties(BasicProperties props) throws JMSException {
this.setJMSDeliveryMode(jmsDeliveryMode(props.getDeliveryMode())); // derive delivery mode from Rabbit delivery mode
Date da = props.getTimestamp();
if (null!=da) this.setJMSTimestamp(da.getTime()/1000l); // JMS timestamps are the number of *seconds* since jan 1 1900
String ex = props.getExpiration(); // this is a time-to-live period in milliseconds, as a Long.toString(long, 10)
if (null!=ex) this.setJMSExpiration(RMQMessage.jmsExpiration(ex, da));
Integer pr = props.getPriority();
if (null!=pr) this.setJMSPriority(pr);
String mi = props.getMessageId(); // This is AMQP's message ID
if (null!=mi) this.setJMSMessageID(mi);
String ci = props.getCorrelationId(); // This is AMQP's correlation ID
if (null!=ci) this.setJMSCorrelationID(ci);
this.setJMSType(isAmqpTextMessage(props.getHeaders()) ? "TextMessage" : "BytesMessage");
// now set properties from header: these may overwrite the ones that have already been set above
Map hdrs = props.getHeaders();
if (hdrs!=null) {
for (Entry e : hdrs.entrySet()) {
String key = e.getKey();
Object val = e.getValue();
if (key.equals("JMSExpiration")) { this.setJMSExpiration(objectToLong(val, 0l));}
else if (key.equals("JMSPriority")) { this.setJMSPriority(objectToInt(val, 4));}
else if (key.equals("JMSTimestamp")) { this.setJMSTimestamp(objectToLong(val, 0l));}
else if (key.equals("JMSMessageID")) { this.setJMSMessageID(val.toString());}
else if (key.equals("JMSCorrelationID")){ this.setJMSCorrelationID(val.toString());}
else if (key.equals("JMSType")) { this.setJMSType(val.toString());}
else if (key.startsWith(PREFIX)) {} // avoid setting this internal field
else if (key.startsWith("JMS")) {} // avoid setting this field
else { this.userJmsProperties.put(key, val.toString());}
}
}
}
static boolean isAmqpTextMessage(Map hdrs) {
boolean isTextMessage = false;
if(hdrs != null) {
Object headerJMSType = hdrs.get("JMSType");
isTextMessage = (headerJMSType != null && "TextMessage".equals(headerJMSType.toString()));
}
return isTextMessage;
}
private static long objectToLong(Object val, long dft) {
if (val==null) return dft;
if (val instanceof Number) return ((Number) val).longValue();
try {
if (val instanceof CharSequence) return Long.valueOf(val.toString());
} catch (NumberFormatException e) { /*ignore*/ }
return dft;
}
private static int objectToInt(Object val, int dft) {
if (val==null) return dft;
if (val instanceof Number) return ((Number) val).intValue();
try {
if (val instanceof CharSequence) return Integer.valueOf(val.toString());
} catch (NumberFormatException e) { /*ignore*/ }
return dft;
}
static final RMQMessage normalise(Message msg) throws JMSException {
if (msg instanceof RMQMessage) return (RMQMessage) msg;
/* If not one of our own, copy it into an RMQMessage */
if (msg instanceof BytesMessage ) return RMQBytesMessage.recreate((BytesMessage)msg);
else if (msg instanceof MapMessage ) return RMQMapMessage.recreate((MapMessage)msg);
else if (msg instanceof ObjectMessage) return RMQObjectMessage.recreate((ObjectMessage)msg);
else if (msg instanceof StreamMessage) return RMQStreamMessage.recreate((StreamMessage)msg);
else if (msg instanceof TextMessage ) return RMQTextMessage.recreate((TextMessage)msg);
else return RMQNullMessage.recreate(msg);
}
/** Assign generic attributes.
*
* “rmqMessage = (RMQMessage) message;”
* With conversion as appropriate.
* @param rmqMessage message filled in with attributes
* @param message message from which attributes are gained.
*/
protected static void copyAttributes(RMQMessage rmqMessage, Message message) throws JMSException {
try {
rmqMessage.setJMSCorrelationID(message.getJMSCorrelationID());
rmqMessage.setJMSType(message.getJMSType());
// NOTE: all other JMS header values are set when the message is sent; except for ReplyTo which is ignored on
// a foreign message.
copyProperties(rmqMessage, message);
} catch (Exception e) {
throw new RMQJMSException("Error converting Message to RMQMessage.", e);
}
}
private static void copyProperties(RMQMessage rmqMsg, Message msg) throws JMSException {
@SuppressWarnings("unchecked")
Enumeration propNames = msg.getPropertyNames();
while (propNames.hasMoreElements()) {
String name = propNames.nextElement();
rmqMsg.setObjectProperty(name, msg.getObjectProperty(name));
}
}
}