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}