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

Java tutorial

Introduction

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

Source

/**
 * Copyright (c) 2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * https://www.eclipse.org/legal/epl-v10.html
 *
 * SPDX-License-Identifier: EPL-1.0
 */

package org.eclipse.hono.util;

import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.UnsignedLong;
import org.apache.qpid.proton.message.Message;

import io.vertx.core.json.JsonObject;

/**
 * A wrapper around a JSON object which can be used to convey request and/or response
 * information for Hono API operations via the vert.x event bus.
 *
 */
public class EventBusMessage {

    private static final String FIELD_CORRELATION_ID = "correlation-id";
    private static final String FIELD_CORRELATION_ID_TYPE = "correlation-id-type";

    private final JsonObject json;

    private EventBusMessage(final String operation) {
        Objects.requireNonNull(operation);
        this.json = new JsonObject();
        json.put(MessageHelper.SYS_PROPERTY_SUBJECT, operation);
    }

    private EventBusMessage(final JsonObject request) {
        this.json = Objects.requireNonNull(request);
    }

    /**
     * Creates a new (request) message for an operation.
     * 
     * @param operation The name of the operation.
     * @return The request message.
     * @throws NullPointerException if operation is {@code null}.
     */
    public static EventBusMessage forOperation(final String operation) {
        return new EventBusMessage(operation);
    }

    /**
     * Creates a new (request) message from an AMQP 1.0 message.
     * <p>
     * The operation will be determined from the message's
     * <em>subject</em>.
     * 
     * @param message The AMQP message.
     * @return The request message.
     * @throws NullPointerException if message is {@code null}.
     * @throws IllegalArgumentException if the message has no subject set.
     */
    public static EventBusMessage forOperation(final Message message) {
        if (message.getSubject() == null) {
            throw new IllegalArgumentException("message has no subject");
        } else {
            return new EventBusMessage(message.getSubject());
        }
    }

    /**
     * Creates a new (response) message for a status code.
     * 
     * @param status The status code indicating the outcome of the operation.
     * @return The response message.
     */
    public static EventBusMessage forStatusCode(final int status) {
        final EventBusMessage result = new EventBusMessage(new JsonObject());
        result.setProperty(MessageHelper.APP_PROPERTY_STATUS, status);
        return result;
    }

    /**
     * Creates a new response message in reply to this (request) message.
     * <p>
     * This method copies the following properties from the request to
     * the response (if not {@code null}):
     * <ul>
     * <li><em>status</em></li>
     * <li><em>operation</em></li>
     * <li><em>appCorrelationId</em></li>
     * <li><em>correlationId</em></li>
     * <li><em>replyToAddress</em></li>
     * <li><em>tenant</em></li>
     * </ul>
     * 
     * @param status The status code indicating the outcome of the operation.
     * @return The response message.
     * @throws NullPointerException if request is {@code null}.
     */
    public EventBusMessage getResponse(final int status) {

        final EventBusMessage reply = forStatusCode(status);
        reply.setProperty(MessageHelper.SYS_PROPERTY_SUBJECT, getProperty(MessageHelper.SYS_PROPERTY_SUBJECT));
        reply.setProperty(MessageHelper.ANNOTATION_X_OPT_APP_CORRELATION_ID,
                getProperty(MessageHelper.ANNOTATION_X_OPT_APP_CORRELATION_ID));
        reply.setProperty(MessageHelper.SYS_PROPERTY_CORRELATION_ID,
                getProperty(MessageHelper.SYS_PROPERTY_CORRELATION_ID));
        reply.setReplyToAddress(getReplyToAddress());
        reply.setTenant(getTenant());
        return reply;
    }

    /**
     * Creates a new message from a JSON object.
     * <p>
     * Whether the created message represents a request or a response
     * is determined by the <em>status</em> and <em>operation</em> properties.
     * 
     * @param json The JSON object.
     * @return The message.
     */
    public static EventBusMessage fromJson(final JsonObject json) {
        return new EventBusMessage(Objects.requireNonNull(json));
    }

