com.viadeo.kasper.core.component.event.eventbus.EventBusMessageConverter.java Source code

Java tutorial

Introduction

Here is the source code for com.viadeo.kasper.core.component.event.eventbus.EventBusMessageConverter.java

Source

// ----------------------------------------------------------------------------
//  This file is part of the Kasper framework.
//
//  The Kasper framework is free software: you can redistribute it and/or 
//  modify it under the terms of the GNU Lesser General Public License as 
//  published by the Free Software Foundation, either version 3 of the 
//  License, or (at your option) any later version.
//
//  Kasper framework is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License
//  along with the framework Kasper.  
//  If not, see <http://www.gnu.org/licenses/>.
// --
//  Ce fichier fait partie du framework logiciel Kasper
//
//  Ce programme est un logiciel libre ; vous pouvez le redistribuer ou le 
//  modifier suivant les termes de la GNU Lesser General Public License telle 
//  que publie par la Free Software Foundation ; soit la version 3 de la 
//  licence, soit ( votre gr) toute version ultrieure.
//
//  Ce programme est distribu dans l'espoir qu'il sera utile, mais SANS 
//  AUCUNE GARANTIE ; sans mme la garantie tacite de QUALIT MARCHANDE ou 
//  d'ADQUATION  UN BUT PARTICULIER. Consultez la GNU Lesser General Public 
//  License pour plus de dtails.
//
//  Vous devez avoir reu une copie de la GNU Lesser General Public License en 
//  mme temps que ce programme ; si ce n'est pas le cas, consultez 
//  <http://www.gnu.org/licenses>
// ----------------------------------------------------------------------------
// ============================================================================
//                 KASPER - Kasper is the treasure keeper
//    www.viadeo.com - mobile.viadeo.com - api.viadeo.com - dev.viadeo.com
//
//           Viadeo Framework for effective CQRS/DDD architecture
// ============================================================================
package com.viadeo.kasper.core.component.event.eventbus;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.viadeo.kasper.api.context.Context;
import com.viadeo.kasper.api.context.ContextHelper;
import com.viadeo.kasper.common.serde.ObjectMapperProvider;
import org.axonframework.domain.DomainEventMessage;
import org.axonframework.domain.EventMessage;
import org.axonframework.domain.GenericDomainEventMessage;
import org.axonframework.domain.GenericEventMessage;
import org.axonframework.eventhandling.io.EventMessageType;
import org.axonframework.serializer.MessageSerializer;
import org.axonframework.serializer.SerializedObject;
import org.axonframework.serializer.Serializer;
import org.axonframework.serializer.SimpleSerializedObject;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.amqp.support.converter.MessageConverter;

import java.util.Date;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;

public class EventBusMessageConverter implements MessageConverter {

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

    private static final String HEADER_REQUIRED_MESSAGE = "header %s is required";

    protected static final String SERIALIZER_VERSION_KEY = "X-SERIALIZER-VERSION";
    protected static final String AGGREGATE_ID_KEY = "X-AGGREGATE-ID";
    protected static final String AGGREGATE_ID_TYPE_KEY = "X-AGGREGATE-TYPE-ID";
    protected static final String SEQUENCE_NUMBER_KEY = "X-SEQUENCE-NUMBER";

    protected static final String PAYLOAD_REVISION_KEY = "X-PAYLOAD-REVISION";
    protected static final String PAYLOAD_TYPE_KEY = "X-PAYLOAD-TYPE";

    protected static final String EVENT_TYPE_KEY = "X-EVENT-TYPE";
    protected static final String EVENT_TIMESTAMP_KEY = "X-EVENT-TIMESTAMP";
    protected static final String EVENT_UNIT_OF_WORK_ID = "X-EVENT-UNIT-OF-WORK-ID";
    protected static final String EVENT_PERSISTENCY_TYPE = "X-EVENT-PERSISTENCY-TYPE";

