org.eclipse.hono.util.MessageHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.util.MessageHelper.java

Source

/**
 * Copyright (c) 2016, 2017 Bosch Software Innovations GmbH.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bosch Software Innovations GmbH - initial creation
 */
package org.eclipse.hono.util;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Objects;
import java.util.UUID;

import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.UnsignedLong;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.Data;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.transport.ErrorCondition;
import org.apache.qpid.proton.message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import io.vertx.proton.ProtonDelivery;
import io.vertx.proton.ProtonLink;
import io.vertx.proton.impl.ProtonReceiverImpl;
import io.vertx.proton.impl.ProtonSenderImpl;

/**
 * Utility methods for working with Proton {@code Message}s.
 *
 */
public final class MessageHelper {

    /**
     * The name of the AMQP 1.0 message application property containing the id of the device that has reported the data
     * belongs to.
     */
    public static final String APP_PROPERTY_DEVICE_ID = "device_id";
    /**
     * The name of the AMQP 1.0 message application property containing the id of the tenant the device that has
     * reported the data belongs to.
     */
    public static final String APP_PROPERTY_TENANT_ID = "tenant_id";
    /**
     * The name of the AMQP 1.0 message application property containing a JWT token asserting a device's registration status.
     */
    public static final String APP_PROPERTY_REGISTRATION_ASSERTION = "reg_assertion";
    /**
     * The name of the AMQP 1.0 message application property containing the resource a message is addressed at.
     */
    public static final String APP_PROPERTY_RESOURCE = "resource";

    /**
     * The name of the AMQP 1.0 message system property 'subject'.
     */
    public static final String SYS_PROPERTY_SUBJECT = "subject";
    public static final String SYS_PROPERTY_CORRELATION_ID = "correlation-id";

    public static final String ANNOTATION_X_OPT_APP_CORRELATION_ID = "x-opt-app-correlation-id";
    public static final String APP_PROPERTY_STATUS = "status";

    private static final Logger LOG = LoggerFactory.getLogger(MessageHelper.class);

    private MessageHelper() {
    }

    public static String getDeviceId(final Message msg) {
        Objects.requireNonNull(msg);
        return getApplicationProperty(msg.getApplicationProperties(), APP_PROPERTY_DEVICE_ID, String.class);
    }

    public static String getTenantId(final Message msg) {
        Objects.requireNonNull(msg);
        return getApplicationProperty(msg.getApplicationProperties(), APP_PROPERTY_TENANT_ID, String.class);
    }

    /**
     * Gets the registration assertion conveyed in an AMQP 1.0 message.
     * <p>
     * The assertion is expected to be contained in the messages's <em>delivery-annotation</em>
     * under key {@link #APP_PROPERTY_REGISTRATION_ASSERTION}.
     * 
     * @param msg The message.
     * @return The assertion or {@code null} if the message does not contain an assertion (at the
     *         expected location).
     */
    public static String getRegistrationAssertion(final Message msg) {
        return getRegistrationAssertion(msg, false);
    }

    /**
     * Gets and removes the registration assertion conveyed in an AMQP 1.0 message.
     * <p>
     * The assertion is expected to be contained in the messages's <em>delivery-annotation</em>
     * under key {@link #APP_PROPERTY_REGISTRATION_ASSERTION}.
     * 
     * @param msg The message.
     * @return The assertion or {@code null} if the message does not contain an assertion (at the
     *         expected location).
     */
    public static String getAndRemoveRegistrationAssertion(final Message msg) {
        return getRegistrationAssertion(msg, true);
    }

    private static String getRegistrationAssertion(final Message msg, final boolean removeAssertion) {
        Objects.requireNonNull(msg);
        String assertion = null;
        ApplicationProperties properties = msg.getApplicationProperties();
        if (properties != null) {
            Object obj = null;
            if (removeAssertion) {
                obj = properties.getValue().remove(APP_PROPERTY_REGISTRATION_ASSERTION);
            } else {
                obj = properties.getValue().get(APP_PROPERTY_REGISTRATION_ASSERTION);
            }
            if (obj instanceof String) {
                assertion = (String) obj;
            }
        }
        return assertion;
    }

    public static String getDeviceIdAnnotation(final Message msg) {
        Objects.requireNonNull(msg);
        return getAnnotation(msg, APP_PROPERTY_DEVICE_ID, String.class);
    }

    public static String getTenantIdAnnotation(final Message msg) {
        Objects.requireNonNull(msg);
        return getAnnotation(msg, APP_PROPERTY_TENANT_ID, String.class);
    }