    /**
     * Checks if this (response) message has all properties required
     * for successful delivery to the client.
     * 
     * @return {@code true} if this message has {@code non-null} values for
     *         properties <em>operation</em>, <em>replyToAddress</em> and
     *         <em>correlationId</em>.
     */
    public boolean hasResponseProperties() {
        return getOperation() != null && getReplyToAddress() != null
                && getProperty(MessageHelper.SYS_PROPERTY_CORRELATION_ID) != null;
    }

    /**
     * Gets the operation to invoke.
     * 
     * @return The operation of {@code null} if this is a response message.
     */
    public String getOperation() {
        return getProperty(MessageHelper.SYS_PROPERTY_SUBJECT);
    }

    /**
     * Gets the status code indicating the outcome of the invocation
     * of the operation.
     * 
     * @return The status code or {@code null} if this is a request message.
     */
    public Integer getStatus() {
        return getProperty(MessageHelper.APP_PROPERTY_STATUS);
    }

    /**
     * Adds a property for the address that responses to
     * this (request) message should be sent.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param address The address.
     * @return This message for chaining.
     */
    public EventBusMessage setReplyToAddress(final String address) {
        setProperty(MessageHelper.SYS_PROPERTY_REPLY_TO, address);
        return this;
    }

    /**
     * Adds a property for the address that responses to
     * this (request) message should be sent.
     * <p>
     * The property will only be added if the AMQP message contains
     * a non-{@code null} <em>reply-to</em> property.
     * 
     * @param msg The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setReplyToAddress(final Message msg) {
        setReplyToAddress(msg.getReplyTo());
        return this;
    }

    /**
     * Gets the value of the reply-to address property.
     * 
     * @return The value or {@code null} if not set.
     */
    public String getReplyToAddress() {
        return getProperty(MessageHelper.SYS_PROPERTY_REPLY_TO);
    }

    /**
     * Adds a property for the tenant identifier.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param tenantId The tenant identifier.
     * @return This message for chaining.
     */
    public EventBusMessage setTenant(final String tenantId) {
        setProperty(MessageHelper.APP_PROPERTY_TENANT_ID, tenantId);
        return this;
    }

    /**
     * Adds a property for the tenant identifier.
     * <p>
     * The property will only be added if the AMQP message contains
     * a non-{@code null} tenant identifier.
     * 
     * @param msg The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setTenant(final Message msg) {
        setTenant(MessageHelper.getTenantId(msg));
        return this;
    }

    /**
     * Gets the value of the tenant identifier property.
     * 
     * @return The value or {@code null} if not set.
     */
    public String getTenant() {
        return getProperty(MessageHelper.APP_PROPERTY_TENANT_ID);
    }

    /**
     * Adds a property for the device identifier.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param deviceId The device identifier.
     * @return This message for chaining.
     */
    public EventBusMessage setDeviceId(final String deviceId) {
        setProperty(MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId);
        return this;
    }

    /**
     * Adds a property for the device identifier.
     * <p>
     * The property will only be added if the AMQP message contains
     * a non-{@code null} device identifier.
     * 
     * @param msg The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setDeviceId(final Message msg) {
        setDeviceId(MessageHelper.getDeviceId(msg));
        return this;
    }

    /**
     * Gets the value of the device identifier property.
     * 
     * @return The value or {@code null} if not set.
     */
    public String getDeviceId() {
        return getProperty(MessageHelper.APP_PROPERTY_DEVICE_ID);
    }

    /**
     * Adds a property for the request/response payload.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param payload The payload.
     * @return This message for chaining.
     */
    public EventBusMessage setJsonPayload(final JsonObject payload) {
        setProperty(RequestResponseApiConstants.FIELD_PAYLOAD, payload);
        return this;
    }

    /**
     * Adds a property for the request/response payload.
     * <p>
     * The property will only be added if the AMQP message contains
     * a JSON payload.
     * 
     * @param msg The AMQP message to retrieve the payload from.
     * @return This message for chaining.
     */
    public EventBusMessage setJsonPayload(final Message msg) {
        setJsonPayload(MessageHelper.getJsonPayload(msg));
        return this;
    }