    protected static final String PREFIX_METADATA_KEY = "X-META-";
    protected static final String PREFIX_CONTEXT_KEY = "X-CONTEXT-";
    public static final int MAX_PAYLOAD_SIZE = 128 * 1000;
    public static final String MAX_PAYLOAD_SIZE_MESSAGE = "The message payload exceed allowed limit of %s, event %s has the following size %s";

    private final MessageSerializer serializer;
    private final ContextHelper contextHelper;
    private final ObjectMapper objectMapper;

    /**
     * Constructor
     *
     * @param contextHelper the contextHelper
     * @param serializer message serializer
     */
    public EventBusMessageConverter(final ContextHelper contextHelper, final Serializer serializer) {
        this.serializer = new MessageSerializer(checkNotNull(serializer));
        this.contextHelper = checkNotNull(contextHelper);
        this.objectMapper = ObjectMapperProvider.INSTANCE.mapper();
    }

    /**
     * Utility method to check headers when deserializing
     *
     * @param headers headers
     * @param headerName header to check
     * @return header
     */
    private Object checkAndGetHeader(final Map<String, Object> headers, final String headerName) {
        return checkNotNull(headers.get(headerName), HEADER_REQUIRED_MESSAGE, headerName);
    }

    /**
     * Transform rabbitmq message properties to axon metadata
     *
     * @param properties message properties
     * @return axon metadata
     */
    protected Map<String, Object> toMetadata(final MessageProperties properties) {
        checkNotNull(properties);

        final Map<String, String> contextMap = Maps.newHashMap();
        final Map<String, Object> metadata = Maps.newHashMap();

        if (properties.getHeaders() != null) {
            for (final Map.Entry<String, Object> header : properties.getHeaders().entrySet()) {
                final Object value = header.getValue();
                if (value != null) {
                    if (header.getKey().startsWith(PREFIX_CONTEXT_KEY)) {
                        contextMap.put(header.getKey().substring(PREFIX_CONTEXT_KEY.length()),
                                String.valueOf(value));
                    } else {
                        metadata.put(header.getKey(), value);
                    }
                }
            }
        }

        metadata.put("delivery-mode", MessageDeliveryMode.toInt(properties.getDeliveryMode()));
        metadata.put("message-id", properties.getMessageId());
        metadata.put("content-encoding", properties.getContentEncoding());
        metadata.put("content-type", properties.getContentType());
        metadata.put("type", properties.getType());
        metadata.put(Context.METANAME, contextHelper.createFrom(contextMap));

        return metadata;
    }

    /**
     * Transform an axon event message to an amqp message
     *
     * @param object message to convert
     * @param messageProperties spring message properties
     * @return converted message
     * @throws org.springframework.amqp.support.converter.MessageConversionException throws a MessageConversionException
     */
    public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {

        EventMessage eventMessage = (EventMessage) object;

        checkNotNull(eventMessage);

        final SerializedObject<byte[]> payload = serializer.serializePayload(eventMessage, byte[].class);
        DateTime timestamp = checkNotNull(eventMessage.getTimestamp());
        String className = checkNotNull(eventMessage.getPayloadType().getName());
        MessageBuilderSupport<Message> builder = MessageBuilder.withBody(payload.getData())
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT).setContentType("application/json")
                .setContentEncoding("UTF-8").setMessageId(checkNotNull(eventMessage.getIdentifier()))
                .setType(className).setTimestamp(new Date(eventMessage.getTimestamp().getMillis()))
                .setHeader(SERIALIZER_VERSION_KEY, "1.0").setHeader(EVENT_TIMESTAMP_KEY, timestamp.toString())
                .setHeader(PAYLOAD_REVISION_KEY, checkNotNull(payload.getType().getRevision()))
                .setHeader(PAYLOAD_TYPE_KEY, className)
                .setHeader(EVENT_TYPE_KEY, checkNotNull(EventMessageType.forMessage(eventMessage).getTypeByte()));