    /**
     * Gets the value of the {@code x-opt-appl-correlation-id} annotation from a message.
     * 
     * @param msg the message to get the annotation from.
     * @return the value of the annotation (if present) or {@code false} if the message
     *         does not contain the annotation.
     */
    public static boolean getXOptAppCorrelationId(final Message msg) {
        Objects.requireNonNull(msg);
        Boolean value = getAnnotation(msg, ANNOTATION_X_OPT_APP_CORRELATION_ID, Boolean.class);
        return value == null ? false : value;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getApplicationProperty(final ApplicationProperties props, final String name,
            final Class<T> type) {
        if (props == null) {
            return null;
        } else {
            Object value = props.getValue().get(name);
            if (type.isInstance(value)) {
                return (T) value;
            } else {
                return null;
            }
        }
    }

    /**
     * Parses a message's body into a JSON object.
     * 
     * @param msg The AMQP 1.0 message to parse the body of.
     * @return The message body parsed into a JSON object or {@code null} if the message does not have a
     *         <em>Data</em> nor an <em>AmqpValue</em> section.
     * @throws NullPointerException if the message is {@code null}.
     * @throws DecodeException if the payload cannot be parsed into a JSON object.
     */
    public static JsonObject getJsonPayload(final Message msg) {

        final String payload = getPayload(msg);
        return (payload != null ? new JsonObject(payload) : null);
    }

    /**
     * Gets a message's body as String object that can be used for constructing a JsonObject or bind a POJO using
     * jackson-databind e.g.
     *
     * @param msg The AMQP 1.0 message to parse the body of.
     * @return The message body parsed into a JSON object or {@code null} if the message does not have a <em>Data</em>
     *         nor an <em>AmqpValue</em> section.
     * @throws NullPointerException if the message is {@code null}.
     */
    public static String getPayload(final Message msg) {

        Objects.requireNonNull(msg);
        if (msg.getBody() == null) {
            LOG.debug("message has no body");
            return null;
        }

        if (msg.getBody() instanceof Data) {
            Data body = (Data) msg.getBody();
            return new String(body.getValue().getArray(), StandardCharsets.UTF_8);
        } else if (msg.getBody() instanceof AmqpValue) {
            AmqpValue body = (AmqpValue) msg.getBody();
            if (body.getValue() instanceof String) {
                return (String) body.getValue();
            }
        }

        LOG.debug("unsupported body type [{}]", msg.getBody().getClass().getName());
        return null;
    }

    public static void addTenantId(final Message msg, final String tenantId) {
        addProperty(msg, APP_PROPERTY_TENANT_ID, tenantId);
    }

    public static void addDeviceId(final Message msg, final String deviceId) {
        addProperty(msg, APP_PROPERTY_DEVICE_ID, deviceId);
    }

    /**
     * Adds a registration assertion to an AMQP 1.0 message.
     * <p>
     * The assertion is put to the message's <em>application-properties</em> under key
     * {@link #APP_PROPERTY_REGISTRATION_ASSERTION}.
     * 
     * @param msg The message.
     * @param token The assertion to add.
     */
    public static void addRegistrationAssertion(final Message msg, final String token) {
        addProperty(msg, APP_PROPERTY_REGISTRATION_ASSERTION, token);
    }

    /**
     * Adds a property to an AMQP 1.0 message.
     * <p>
     * The property is added to the message's <em>application-properties</em>.
     * 
     * @param msg The message.
     * @param key The property key.
     * @param value The property value.
     */
    @SuppressWarnings("unchecked")
    public static void addProperty(final Message msg, final String key, final Object value) {
        ApplicationProperties props = msg.getApplicationProperties();
        if (props == null) {
            props = new ApplicationProperties(new HashMap<String, Object>());
            msg.setApplicationProperties(props);
        }
        props.getValue().put(key, value);
    }

    /**
     * Sets an AMQP 1.0 message's delivery state to <em>rejected</em>.
     * 
     * @param delivery The message's delivery object.
     * @param error The error condition to set as the reason for rejecting the message.
     */
    public static void rejected(final ProtonDelivery delivery, final ErrorCondition error) {
        final Rejected rejected = new Rejected();
        rejected.setError(error);
        delivery.disposition(rejected, true);
    }

    /**
     * Adds several AMQP 1.0 message <em>annotations</em> to the given message that are used to process/route the message.
     * <p>
     * In particular, the following annotations are added:
     * <ul>
     * <li>{@link #APP_PROPERTY_DEVICE_ID} - the ID of the device that reported the data.</li>
     * <li>{@link #APP_PROPERTY_TENANT_ID} - the ID of the tenant as indicated by the link target's second segment.</li>
     * <li>{@link #APP_PROPERTY_RESOURCE} - the full resource path including the endpoint, the tenant and the device ID.</li>
     * </ul>
     *
     * @param msg the message to add the message annotations to.
     * @param resourceIdentifier the resource identifier that will be added as annotation.
     */
    public static void annotate(final Message msg, final ResourceIdentifier resourceIdentifier) {
        MessageHelper.addAnnotation(msg, APP_PROPERTY_TENANT_ID, resourceIdentifier.getTenantId());
        MessageHelper.addAnnotation(msg, APP_PROPERTY_DEVICE_ID, resourceIdentifier.getResourceId());
        MessageHelper.addAnnotation(msg, APP_PROPERTY_RESOURCE, resourceIdentifier.toString());
    }

    /**
     * Adds a value for a symbol to an AMQP 1.0 message's <em>annotations</em>.
     * 
     * @param msg the message to add the symbol to.
     * @param key the name of the symbol to add a value for.
     * @param value the value to add.
     */
    public static void addAnnotation(final Message msg, final String key, final Object value) {
        MessageAnnotations annotations = msg.getMessageAnnotations();
        if (annotations == null) {
            annotations = new MessageAnnotations(new HashMap<>());
            msg.setMessageAnnotations(annotations);
        }
        annotations.getValue().put(Symbol.getSymbol(key), value);
    }

    /**
     * Returns the value to which the specified key is mapped in the message annotations,
     * or {@code null} if the message annotations contain no mapping for the key.
     *
     * @param <T> the expected type of the property to read.
     * @param msg the message that contains the annotations.
     * @param key the name of the symbol to return a value for.
     * @param type the expected type of the value.
     * @return the annotation's value or {@code null} if no such annotation exists or its value
     *          is not of the expected type.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAnnotation(final Message msg, final String key, final Class<T> type) {
        MessageAnnotations annotations = msg.getMessageAnnotations();
        if (annotations == null) {
            return null;
        } else {
            Object value = annotations.getValue().get(Symbol.getSymbol(key));
            if (type.isInstance(value)) {
                return (T) value;
            } else {
                return null;
            }
        }
    }

    public static String getLinkName(final ProtonLink<?> link) {
        if (link instanceof ProtonReceiverImpl) {
            return ((ProtonReceiverImpl) link).getName();
        } else if (link instanceof ProtonSenderImpl) {
            return ((ProtonSenderImpl) link).getName();
        } else {
            return "unknown";
        }
    }

    /**
     * Encodes the given ID object to JSON representation. Supported types for AMQP 1.0 correlation/messageIds are
     * String, UnsignedLong, UUID and Binary.
     * @param id the id to encode to JSON
     * @return a JsonObject containing the JSON represenatation
     * @throws IllegalArgumentException if the type is not supported
     */
    public static JsonObject encodeIdToJson(final Object id) {

        final JsonObject json = new JsonObject();
        if (id instanceof String) {
            json.put("type", "string");
            json.put("id", id);
        } else if (id instanceof UnsignedLong) {
            json.put("type", "ulong");
            json.put("id", id.toString());
        } else if (id instanceof UUID) {
            json.put("type", "uuid");
            json.put("id", id.toString());
        } else if (id instanceof Binary) {
            json.put("type", "binary");
            final Binary binary = (Binary) id;
            json.put("id", Base64.getEncoder().encodeToString(binary.getArray()));
        } else {
            throw new IllegalArgumentException("type " + id.getClass().getName() + " is not supported");
        }
        return json;
    }

    /**
     * Decodes the given JsonObject to JSON representation.
     * Supported types for AMQP 1.0 correlation/messageIds are String, UnsignedLong, UUID and Binary.
     * @param json JSON representation of an ID
     * @return an ID object of correct type
     */
    public static Object decodeIdFromJson(final JsonObject json) {
        final String type = json.getString("type");
        final String id = json.getString("id");
        switch (type) {
        case "string":
            return id;
        case "ulong":
            return UnsignedLong.valueOf(id);
        case "uuid":
            return UUID.fromString(id);
        case "binary":
            return new Binary(Base64.getDecoder().decode(id));
        default:
            throw new IllegalArgumentException("type " + type + " is not supported");
        }
    }
}