com.kinglcc.spring.jms.core.converter.Jackson2JmsMessageConverter.java Source code

Java tutorial

Introduction

Here is the source code for com.kinglcc.spring.jms.core.converter.Jackson2JmsMessageConverter.java

Source

/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed 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 com.kinglcc.spring.jms.core.converter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.atomic.AtomicReference;

import javax.jms.BytesMessage;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.jms.support.converter.MessageConversionException;
import org.springframework.jms.support.converter.MessageType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Jackson2JmsMessageConverter
 * <pre>
 * Jackson2 jms message converter:
 * Use Jackson2 to convert java object to {@link javax.jms.Message}.
 * Convert {@link javax.jms.Message} to {@link String}
 * </pre>
 *
 * @author liaochaochao
 * @since 2016129 ?1:07:01
 */
public class Jackson2JmsMessageConverter implements JmsMessageConverter, BeanClassLoaderAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(Jackson2JmsMessageConverter.class);

    /**
     * Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
     */
    private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize",
            JavaType.class, AtomicReference.class);

    /**
     * The default encoding used for writing to text messages: UTF-8.
     */
    public static final String DEFAULT_ENCODING = "UTF-8";
    private static final String JMS_MESSAGE_ENCODING_PROP = "messageEncoding@";
    private static final String JMS_MESSAGE_TYPE_PROP = "messageTypeId@";

    private MessageType targetType = MessageType.TEXT;
    private String encoding = DEFAULT_ENCODING;
    private ObjectMapper objectMapper;
    private String encodingPropertyName = JMS_MESSAGE_ENCODING_PROP;
    private String typeIdPropertyName = JMS_MESSAGE_TYPE_PROP;

    private ClassLoader beanClassLoader;

    public Jackson2JmsMessageConverter() {
        this.objectMapper = new ObjectMapper();
        this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
        this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * Specify the {@link ObjectMapper} to use instead of using the default.
     */
    public void setObjectMapper(ObjectMapper objectMapper) {
        Assert.notNull(objectMapper, "ObjectMapper must not be null");
        this.objectMapper = objectMapper;
    }

    /**
     * Specify whether {@link #toMessage(Object, Session)} should marshal to a
     * {@link BytesMessage} or a {@link TextMessage}.
     * <p>The default is {@link MessageType#BYTES}, i.e. this converter marshals to
     * a {@link BytesMessage}. Note that the default version of this converter
     * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only.
     * @see MessageType#BYTES
     * @see MessageType#TEXT
     */
    public void setTargetType(MessageType targetType) {
        Assert.notNull(targetType, "MessageType must not be null");
        this.targetType = targetType;
    }

    /**
     * Specify the encoding to use when converting to and from text-based
     * message body content. The default encoding will be "UTF-8".
     * <p>When reading from a a text-based message, an encoding may have been
     * suggested through a special JMS property which will then be preferred
     * over the encoding set on this MessageConverter instance.
     * @see #setEncodingPropertyName
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /**
     * Specify the name of the JMS message property that carries the encoding from
     * bytes to String and back is BytesMessage is used during the conversion process.
     * <p>Default is none. Setting this property is optional; if not set, UTF-8 will
     * be used for decoding any incoming bytes message.
     * @see #setEncoding
     */
    public void setEncodingPropertyName(String encodingPropertyName) {
        this.encodingPropertyName = encodingPropertyName;
    }

    /**
     * Specify the name of the JMS message property that carries the type id for the
     * contained object: either a mapped id value or a raw Java class name.
     * <p>Default is none. <b>NOTE: This property needs to be set in order to allow
     * for converting from an incoming message to a Java object.</b>
     */
    public void setTypeIdPropertyName(String typeIdPropertyName) {
        this.typeIdPropertyName = typeIdPropertyName;
    }

    @Override
    public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException {
        Message message;
        try {
            switch (this.targetType) {
            case TEXT:
                message = mapToTextMessage(object, session, this.objectMapper);
                break;
            case BYTES:
                message = mapToBytesMessage(object, session, this.objectMapper);
                break;
            default:
                message = mapToMessage(object, session, this.objectMapper, this.targetType);
            }
        } catch (IOException ex) {
            throw new MessageConversionException("Could not map JSON object [" + object + "]", ex);
        }
        setTypeIdOnMessage(object, message);
        return message;
    }

    protected void setTypeIdOnMessage(Object object, Message message) throws JMSException {
        if (this.typeIdPropertyName != null) {
            String typeId = object.getClass().getCanonicalName();
            message.setStringProperty(this.typeIdPropertyName, typeId);
        }
    }

    @Override
    public Object fromMessage(Message message) throws JMSException, MessageConversionException {
        try {
            JavaType targetJavaType = getJavaTypeForMessage(message);
            String payload = getPayload(message);
            if (null != targetJavaType) {
                return this.objectMapper.readValue(payload, targetJavaType);
            }
            return new GenericMessage(payload);
        } catch (IOException ex) {
            throw new MessageConversionException("Failed to convert JSON message content", ex);
        }
    }

    /**
     * Map the given object to a {@link TextMessage}.
     * @param object the object to be mapped
     * @param session current JMS session
     * @param objectMapper the mapper to use
     * @return the resulting message
     * @throws JMSException if thrown by JMS methods
     * @throws IOException in case of I/O errors
     * @see Session#createBytesMessage
     */
    protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper)
            throws JMSException, IOException {

        StringWriter writer = new StringWriter();
        objectMapper.writeValue(writer, object);
        return session.createTextMessage(writer.toString());
    }

    /**
     * Map the given object to a {@link BytesMessage}.
     * @param object the object to be mapped
     * @param session current JMS session
     * @param objectMapper the mapper to use
     * @return the resulting message
     * @throws JMSException if thrown by JMS methods
     * @throws IOException in case of I/O errors
     * @see Session#createBytesMessage
     */
    protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper)
            throws JMSException, IOException {

        ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
        OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding);
        objectMapper.writeValue(writer, object);

        BytesMessage message = session.createBytesMessage();
        message.writeBytes(bos.toByteArray());
        if (this.encodingPropertyName != null) {
            message.setStringProperty(this.encodingPropertyName, this.encoding);
        }
        return message;
    }

    /**
     * Template method that allows for custom message mapping.
     * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or
     * {@link MessageType#BYTES}.
     * <p>The default implementation throws an {@link IllegalArgumentException}.
     * @param object the object to marshal
     * @param session the JMS Session
     * @param objectMapper the mapper to use
     * @param targetType the target message type (other than TEXT or BYTES)
     * @return the resulting message
     * @throws JMSException if thrown by JMS methods
     * @throws IOException in case of I/O errors
     */
    protected Message mapToMessage(Object object, Session session, ObjectMapper objectMapper,
            MessageType targetType) throws JMSException, IOException {

        throw new IllegalArgumentException("Unsupported message type [" + targetType
                + "]. MappingJackson2MessageConverter by default only supports TextMessages and BytesMessages.");
    }

    /**
     * Convenience method to dispatch to converters for individual message types.
     * 
     * @param message the input message
     * @return the message content
     */
    private String getPayload(Message message) throws JMSException, IOException {
        if (message instanceof TextMessage) {
            return getPayloadFromTextMessage((TextMessage) message);
        } else if (message instanceof BytesMessage) {
            return getPayloadFromBytesMessage((BytesMessage) message);
        } else {
            return getPayloadFromMessage(message);
        }
    }

    /**
     * Convert a TextMessage to a Java Object with the specified type.
     * @param message the input message
     * @return the message text
     * @throws JMSException if thrown by JMS
     * @throws IOException in case of I/O errors
     */
    protected String getPayloadFromTextMessage(TextMessage message) throws JMSException, IOException {
        return message.getText();
    }

    /**
     * Convert a BytesMessage to a Java Object with the specified type.
     * @param message the input message
     * @return the message body
     * @throws JMSException if thrown by JMS
     * @throws IOException in case of I/O errors
     */
    protected String getPayloadFromBytesMessage(BytesMessage message) throws JMSException, IOException {

        String encoding = this.encoding;
        if (this.encodingPropertyName != null && message.propertyExists(this.encodingPropertyName)) {
            encoding = message.getStringProperty(this.encodingPropertyName);
        }
        byte[] bytes = new byte[(int) message.getBodyLength()];
        message.readBytes(bytes);
        try {
            return new String(bytes, encoding);
        } catch (UnsupportedEncodingException ex) {
            throw new MessageConversionException("Cannot convert bytes to String", ex);
        }
    }

    /**
     * Template method that allows for custom message mapping.
     * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or
     * {@link MessageType#BYTES}.
     * <p>The default implementation throws an {@link IllegalArgumentException}.
     * @param message the input message
     * @return the message content
     * @throws JMSException if thrown by JMS
     * @throws IOException in case of I/O errors
     */
    protected String getPayloadFromMessage(Message message) throws JMSException, IOException {
        throw new IllegalArgumentException("Unsupported message type [" + message.getClass()
                + "]. default only supports TextMessages and BytesMessages.");
    }

    @Override
    public boolean canConvertFrom(Message message) {
        return message instanceof TextMessage || message instanceof BytesMessage;
    }

    @Override
    public boolean canConvertTo(Object payload) {
        return hasJackson2Converter(payload.getClass()) && canSerialize(payload);
    }

    private boolean hasJackson2Converter(Class<?> clazz) {
        return null != clazz.getAnnotation(Jackson2Converter.class);
    }

    private boolean canSerialize(Object payload) {
        if (!jackson23Available || !LOGGER.isWarnEnabled()) {
            return this.objectMapper.canSerialize(payload.getClass());
        }
        AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
        if (this.objectMapper.canSerialize(payload.getClass(), causeRef)) {
            return true;
        }
        Throwable cause = causeRef.get();
        if (cause != null) {
            String msg = "Failed to evaluate serialization for type [" + payload.getClass() + "]";
            if (LOGGER.isDebugEnabled()) {
                LOGGER.warn(msg, cause);
            } else {
                LOGGER.warn(msg + ": " + cause);
            }
        }
        return false;
    }

    protected JavaType getJavaTypeForMessage(Message message) throws JMSException {
        if (null == this.typeIdPropertyName || !message.propertyExists(this.typeIdPropertyName)) {
            return null;
        }
        String typeId = message.getStringProperty(this.typeIdPropertyName);
        if (typeId == null) {
            LOGGER.debug("Could not find type id property [{}]", typeIdPropertyName);
            return null;
        }

        try {
            Class<?> typeClass = ClassUtils.forName(typeId, this.beanClassLoader);
            return this.objectMapper.getTypeFactory().constructType(typeClass);
        } catch (Throwable ex) {
            throw new MessageConversionException("Failed to resolve type id [" + typeId + "]", ex);
        }
    }

    public static final class GenericMessage implements Serializable {

        private static final long serialVersionUID = -5140766195099007531L;

        private final Object content;

        public GenericMessage(Object content) {
            this.content = content;
        }

        public Object getContent() {
            return content;
        }
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.beanClassLoader = classLoader;
    }

}