        if (eventMessage instanceof DomainEventMessage) {
            final DomainEventMessage domainEventMessage = (DomainEventMessage) eventMessage;
            final Object aggregateIdentifier = checkNotNull(domainEventMessage.getAggregateIdentifier());
            try {
                builder.setHeader(AGGREGATE_ID_KEY, objectMapper.writeValueAsString(aggregateIdentifier))
                        .setHeader(AGGREGATE_ID_TYPE_KEY, aggregateIdentifier.getClass().getName())
                        .setHeader(SEQUENCE_NUMBER_KEY, checkNotNull(domainEventMessage.getSequenceNumber()));
            } catch (JsonProcessingException e) {
                LOGGER.warn("Failed to specify properties of a domain event message", e);
            }
        }

        serializeMetadata(eventMessage, builder);

        Message message = builder.build();

        int bodyLength = message.getBody().length;
        Preconditions.checkState(MAX_PAYLOAD_SIZE > bodyLength, MAX_PAYLOAD_SIZE_MESSAGE, MAX_PAYLOAD_SIZE,
                className, bodyLength);

        return message;
    }

    /**
     * Serialize metadata
     * If null value is encountered for context, then an exception is thrown
     * If null value is encountered for other keys, a warn message is logged
     *
     * @param eventMessage event message
     * @param builder builder
     * @throws NullPointerException when encountering null value in property
     */
    private void serializeMetadata(EventMessage eventMessage, MessageBuilderSupport<Message> builder) {
        for (final Map.Entry<String, Object> entry : eventMessage.getMetaData().entrySet()) {
            checkNotNull(entry.getValue(), "encountered null value when serializing metadata %s", entry.getKey());
            if (Context.METANAME.equals(entry.getKey())) {
                final Context context = (Context) entry.getValue();
                for (final Map.Entry<String, String> contextEntry : context.asMap().entrySet()) {
                    checkNotNull(contextEntry.getValue(), "encountered null value in context %s",
                            contextEntry.getKey());
                    builder.setHeader(PREFIX_CONTEXT_KEY + contextEntry.getKey(), contextEntry.getValue());
                }
            } else {
                builder.setHeader(PREFIX_METADATA_KEY + entry.getKey(), entry.getValue());
            }
        }
    }

    @Override
    public Object fromMessage(Message message) throws MessageConversionException {

        try {
            checkNotNull(message);

            final MessageProperties messageProperties = message.getMessageProperties();
            final Map<String, Object> headers = messageProperties.getHeaders();
            final DateTime timestamp = new DateTime(checkAndGetHeader(headers, EVENT_TIMESTAMP_KEY));
            final Object payloadType = checkAndGetHeader(headers, PAYLOAD_TYPE_KEY);
            final Object payloadRevision = checkAndGetHeader(headers, PAYLOAD_REVISION_KEY);
            final SimpleSerializedObject<byte[]> serializedPayload = new SimpleSerializedObject<>(message.getBody(),
                    byte[].class, (String) payloadType, (String) payloadRevision);

            final Object deserializedObject = serializer.deserialize(serializedPayload);

            if (checkAndGetHeader(headers, EVENT_TYPE_KEY)
                    .equals(EventMessageType.DOMAIN_EVENT_MESSAGE.getTypeByte())) {
                if (headers.containsKey(AGGREGATE_ID_TYPE_KEY)) {
                    try {
                        final Class<?> aggregateIdentifierClass = Class
                                .forName(String.valueOf(headers.get(AGGREGATE_ID_TYPE_KEY)));
                        final Object aggregateIdentifier = objectMapper.readValue(
                                String.valueOf(checkAndGetHeader(headers, AGGREGATE_ID_KEY)),
                                aggregateIdentifierClass);

                        return new GenericDomainEventMessage<>(messageProperties.getMessageId(), timestamp,
                                aggregateIdentifier, (Long) checkAndGetHeader(headers, SEQUENCE_NUMBER_KEY),
                                deserializedObject, toMetadata(messageProperties));
                    } catch (Exception e) {
                        LOGGER.warn("Failed to restore properties of a domain event message", e);
                    }
                }
            }

            return new GenericEventMessage<>(messageProperties.getMessageId(), timestamp, deserializedObject,
                    toMetadata(messageProperties));
        } catch (Exception e) {
            throw new EventBusMessageConversionException(message, e);
        }
    }
}