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 static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_DATA;
020import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_NULL;
021import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_SEQUENCE;
022import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_UNKNOWN;
023import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_VALUE_BINARY;
024import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_VALUE_LIST;
025import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.AMQP_VALUE_STRING;
026import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.CONTENT_ENCODING;
027import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.CONTENT_TYPE;
028import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.DELIVERY_ANNOTATION_PREFIX;
029import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.EMPTY_BINARY;
030import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.FIRST_ACQUIRER;
031import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.FOOTER_PREFIX;
032import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.HEADER;
033import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_CONTENT_TYPE;
034import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_DELIVERY_ANNOTATION_PREFIX;
035import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_FOOTER_PREFIX;
036import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_MESSAGE_ANNOTATION_PREFIX;
037import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_ORIGINAL_ENCODING;
038import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_PREFIX;
039import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.JMS_AMQP_PREFIX_LENGTH;
040import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.MESSAGE_ANNOTATION_PREFIX;
041import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.MESSAGE_FORMAT;
042import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.NATIVE;
043import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.ORIGINAL_ENCODING;
044import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.PROPERTIES;
045import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.REPLYTO_GROUP_ID;
046import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.SERIALIZED_JAVA_OBJECT_CONTENT_TYPE;
047import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.getBinaryFromMessageBody;
048import static org.apache.activemq.transport.amqp.message.AmqpMessageSupport.getMapFromMessageBody;
049
050import java.io.IOException;
051import java.nio.charset.StandardCharsets;
052import java.util.ArrayList;
053import java.util.Date;
054import java.util.HashMap;
055import java.util.LinkedHashMap;
056import java.util.Map;
057
058import javax.jms.JMSException;
059import javax.jms.Message;
060import javax.jms.MessageEOFException;
061import javax.jms.TextMessage;
062
063import org.apache.activemq.command.ActiveMQBytesMessage;
064import org.apache.activemq.command.ActiveMQDestination;
065import org.apache.activemq.command.ActiveMQMapMessage;
066import org.apache.activemq.command.ActiveMQMessage;
067import org.apache.activemq.command.ActiveMQObjectMessage;
068import org.apache.activemq.command.ActiveMQStreamMessage;
069import org.apache.activemq.command.ActiveMQTextMessage;
070import org.apache.activemq.command.CommandTypes;
071import org.apache.activemq.command.ConnectionId;
072import org.apache.activemq.command.ConnectionInfo;
073import org.apache.activemq.command.ConsumerId;
074import org.apache.activemq.command.MessageId;
075import org.apache.activemq.command.RemoveInfo;
076import org.apache.activemq.transport.amqp.AmqpProtocolException;
077import org.apache.activemq.util.JMSExceptionSupport;
078import org.apache.activemq.util.TypeConversionSupport;
079import org.apache.qpid.proton.amqp.Binary;
080import org.apache.qpid.proton.amqp.Symbol;
081import org.apache.qpid.proton.amqp.UnsignedByte;
082import org.apache.qpid.proton.amqp.UnsignedInteger;
083import org.apache.qpid.proton.amqp.messaging.AmqpSequence;
084import org.apache.qpid.proton.amqp.messaging.AmqpValue;
085import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
086import org.apache.qpid.proton.amqp.messaging.Data;
087import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
088import org.apache.qpid.proton.amqp.messaging.Footer;
089import org.apache.qpid.proton.amqp.messaging.Header;
090import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
091import org.apache.qpid.proton.amqp.messaging.Properties;
092import org.apache.qpid.proton.amqp.messaging.Section;
093import org.apache.qpid.proton.codec.AMQPDefinedTypes;
094import org.apache.qpid.proton.codec.DecoderImpl;
095import org.apache.qpid.proton.codec.EncoderImpl;
096
097public class JMSMappingOutboundTransformer implements OutboundTransformer {
098
099    public static final Symbol JMS_DEST_TYPE_MSG_ANNOTATION = Symbol.valueOf("x-opt-jms-dest");
100    public static final Symbol JMS_REPLY_TO_TYPE_MSG_ANNOTATION = Symbol.valueOf("x-opt-jms-reply-to");
101
102    private static final String AMQ_SCHEDULED_MESSAGE_PREFIX = "AMQ_SCHEDULED_";
103
104    public static final byte QUEUE_TYPE = 0x00;
105    public static final byte TOPIC_TYPE = 0x01;
106    public static final byte TEMP_QUEUE_TYPE = 0x02;
107    public static final byte TEMP_TOPIC_TYPE = 0x03;
108
109    private final UTF8BufferType utf8BufferEncoding;
110
111    // For now Proton requires that we create a decoder to create an encoder
112    private final DecoderImpl decoder = new DecoderImpl();
113    private final EncoderImpl encoder = new EncoderImpl(decoder);
114    {
115        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
116
117        utf8BufferEncoding = new UTF8BufferType(encoder, decoder);
118
119        encoder.register(utf8BufferEncoding);
120    }
121
122    @Override
123    public EncodedMessage transform(ActiveMQMessage message) throws Exception {
124        if (message == null) {
125            return null;
126        }
127
128        long messageFormat = 0;
129        Header header = null;
130        Properties properties = null;
131        Map<Symbol, Object> daMap = null;
132        Map<Symbol, Object> maMap = null;
133        Map<String,Object> apMap = null;
134        Map<Object, Object> footerMap = null;
135
136        Section body = convertBody(message);
137
138        if (message.isPersistent()) {
139            if (header == null) {
140                header = new Header();
141            }
142            header.setDurable(true);
143        }
144        byte priority = message.getPriority();
145        if (priority != Message.DEFAULT_PRIORITY) {
146            if (header == null) {
147                header = new Header();
148            }
149            header.setPriority(UnsignedByte.valueOf(priority));
150        }
151        String type = message.getType();
152        if (type != null) {
153            if (properties == null) {
154                properties = new Properties();
155            }
156            properties.setSubject(type);
157        }
158        MessageId messageId = message.getMessageId();
159        if (messageId != null) {
160            if (properties == null) {
161                properties = new Properties();
162            }
163            properties.setMessageId(getOriginalMessageId(message));
164        }
165        ActiveMQDestination destination = message.getDestination();
166        if (destination != null) {
167            if (properties == null) {
168                properties = new Properties();
169            }
170            properties.setTo(destination.getQualifiedName());
171            if (maMap == null) {
172                maMap = new HashMap<>();
173            }
174            maMap.put(JMS_DEST_TYPE_MSG_ANNOTATION, destinationType(destination));
175        }
176        ActiveMQDestination replyTo = message.getReplyTo();
177        if (replyTo != null) {
178            if (properties == null) {
179                properties = new Properties();
180            }
181            properties.setReplyTo(replyTo.getQualifiedName());
182            if (maMap == null) {
183                maMap = new HashMap<>();
184            }
185            maMap.put(JMS_REPLY_TO_TYPE_MSG_ANNOTATION, destinationType(replyTo));
186        }
187        String correlationId = message.getCorrelationId();
188        if (correlationId != null) {
189            if (properties == null) {
190                properties = new Properties();
191            }
192            try {
193                properties.setCorrelationId(AMQPMessageIdHelper.INSTANCE.toIdObject(correlationId));
194            } catch (AmqpProtocolException e) {
195                properties.setCorrelationId(correlationId);
196            }
197        }
198        long expiration = message.getExpiration();
199        if (expiration != 0) {
200            long ttl = expiration - System.currentTimeMillis();
201            if (ttl < 0) {
202                ttl = 1;
203            }
204
205            if (header == null) {
206                header = new Header();
207            }
208            header.setTtl(new UnsignedInteger((int) ttl));
209
210            if (properties == null) {
211                properties = new Properties();
212            }
213            properties.setAbsoluteExpiryTime(new Date(expiration));
214        }
215        long timeStamp = message.getTimestamp();
216        if (timeStamp != 0) {
217            if (properties == null) {
218                properties = new Properties();
219            }
220            properties.setCreationTime(new Date(timeStamp));
221        }
222
223        // JMSX Message Properties
224        int deliveryCount = message.getRedeliveryCounter();
225        if (deliveryCount > 0) {
226            if (header == null) {
227                header = new Header();
228            }
229            header.setDeliveryCount(UnsignedInteger.valueOf(deliveryCount));
230        }
231        String userId = message.getUserID();
232        if (userId != null) {
233            if (properties == null) {
234                properties = new Properties();
235            }
236            properties.setUserId(new Binary(userId.getBytes(StandardCharsets.UTF_8)));
237        }
238        String groupId = message.getGroupID();
239        if (groupId != null) {
240            if (properties == null) {
241                properties = new Properties();
242            }
243            properties.setGroupId(groupId);
244        }
245        int groupSequence = message.getGroupSequence();
246        if (groupSequence > 0) {
247            if (properties == null) {
248                properties = new Properties();
249            }
250            properties.setGroupSequence(UnsignedInteger.valueOf(groupSequence));
251        }
252
253        final Map<String, Object> entries;
254        try {
255            entries = message.getProperties();
256        } catch (IOException e) {
257            throw JMSExceptionSupport.create(e);
258        }
259
260        for (Map.Entry<String, Object> entry : entries.entrySet()) {
261            String key = entry.getKey();
262            Object value = entry.getValue();
263
264            if (key.startsWith(JMS_AMQP_PREFIX)) {
265                if (key.startsWith(NATIVE, JMS_AMQP_PREFIX_LENGTH)) {
266                    // skip transformer appended properties
267                    continue;
268                } else if (key.startsWith(ORIGINAL_ENCODING, JMS_AMQP_PREFIX_LENGTH)) {
269                    // skip transformer appended properties
270                    continue;
271                } else if (key.startsWith(MESSAGE_FORMAT, JMS_AMQP_PREFIX_LENGTH)) {
272                    messageFormat = (long) TypeConversionSupport.convert(entry.getValue(), Long.class);
273                    continue;
274                } else if (key.startsWith(HEADER, JMS_AMQP_PREFIX_LENGTH)) {
275                    if (header == null) {
276                        header = new Header();
277                    }
278                    continue;
279                } else if (key.startsWith(PROPERTIES, JMS_AMQP_PREFIX_LENGTH)) {
280                    if (properties == null) {
281                        properties = new Properties();
282                    }
283                    continue;
284                } else if (key.startsWith(MESSAGE_ANNOTATION_PREFIX, JMS_AMQP_PREFIX_LENGTH)) {
285                    if (maMap == null) {
286                        maMap = new HashMap<>();
287                    }
288                    String name = key.substring(JMS_AMQP_MESSAGE_ANNOTATION_PREFIX.length());
289                    maMap.put(Symbol.valueOf(name), value);
290                    continue;
291                } else if (key.startsWith(FIRST_ACQUIRER, JMS_AMQP_PREFIX_LENGTH)) {
292                    if (header == null) {
293                        header = new Header();
294                    }
295                    header.setFirstAcquirer((boolean) TypeConversionSupport.convert(value, Boolean.class));
296                    continue;
297                } else if (key.startsWith(CONTENT_TYPE, JMS_AMQP_PREFIX_LENGTH)) {
298                    if (properties == null) {
299                        properties = new Properties();
300                    }
301                    properties.setContentType(Symbol.getSymbol((String) TypeConversionSupport.convert(value, String.class)));
302                    continue;
303                } else if (key.startsWith(CONTENT_ENCODING, JMS_AMQP_PREFIX_LENGTH)) {
304                    if (properties == null) {
305                        properties = new Properties();
306                    }
307                    properties.setContentEncoding(Symbol.getSymbol((String) TypeConversionSupport.convert(value, String.class)));
308                    continue;
309                } else if (key.startsWith(REPLYTO_GROUP_ID, JMS_AMQP_PREFIX_LENGTH)) {
310                    if (properties == null) {
311                        properties = new Properties();
312                    }
313                    properties.setReplyToGroupId((String) TypeConversionSupport.convert(value, String.class));
314                    continue;
315                } else if (key.startsWith(DELIVERY_ANNOTATION_PREFIX, JMS_AMQP_PREFIX_LENGTH)) {
316                    if (daMap == null) {
317                        daMap = new HashMap<>();
318                    }
319                    String name = key.substring(JMS_AMQP_DELIVERY_ANNOTATION_PREFIX.length());
320                    daMap.put(Symbol.valueOf(name), value);
321                    continue;
322                } else if (key.startsWith(FOOTER_PREFIX, JMS_AMQP_PREFIX_LENGTH)) {
323                    if (footerMap == null) {
324                        footerMap = new HashMap<>();
325                    }
326                    String name = key.substring(JMS_AMQP_FOOTER_PREFIX.length());
327                    footerMap.put(Symbol.valueOf(name), value);
328                    continue;
329                }
330            } else if (key.startsWith(AMQ_SCHEDULED_MESSAGE_PREFIX )) {
331                // strip off the scheduled message properties
332                continue;
333            }
334
335            // The property didn't map into any other slot so we store it in the
336            // Application Properties section of the message.
337            if (apMap == null) {
338                apMap = new HashMap<>();
339            }
340            apMap.put(key, value);
341
342            int messageType = message.getDataStructureType();
343            if (messageType == CommandTypes.ACTIVEMQ_MESSAGE) {
344                // Type of command to recognize advisory message
345                Object data = message.getDataStructure();
346                if(data != null) {
347                    apMap.put("ActiveMqDataStructureType", data.getClass().getSimpleName());
348                }
349            }
350        }
351
352        final AmqpWritableBuffer buffer = new AmqpWritableBuffer();
353        encoder.setByteBuffer(buffer);
354
355        if (header != null) {
356            encoder.writeObject(header);
357        }
358        if (daMap != null) {
359            encoder.writeObject(new DeliveryAnnotations(daMap));
360        }
361        if (maMap != null) {
362            encoder.writeObject(new MessageAnnotations(maMap));
363        }
364        if (properties != null) {
365            encoder.writeObject(properties);
366        }
367        if (apMap != null) {
368            encoder.writeObject(new ApplicationProperties(apMap));
369        }
370        if (body != null) {
371            encoder.writeObject(body);
372        }
373        if (footerMap != null) {
374            encoder.writeObject(new Footer(footerMap));
375        }
376
377        return new EncodedMessage(messageFormat, buffer.getArray(), 0, buffer.getArrayLength());
378    }
379
380    private Section convertBody(ActiveMQMessage message) throws JMSException {
381
382        Section body = null;
383        short orignalEncoding = AMQP_UNKNOWN;
384
385        try {
386            orignalEncoding = message.getShortProperty(JMS_AMQP_ORIGINAL_ENCODING);
387        } catch (Exception ex) {
388            // Ignore and stick with UNKNOWN
389        }
390
391        int messageType = message.getDataStructureType();
392
393        if (messageType == CommandTypes.ACTIVEMQ_MESSAGE) {
394                Object data = message.getDataStructure();
395            if (data instanceof ConnectionInfo) {
396                        ConnectionInfo connectionInfo = (ConnectionInfo)data;
397                        final HashMap<String, Object> connectionMap = new LinkedHashMap<String, Object>();
398                        
399                        connectionMap.put("ConnectionId", connectionInfo.getConnectionId().getValue());
400                        connectionMap.put("ClientId", connectionInfo.getClientId());
401                        connectionMap.put("ClientIp", connectionInfo.getClientIp());
402                        connectionMap.put("UserName", connectionInfo.getUserName());
403                        connectionMap.put("BrokerMasterConnector", connectionInfo.isBrokerMasterConnector());
404                        connectionMap.put("Manageable", connectionInfo.isManageable());
405                        connectionMap.put("ClientMaster", connectionInfo.isClientMaster());
406                        connectionMap.put("FaultTolerant", connectionInfo.isFaultTolerant());
407                        connectionMap.put("FailoverReconnect", connectionInfo.isFailoverReconnect());
408                        
409                        body = new AmqpValue(connectionMap);
410            } else if (data instanceof RemoveInfo) {
411                        RemoveInfo removeInfo = (RemoveInfo)message.getDataStructure();
412                        final HashMap<String, Object> removeMap = new LinkedHashMap<String, Object>();
413                        
414                if (removeInfo.isConnectionRemove()) {
415                        removeMap.put(ConnectionId.class.getSimpleName(), ((ConnectionId)removeInfo.getObjectId()).getValue());
416                } else if (removeInfo.isConsumerRemove()) {
417                        removeMap.put(ConsumerId.class.getSimpleName(), ((ConsumerId)removeInfo.getObjectId()).getValue());
418                        removeMap.put("SessionId", ((ConsumerId)removeInfo.getObjectId()).getSessionId());
419                        removeMap.put("ConnectionId", ((ConsumerId)removeInfo.getObjectId()).getConnectionId());
420                        removeMap.put("ParentId", ((ConsumerId)removeInfo.getObjectId()).getParentId());
421                }
422                
423                body = new AmqpValue(removeMap);
424            }
425        } else if (messageType == CommandTypes.ACTIVEMQ_BYTES_MESSAGE) {
426            Binary payload = getBinaryFromMessageBody((ActiveMQBytesMessage) message);
427
428            if (payload == null) {
429                payload = EMPTY_BINARY;
430            }
431
432            switch (orignalEncoding) {
433                case AMQP_NULL:
434                    break;
435                case AMQP_VALUE_BINARY:
436                    body = new AmqpValue(payload);
437                    break;
438                case AMQP_DATA:
439                case AMQP_UNKNOWN:
440                default:
441                    body = new Data(payload);
442                    break;
443            }
444        } else if (messageType == CommandTypes.ACTIVEMQ_TEXT_MESSAGE) {
445            switch (orignalEncoding) {
446                case AMQP_NULL:
447                    break;
448                case AMQP_DATA:
449                    body = new Data(getBinaryFromMessageBody((ActiveMQTextMessage) message));
450                    break;
451                case AMQP_VALUE_STRING:
452                case AMQP_UNKNOWN:
453                default:
454                    body = new AmqpValue(((TextMessage) message).getText());
455                    break;
456            }
457        } else if (messageType == CommandTypes.ACTIVEMQ_MAP_MESSAGE) {
458            body = new AmqpValue(getMapFromMessageBody((ActiveMQMapMessage) message));
459        } else if (messageType == CommandTypes.ACTIVEMQ_STREAM_MESSAGE) {
460            ArrayList<Object> list = new ArrayList<>();
461            final ActiveMQStreamMessage m = (ActiveMQStreamMessage) message;
462            try {
463                while (true) {
464                    list.add(m.readObject());
465                }
466            } catch (MessageEOFException e) {
467            }
468
469            switch (orignalEncoding) {
470                case AMQP_SEQUENCE:
471                    body = new AmqpSequence(list);
472                    break;
473                case AMQP_VALUE_LIST:
474                case AMQP_UNKNOWN:
475                default:
476                    body = new AmqpValue(list);
477                    break;
478            }
479        } else if (messageType == CommandTypes.ACTIVEMQ_OBJECT_MESSAGE) {
480            Binary payload = getBinaryFromMessageBody((ActiveMQObjectMessage) message);
481
482            if (payload == null) {
483                payload = EMPTY_BINARY;
484            }
485
486            switch (orignalEncoding) {
487                case AMQP_VALUE_BINARY:
488                    body = new AmqpValue(payload);
489                    break;
490                case AMQP_DATA:
491                case AMQP_UNKNOWN:
492                default:
493                    body = new Data(payload);
494                    break;
495            }
496
497            // For a non-AMQP message we tag the outbound content type as containing
498            // a serialized Java object so that an AMQP client has a hint as to what
499            // we are sending it.
500            if (!message.propertyExists(JMS_AMQP_CONTENT_TYPE)) {
501                message.setReadOnlyProperties(false);
502                message.setStringProperty(JMS_AMQP_CONTENT_TYPE, SERIALIZED_JAVA_OBJECT_CONTENT_TYPE);
503                message.setReadOnlyProperties(true);
504            }
505        }
506
507        return body;
508    }
509
510    private static byte destinationType(ActiveMQDestination destination) {
511        if (destination.isQueue()) {
512            if (destination.isTemporary()) {
513                return TEMP_QUEUE_TYPE;
514            } else {
515                return QUEUE_TYPE;
516            }
517        } else if (destination.isTopic()) {
518            if (destination.isTemporary()) {
519                return TEMP_TOPIC_TYPE;
520            } else {
521                return TOPIC_TYPE;
522            }
523        }
524
525        throw new IllegalArgumentException("Unknown Destination Type passed to JMS Transformer.");
526    }
527
528    private static Object getOriginalMessageId(ActiveMQMessage message) {
529        Object result;
530        MessageId messageId = message.getMessageId();
531        if (messageId.getTextView() != null) {
532            try {
533                result = AMQPMessageIdHelper.INSTANCE.toIdObject(messageId.getTextView());
534            } catch (AmqpProtocolException e) {
535                result = messageId.getTextView();
536            }
537        } else {
538            result = messageId.toString();
539        }
540
541        return result;
542    }
543}