001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.activemq.transport.amqp.message; 018 019import java.io.DataInputStream; 020import java.io.IOException; 021import java.io.ObjectOutputStream; 022import java.io.Serializable; 023import java.nio.charset.Charset; 024import java.nio.charset.StandardCharsets; 025import java.util.HashMap; 026import java.util.LinkedHashMap; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.zip.InflaterInputStream; 030 031import javax.jms.JMSException; 032 033import org.apache.activemq.command.ActiveMQBytesMessage; 034import org.apache.activemq.command.ActiveMQMapMessage; 035import org.apache.activemq.command.ActiveMQObjectMessage; 036import org.apache.activemq.command.ActiveMQTextMessage; 037import org.apache.activemq.util.ByteArrayInputStream; 038import org.apache.activemq.util.ByteArrayOutputStream; 039import org.apache.activemq.util.ByteSequence; 040import org.apache.activemq.util.JMSExceptionSupport; 041import org.apache.qpid.proton.amqp.Binary; 042import org.apache.qpid.proton.amqp.Symbol; 043import org.apache.qpid.proton.amqp.messaging.Data; 044import org.apache.qpid.proton.message.Message; 045 046/** 047 * Support class containing constant values and static methods that are 048 * used to map to / from AMQP Message types being sent or received. 049 */ 050public final class AmqpMessageSupport { 051 052 // Message Properties used to map AMQP to JMS and back 053 054 public static final String JMS_AMQP_PREFIX = "JMS_AMQP_"; 055 public static final int JMS_AMQP_PREFIX_LENGTH = JMS_AMQP_PREFIX.length(); 056 057 public static final String MESSAGE_FORMAT = "MESSAGE_FORMAT"; 058 public static final String ORIGINAL_ENCODING = "ORIGINAL_ENCODING"; 059 public static final String NATIVE = "NATIVE"; 060 public static final String HEADER = "HEADER"; 061 public static final String PROPERTIES = "PROPERTIES"; 062 063 public static final String FIRST_ACQUIRER = "FirstAcquirer"; 064 public static final String CONTENT_TYPE = "ContentType"; 065 public static final String CONTENT_ENCODING = "ContentEncoding"; 066 public static final String REPLYTO_GROUP_ID = "ReplyToGroupID"; 067 068 public static final String DELIVERY_ANNOTATION_PREFIX = "DA_"; 069 public static final String MESSAGE_ANNOTATION_PREFIX = "MA_"; 070 public static final String FOOTER_PREFIX = "FT_"; 071 072 public static final String JMS_AMQP_HEADER = JMS_AMQP_PREFIX + HEADER; 073 public static final String JMS_AMQP_PROPERTIES = JMS_AMQP_PREFIX + PROPERTIES; 074 public static final String JMS_AMQP_ORIGINAL_ENCODING = JMS_AMQP_PREFIX + ORIGINAL_ENCODING; 075 public static final String JMS_AMQP_MESSAGE_FORMAT = JMS_AMQP_PREFIX + MESSAGE_FORMAT; 076 public static final String JMS_AMQP_NATIVE = JMS_AMQP_PREFIX + NATIVE; 077 public static final String JMS_AMQP_FIRST_ACQUIRER = JMS_AMQP_PREFIX + FIRST_ACQUIRER; 078 public static final String JMS_AMQP_CONTENT_TYPE = JMS_AMQP_PREFIX + CONTENT_TYPE; 079 public static final String JMS_AMQP_CONTENT_ENCODING = JMS_AMQP_PREFIX + CONTENT_ENCODING; 080 public static final String JMS_AMQP_REPLYTO_GROUP_ID = JMS_AMQP_PREFIX + REPLYTO_GROUP_ID; 081 public static final String JMS_AMQP_DELIVERY_ANNOTATION_PREFIX = JMS_AMQP_PREFIX + DELIVERY_ANNOTATION_PREFIX; 082 public static final String JMS_AMQP_MESSAGE_ANNOTATION_PREFIX = JMS_AMQP_PREFIX + MESSAGE_ANNOTATION_PREFIX; 083 public static final String JMS_AMQP_FOOTER_PREFIX = JMS_AMQP_PREFIX + FOOTER_PREFIX; 084 085 // Message body type definitions 086 public static final Binary EMPTY_BINARY = new Binary(new byte[0]); 087 public static final Data EMPTY_BODY = new Data(EMPTY_BINARY); 088 public static final Data NULL_OBJECT_BODY; 089 090 public static final short AMQP_UNKNOWN = 0; 091 public static final short AMQP_NULL = 1; 092 public static final short AMQP_DATA = 2; 093 public static final short AMQP_SEQUENCE = 3; 094 public static final short AMQP_VALUE_NULL = 4; 095 public static final short AMQP_VALUE_STRING = 5; 096 public static final short AMQP_VALUE_BINARY = 6; 097 public static final short AMQP_VALUE_MAP = 7; 098 public static final short AMQP_VALUE_LIST = 8; 099 100 static { 101 byte[] bytes; 102 try { 103 bytes = getSerializedBytes(null); 104 } catch (IOException e) { 105 throw new RuntimeException("Failed to initialise null object body", e); 106 } 107 108 NULL_OBJECT_BODY = new Data(new Binary(bytes)); 109 } 110 111 /** 112 * Content type used to mark Data sections as containing a serialized java object. 113 */ 114 public static final String SERIALIZED_JAVA_OBJECT_CONTENT_TYPE = "application/x-java-serialized-object"; 115 116 /** 117 * Content type used to mark Data sections as containing arbitrary bytes. 118 */ 119 public static final String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream"; 120 121 /** 122 * Lookup and return the correct Proton Symbol instance based on the given key. 123 * 124 * @param key 125 * the String value name of the Symbol to locate. 126 * 127 * @return the Symbol value that matches the given key. 128 */ 129 public static Symbol getSymbol(String key) { 130 return Symbol.valueOf(key); 131 } 132 133 /** 134 * Safe way to access message annotations which will check internal structure and 135 * either return the annotation if it exists or null if the annotation or any annotations 136 * are present. 137 * 138 * @param key 139 * the String key to use to lookup an annotation. 140 * @param message 141 * the AMQP message object that is being examined. 142 * 143 * @return the given annotation value or null if not present in the message. 144 */ 145 public static Object getMessageAnnotation(String key, Message message) { 146 if (message != null && message.getMessageAnnotations() != null) { 147 Map<Symbol, Object> annotations = message.getMessageAnnotations().getValue(); 148 return annotations.get(AmqpMessageSupport.getSymbol(key)); 149 } 150 151 return null; 152 } 153 154 /** 155 * Check whether the content-type field of the properties section (if present) in 156 * the given message matches the provided string (where null matches if there is 157 * no content type present. 158 * 159 * @param contentType 160 * content type string to compare against, or null if none 161 * @param message 162 * the AMQP message object that is being examined. 163 * 164 * @return true if content type matches 165 */ 166 public static boolean isContentType(String contentType, Message message) { 167 if (contentType == null) { 168 return message.getContentType() == null; 169 } else { 170 return contentType.equals(message.getContentType()); 171 } 172 } 173 174 /** 175 * @param contentType the contentType of the received message 176 * @return the character set to use, or null if not to treat the message as text 177 */ 178 public static Charset getCharsetForTextualContent(String contentType) { 179 try { 180 return AmqpContentTypeSupport.parseContentTypeForTextualCharset(contentType); 181 } catch (InvalidContentTypeException e) { 182 return null; 183 } 184 } 185 186 private static byte[] getSerializedBytes(Serializable value) throws IOException { 187 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 188 ObjectOutputStream oos = new ObjectOutputStream(baos)) { 189 190 oos.writeObject(value); 191 oos.flush(); 192 oos.close(); 193 194 return baos.toByteArray(); 195 } 196 } 197 198 /** 199 * Return the encoded form of the BytesMessage as an AMQP Binary instance. 200 * 201 * @param message 202 * the Message whose binary encoded body is needed. 203 * 204 * @return a Binary instance containing the encoded message body. 205 * 206 * @throws JMSException if an error occurs while fetching the binary payload. 207 */ 208 public static Binary getBinaryFromMessageBody(ActiveMQBytesMessage message) throws JMSException { 209 Binary result = null; 210 211 if (message.getContent() != null) { 212 ByteSequence contents = message.getContent(); 213 214 if (message.isCompressed()) { 215 int length = (int) message.getBodyLength(); 216 byte[] uncompressed = new byte[length]; 217 message.readBytes(uncompressed); 218 219 result = new Binary(uncompressed); 220 } else { 221 return new Binary(contents.getData(), contents.getOffset(), contents.getLength()); 222 } 223 } 224 225 return result; 226 } 227 228 /** 229 * Return the encoded form of the BytesMessage as an AMQP Binary instance. 230 * 231 * @param message 232 * the Message whose binary encoded body is needed. 233 * 234 * @return a Binary instance containing the encoded message body. 235 * 236 * @throws JMSException if an error occurs while fetching the binary payload. 237 */ 238 public static Binary getBinaryFromMessageBody(ActiveMQObjectMessage message) throws JMSException { 239 Binary result = null; 240 241 if (message.getContent() != null) { 242 ByteSequence contents = message.getContent(); 243 244 if (message.isCompressed()) { 245 try (ByteArrayOutputStream os = new ByteArrayOutputStream(); 246 ByteArrayInputStream is = new ByteArrayInputStream(contents); 247 InflaterInputStream iis = new InflaterInputStream(is);) { 248 249 byte value; 250 while ((value = (byte) iis.read()) != -1) { 251 os.write(value); 252 } 253 254 ByteSequence expanded = os.toByteSequence(); 255 result = new Binary(expanded.getData(), expanded.getOffset(), expanded.getLength()); 256 } catch (Exception cause) { 257 throw JMSExceptionSupport.create(cause); 258 } 259 } else { 260 return new Binary(contents.getData(), contents.getOffset(), contents.getLength()); 261 } 262 } 263 264 return result; 265 } 266 267 /** 268 * Return the encoded form of the Message as an AMQP Binary instance. 269 * 270 * @param message 271 * the Message whose binary encoded body is needed. 272 * 273 * @return a Binary instance containing the encoded message body. 274 * 275 * @throws JMSException if an error occurs while fetching the binary payload. 276 */ 277 public static Binary getBinaryFromMessageBody(ActiveMQTextMessage message) throws JMSException { 278 Binary result = null; 279 280 if (message.getContent() != null) { 281 ByteSequence contents = message.getContent(); 282 283 if (message.isCompressed()) { 284 try (ByteArrayInputStream is = new ByteArrayInputStream(contents); 285 InflaterInputStream iis = new InflaterInputStream(is); 286 DataInputStream dis = new DataInputStream(iis);) { 287 288 int size = dis.readInt(); 289 byte[] uncompressed = new byte[size]; 290 dis.readFully(uncompressed); 291 292 result = new Binary(uncompressed); 293 } catch (Exception cause) { 294 throw JMSExceptionSupport.create(cause); 295 } 296 } else { 297 // Message includes a size prefix of four bytes for the OpenWire marshaler 298 result = new Binary(contents.getData(), contents.getOffset() + 4, contents.getLength() - 4); 299 } 300 } else if (message.getText() != null) { 301 result = new Binary(message.getText().getBytes(StandardCharsets.UTF_8)); 302 } 303 304 return result; 305 } 306 307 /** 308 * Return the underlying Map from the JMS MapMessage instance. 309 * 310 * @param message 311 * the MapMessage whose underlying Map is requested. 312 * 313 * @return the underlying Map used to store the value in the given MapMessage. 314 * 315 * @throws JMSException if an error occurs in constructing or fetching the Map. 316 */ 317 public static Map<String, Object> getMapFromMessageBody(ActiveMQMapMessage message) throws JMSException { 318 final HashMap<String, Object> map = new LinkedHashMap<String, Object>(); 319 320 final Map<String, Object> contentMap = message.getContentMap(); 321 if (contentMap != null) { 322 for (Entry<String, Object> entry : contentMap.entrySet()) { 323 Object value = entry.getValue(); 324 if (value instanceof byte[]) { 325 value = new Binary((byte[]) value); 326 } 327 map.put(entry.getKey(), value); 328 } 329 } 330 331 return map; 332 } 333}