/* * 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.activemq.transport.amqp.message; import java.io.DataInputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.zip.InflaterInputStream; import javax.jms.JMSException; import org.apache.activemq.command.ActiveMQBytesMessage; import org.apache.activemq.command.ActiveMQMapMessage; import org.apache.activemq.command.ActiveMQObjectMessage; import org.apache.activemq.command.ActiveMQTextMessage; import org.apache.activemq.util.ByteArrayInputStream; import org.apache.activemq.util.ByteArrayOutputStream; import org.apache.activemq.util.ByteSequence; import org.apache.activemq.util.JMSExceptionSupport; import org.apache.qpid.proton.amqp.Binary; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.messaging.Data; import org.apache.qpid.proton.message.Message; /** * Support class containing constant values and static methods that are * used to map to / from AMQP Message types being sent or received. */ public final class AmqpMessageSupport { // Message Properties used to map AMQP to JMS and back public static final String JMS_AMQP_PREFIX = "JMS_AMQP_"; public static final int JMS_AMQP_PREFIX_LENGTH = JMS_AMQP_PREFIX.length(); public static final String MESSAGE_FORMAT = "MESSAGE_FORMAT"; public static final String ORIGINAL_ENCODING = "ORIGINAL_ENCODING"; public static final String NATIVE = "NATIVE"; public static final String HEADER = "HEADER"; public static final String PROPERTIES = "PROPERTIES"; public static final String FIRST_ACQUIRER = "FirstAcquirer"; public static final String CONTENT_TYPE = "ContentType"; public static final String CONTENT_ENCODING = "ContentEncoding"; public static final String REPLYTO_GROUP_ID = "ReplyToGroupID"; public static final String DELIVERY_ANNOTATION_PREFIX = "DA_"; public static final String MESSAGE_ANNOTATION_PREFIX = "MA_"; public static final String FOOTER_PREFIX = "FT_"; public static final String JMS_AMQP_HEADER = JMS_AMQP_PREFIX + HEADER; public static final String JMS_AMQP_PROPERTIES = JMS_AMQP_PREFIX + PROPERTIES; public static final String JMS_AMQP_ORIGINAL_ENCODING = JMS_AMQP_PREFIX + ORIGINAL_ENCODING; public static final String JMS_AMQP_MESSAGE_FORMAT = JMS_AMQP_PREFIX + MESSAGE_FORMAT; public static final String JMS_AMQP_NATIVE = JMS_AMQP_PREFIX + NATIVE; public static final String JMS_AMQP_FIRST_ACQUIRER = JMS_AMQP_PREFIX + FIRST_ACQUIRER; public static final String JMS_AMQP_CONTENT_TYPE = JMS_AMQP_PREFIX + CONTENT_TYPE; public static final String JMS_AMQP_CONTENT_ENCODING = JMS_AMQP_PREFIX + CONTENT_ENCODING; public static final String JMS_AMQP_REPLYTO_GROUP_ID = JMS_AMQP_PREFIX + REPLYTO_GROUP_ID; public static final String JMS_AMQP_DELIVERY_ANNOTATION_PREFIX = JMS_AMQP_PREFIX + DELIVERY_ANNOTATION_PREFIX; public static final String JMS_AMQP_MESSAGE_ANNOTATION_PREFIX = JMS_AMQP_PREFIX + MESSAGE_ANNOTATION_PREFIX; public static final String JMS_AMQP_FOOTER_PREFIX = JMS_AMQP_PREFIX + FOOTER_PREFIX; // Message body type definitions public static final Binary EMPTY_BINARY = new Binary(new byte[0]); public static final Data EMPTY_BODY = new Data(EMPTY_BINARY); public static final Data NULL_OBJECT_BODY; public static final short AMQP_UNKNOWN = 0; public static final short AMQP_NULL = 1; public static final short AMQP_DATA = 2; public static final short AMQP_SEQUENCE = 3; public static final short AMQP_VALUE_NULL = 4; public static final short AMQP_VALUE_STRING = 5; public static final short AMQP_VALUE_BINARY = 6; public static final short AMQP_VALUE_MAP = 7; public static final short AMQP_VALUE_LIST = 8; static { byte[] bytes; try { bytes = getSerializedBytes(null); } catch (IOException e) { throw new RuntimeException("Failed to initialise null object body", e); } NULL_OBJECT_BODY = new Data(new Binary(bytes)); } /** * Content type used to mark Data sections as containing a serialized java object. */ public static final String SERIALIZED_JAVA_OBJECT_CONTENT_TYPE = "application/x-java-serialized-object"; /** * Content type used to mark Data sections as containing arbitrary bytes. */ public static final String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream"; /** * Lookup and return the correct Proton Symbol instance based on the given key. * * @param key * the String value name of the Symbol to locate. * * @return the Symbol value that matches the given key. */ public static Symbol getSymbol(String key) { return Symbol.valueOf(key); } /** * Safe way to access message annotations which will check internal structure and * either return the annotation if it exists or null if the annotation or any annotations * are present. * * @param key * the String key to use to lookup an annotation. * @param message * the AMQP message object that is being examined. * * @return the given annotation value or null if not present in the message. */ public static Object getMessageAnnotation(String key, Message message) { if (message != null && message.getMessageAnnotations() != null) { Map<Symbol, Object> annotations = message.getMessageAnnotations().getValue(); return annotations.get(AmqpMessageSupport.getSymbol(key)); } return null; } /** * Check whether the content-type field of the properties section (if present) in * the given message matches the provided string (where null matches if there is * no content type present. * * @param contentType * content type string to compare against, or null if none * @param message * the AMQP message object that is being examined. * * @return true if content type matches */ public static boolean isContentType(String contentType, Message message) { if (contentType == null) { return message.getContentType() == null; } else { return contentType.equals(message.getContentType()); } } /** * @param contentType the contentType of the received message * @return the character set to use, or null if not to treat the message as text */ public static Charset getCharsetForTextualContent(String contentType) { try { return AmqpContentTypeSupport.parseContentTypeForTextualCharset(contentType); } catch (InvalidContentTypeException e) { return null; } } private static byte[] getSerializedBytes(Serializable value) throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(value); oos.flush(); oos.close(); return baos.toByteArray(); } } /** * Return the encoded form of the BytesMessage as an AMQP Binary instance. * * @param message * the Message whose binary encoded body is needed. * * @return a Binary instance containing the encoded message body. * * @throws JMSException if an error occurs while fetching the binary payload. */ public static Binary getBinaryFromMessageBody(ActiveMQBytesMessage message) throws JMSException { Binary result = null; if (message.getContent() != null) { ByteSequence contents = message.getContent(); if (message.isCompressed()) { int length = (int) message.getBodyLength(); byte[] uncompressed = new byte[length]; message.readBytes(uncompressed); result = new Binary(uncompressed); } else { return new Binary(contents.getData(), contents.getOffset(), contents.getLength()); } } return result; } /** * Return the encoded form of the BytesMessage as an AMQP Binary instance. * * @param message * the Message whose binary encoded body is needed. * * @return a Binary instance containing the encoded message body. * * @throws JMSException if an error occurs while fetching the binary payload. */ public static Binary getBinaryFromMessageBody(ActiveMQObjectMessage message) throws JMSException { Binary result = null; if (message.getContent() != null) { ByteSequence contents = message.getContent(); if (message.isCompressed()) { try (ByteArrayOutputStream os = new ByteArrayOutputStream(); ByteArrayInputStream is = new ByteArrayInputStream(contents); InflaterInputStream iis = new InflaterInputStream(is);) { byte value; while ((value = (byte) iis.read()) != -1) { os.write(value); } ByteSequence expanded = os.toByteSequence(); result = new Binary(expanded.getData(), expanded.getOffset(), expanded.getLength()); } catch (Exception cause) { throw JMSExceptionSupport.create(cause); } } else { return new Binary(contents.getData(), contents.getOffset(), contents.getLength()); } } return result; } /** * Return the encoded form of the Message as an AMQP Binary instance. * * @param message * the Message whose binary encoded body is needed. * * @return a Binary instance containing the encoded message body. * * @throws JMSException if an error occurs while fetching the binary payload. */ public static Binary getBinaryFromMessageBody(ActiveMQTextMessage message) throws JMSException { Binary result = null; if (message.getContent() != null) { ByteSequence contents = message.getContent(); if (message.isCompressed()) { try (ByteArrayInputStream is = new ByteArrayInputStream(contents); InflaterInputStream iis = new InflaterInputStream(is); DataInputStream dis = new DataInputStream(iis);) { int size = dis.readInt(); byte[] uncompressed = new byte[size]; dis.readFully(uncompressed); result = new Binary(uncompressed); } catch (Exception cause) { throw JMSExceptionSupport.create(cause); } } else { // Message includes a size prefix of four bytes for the OpenWire marshaler result = new Binary(contents.getData(), contents.getOffset() + 4, contents.getLength() - 4); } } else if (message.getText() != null) { result = new Binary(message.getText().getBytes(StandardCharsets.UTF_8)); } return result; } /** * Return the underlying Map from the JMS MapMessage instance. * * @param message * the MapMessage whose underlying Map is requested. * * @return the underlying Map used to store the value in the given MapMessage. * * @throws JMSException if an error occurs in constructing or fetching the Map. */ public static Map<String, Object> getMapFromMessageBody(ActiveMQMapMessage message) throws JMSException { final HashMap<String, Object> map = new LinkedHashMap<String, Object>(); final Map<String, Object> contentMap = message.getContentMap(); if (contentMap != null) { for (Entry<String, Object> entry : contentMap.entrySet()) { Object value = entry.getValue(); if (value instanceof byte[]) { value = new Binary((byte[]) value); } map.put(entry.getKey(), value); } } return map; } }