    /**
     * Gets the value of the payload property.
     * 
     * @return The value or {@code null} if not set.
     */
    public JsonObject getJsonPayload() {
        return getProperty(RequestResponseApiConstants.FIELD_PAYLOAD);
    }

    /**
     * Gets the value of the payload property.
     * 
     * @param defaultValue The default value.
     * @return The value of the payload property or the given default
     *         value if not set.
     */
    public JsonObject getJsonPayload(final JsonObject defaultValue) {
        final JsonObject payload = getProperty(RequestResponseApiConstants.FIELD_PAYLOAD);
        return Optional.ofNullable(payload).orElse(defaultValue);
    }

    /**
     * Adds a property for the gateway identifier.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param id The gateway identifier.
     * @return This message for chaining.
     */
    public EventBusMessage setGatewayId(final String id) {
        setProperty(MessageHelper.APP_PROPERTY_GATEWAY_ID, id);
        return this;
    }

    /**
     * Adds a property for the gateway identifier.
     * <p>
     * The property will only be added if the AMQP message contains
     * a non-{@code null} gateway identifier.
     * 
     * @param msg The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setGatewayId(final Message msg) {
        setStringProperty(MessageHelper.APP_PROPERTY_GATEWAY_ID, msg);
        return this;
    }

    /**
     * Gets the value of the gateway identifier property.
     * 
     * @return The value or {@code null} if not set.
     */
    public String getGatewayId() {
        return getProperty(MessageHelper.APP_PROPERTY_GATEWAY_ID);
    }

    /**
     * Adds a property for the cache directive.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param directive The cache directive.
     * @return This message for chaining.
     */
    public EventBusMessage setCacheDirective(final CacheDirective directive) {
        if (directive != null) {
            setProperty(MessageHelper.APP_PROPERTY_CACHE_CONTROL, Objects.requireNonNull(directive).toString());
        }
        return this;
    }

    /**
     * Gets the value of the cache directive property.
     * 
     * @return The value or {@code null} if not set.
     */
    public String getCacheDirective() {
        return getProperty(MessageHelper.APP_PROPERTY_CACHE_CONTROL);
    }

    /**
     * Adds a property for the correlation identifier.
     * <p>
     * The value of the property is set
     * <ol>
     * <li>to the AMQP message's correlation identifier, if not {@code null}, or<li>
     * <li>to the AMQP message's message identifier, if not {@code null}.</li>
     * </ol>
     * 
     * @param message The AMQP message to retrieve the value from.
     * @return This message for chaining.
     * @throws IllegalArgumentException if the message doesn't contain a correlation id
     *            nor a message id.
     */
    public EventBusMessage setCorrelationId(final Message message) {
        if (message.getCorrelationId() != null) {
            return setCorrelationId(message.getCorrelationId());
        } else if (message.getMessageId() != null) {
            return setCorrelationId(message.getMessageId());
        } else {
            throw new IllegalArgumentException("message does not contain message-id nor correlation-id");
        }
    }

    /**
     * Adds a property for the correlation identifier.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param id The correlation identifier.
     * @return This message for chaining.
     * @throws IllegalArgumentException if the identifier is neither a {@code String}
     *                 nor an {@code UnsignedLong} nor a {@code UUID} nor a {@code Binary}.
     */
    public EventBusMessage setCorrelationId(final Object id) {
        setProperty(MessageHelper.SYS_PROPERTY_CORRELATION_ID, encodeIdToJson(id));
        return this;
    }

    /**
     * Gets the value of the correlation identifier property.
     * 
     * @return The value or {@code null} if not set.
     */
    public Object getCorrelationId() {
        final JsonObject encodedId = getProperty(MessageHelper.SYS_PROPERTY_CORRELATION_ID);
        if (encodedId == null) {
            return null;
        } else {
            return decodeIdFromJson(encodedId);
        }
    }

    /**
     * Adds a property for the <em>x-opt-app-correlation-id</em> flag.
     * 
     * @param flag The flag.
     * @return This message for chaining.
     */
    public EventBusMessage setAppCorrelationId(final boolean flag) {
        setProperty(MessageHelper.ANNOTATION_X_OPT_APP_CORRELATION_ID, flag);
        return this;
    }

    /**
     * Adds a property for the <em>x-opt-app-correlation-id</em> flag.
     * <p>
     * The property will be set to the value of the corresponding annotation
     * from the AMQP message or to {@code false}, if the message doesn't
     * contain a corresponding annotation.
     * 
     * @param message The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setAppCorrelationId(final Message message) {
        setProperty(MessageHelper.ANNOTATION_X_OPT_APP_CORRELATION_ID,
                MessageHelper.getXOptAppCorrelationId(message));
        return this;
    }

    /**
     * Gets the value of the <em>x-opt-app-correlation-id</em> flag.
     * 
     * @return The value or {@code false} if not set.
     */
    public boolean isAppCorrelationId() {
        final Boolean result = getProperty(MessageHelper.ANNOTATION_X_OPT_APP_CORRELATION_ID);
        return Optional.ofNullable(result).orElse(Boolean.FALSE);
    }

    /**
     * Adds a property with a value.
     * <p>
     * The property will only be added if the value is not {@code null}.
     * 
     * @param name The name of the property.
     * @param value the value to set.
     * @return This message for chaining.
     * @throws NullPointerException if name is {@code null}.
     */
    public EventBusMessage setProperty(final String name, final Object value) {
        Objects.requireNonNull(name);
        if (value != null) {
            json.put(name, value);
        }
        return this;
    }

    /**
     * Adds a property with a value from an AMQP message.
     * <p>
     * The property will only be added if the AMQP message contains
     * a non-{@code null} <em>application property</em> of the given name.
     * 
     * @param name The name of the property.
     * @param msg The AMQP message to retrieve the value from.
     * @return This message for chaining.
     */
    public EventBusMessage setStringProperty(final String name, final Message msg) {
        setProperty(name, MessageHelper.getApplicationProperty(msg.getApplicationProperties(), name, String.class));
        return this;
    }

    /**
     * Gets a property value.
     * 
     * @param key The name of the property.
     * @param <T> The type of the field.
     * @return The property value or {@code null} if no such property exists or is not of the expected type.
     * @throws NullPointerException if key is {@code null}.
     */
    @SuppressWarnings({ "unchecked" })
    public <T> T getProperty(final String key) {

        Objects.requireNonNull(key);

        try {
            return (T) json.getValue(key);
        } catch (ClassCastException e) {
            return null;
        }
    }

    /**
     * Creates a JSON object representation of this message.
     * <p>
     * The {@link #fromJson(JsonObject)} method can be used to create
     * a {@code EventBusMethod} from its JSON representation.
     * 
     * @return The JSOn object.
     */
    public JsonObject toJson() {
        return json.copy();
    }

    /**
     * Serializes a correlation identifier to JSON.
     * <p>
     * Supported types for AMQP 1.0 correlation IDs are
     * {@code String}, {@code UnsignedLong}, {@code UUID} and {@code Binary}.
     * 
     * @param id The identifier to encode.
     * @return The JSON representation of the identifier.
     * @throws NullPointerException if the correlation id is {@code null}.
     * @throws IllegalArgumentException if the type is not supported.
     */
    private static JsonObject encodeIdToJson(final Object id) {

        Objects.requireNonNull(id);

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

    /**
     * Deserializes a correlation identifier from JSON.
     * <p>
     * Supported types for AMQP 1.0 correlation IDs are
     * {@code String}, {@code UnsignedLong}, {@code UUID} and {@code Binary}.
     * 
     * @param json The JSON representation of the identifier.
     * @return The correlation identifier.
     * @throws NullPointerException if the JSON is {@code null}.
     */
    private static Object decodeIdFromJson(final JsonObject json) {
        Objects.requireNonNull(json);

        final String type = json.getString(FIELD_CORRELATION_ID_TYPE);
        final String id = json.getString(FIELD_CORRELATION_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");
        }
    }
}