org.eclipse.hono.service.amqp.RequestResponseEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.service.amqp.RequestResponseEndpoint.java

Source

/**
 * Copyright (c) 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.service.amqp;

import static io.vertx.proton.ProtonHelper.condition;

import java.util.Objects;

import org.apache.qpid.proton.amqp.transport.AmqpError;
import org.apache.qpid.proton.message.Message;
import org.eclipse.hono.auth.HonoUser;
import org.eclipse.hono.config.ServiceConfigProperties;
import org.eclipse.hono.service.auth.AuthorizationService;
import org.eclipse.hono.service.auth.ClaimsBasedAuthorizationService;
import org.eclipse.hono.util.AmqpErrorException;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.MessageHelper;
import org.eclipse.hono.util.ResourceIdentifier;
import org.springframework.beans.factory.annotation.Autowired;

import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.MessageConsumer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import io.vertx.proton.ProtonConnection;
import io.vertx.proton.ProtonDelivery;
import io.vertx.proton.ProtonHelper;
import io.vertx.proton.ProtonQoS;
import io.vertx.proton.ProtonReceiver;
import io.vertx.proton.ProtonSender;

/**
 * An abstract base class for implementing endpoints that implement a request response pattern.
 * <p>
 * It is used e.g. in the implementation of the device registration and the credentials API endpoints.
 * 
 * @param <T> The type of configuration properties this endpoint uses.
 */
public abstract class RequestResponseEndpoint<T extends ServiceConfigProperties> extends AbstractAmqpEndpoint<T> {

    private static final int REQUEST_RESPONSE_ENDPOINT_DEFAULT_CREDITS = 20;

    private int receiverLinkCredit = REQUEST_RESPONSE_ENDPOINT_DEFAULT_CREDITS;
    private AuthorizationService authorizationService = new ClaimsBasedAuthorizationService();

    /**
     * Creates an endpoint for a Vertx instance.
     *
     * @param vertx The Vertx instance to use.
     * @throws NullPointerException if vertx is {@code null};
     */
    protected RequestResponseEndpoint(final Vertx vertx) {
        super(Objects.requireNonNull(vertx));
    }

    /**
     * Processes an AMQP message received from a client.
     *
     * @param request The Message to process. Must not be null.
     * @param targetAddress The address the message is sent to.
     * @param clientPrincipal The principal representing the client identity and its authorities.
     * @throws DecodeException if the message's payload does not contain a valid JSON string.
     * @throws NullPointerException if message is {@code null}.
     */
    public abstract void processRequest(final Message request, final ResourceIdentifier targetAddress,
            final HonoUser clientPrincipal);

    /**
     * Construct an AMQP reply message that is send back to the caller.
     *
     * @param message The Message as JsonObject from which a reply message is constructed. Must not be null.
     * @return Message The reply message that shall be sent to the client.
     * @throws NullPointerException If message is null.
     */
    protected abstract Message getAmqpReply(final io.vertx.core.eventbus.Message<JsonObject> message);

    /**
     * Gets the number of message credits this endpoint grants as a receiver.
     *
     * @return The number of credits granted.
     */
    public final int getReceiverLinkCredit() {
        return receiverLinkCredit;
    }

    /**
     * Sets the number of message credits this endpoint grants as a receiver.
     * They are replenished automatically after messages are processed.
        
     * @param receiverLinkCredit The number of credits to grant.
     * @throws IllegalArgumentException if the credit is &lt;= 0.
     */
    public final void setReceiverLinkCredit(final int receiverLinkCredit) {
        if (receiverLinkCredit <= 0) {
            throw new IllegalArgumentException("receiver link credit must be at least 1");
        }
        this.receiverLinkCredit = receiverLinkCredit;
    }

    /**
     * Gets the object to use for making authorization decisions.
     * 
     * @return The service.
     */
    public final AuthorizationService getAuthorizationService() {
        return authorizationService;
    }

    /**
     * Sets the object to use for making authorization decisions.
     * <p>
     * If not set a {@link ClaimsBasedAuthorizationService} instance is used.
     * 
     * @param authService The service.
     */
    @Autowired(required = false)
    public final void setAuthorizationService(final AuthorizationService authService) {
        this.authorizationService = authService;
    }

    /**
     * Configure and check the receiver link of the endpoint.
     * The remote link of the receiver must not demand the AT_MOST_ONCE QoS (not supported).
     * The receiver link itself is configured with the AT_LEAST_ONCE QoS and grants the configured credits ({@link #setReceiverLinkCredit(int)})
     * with autoAcknowledge.
     * <p>
     * Handling of received messages is delegated to {@link #handleMessage(ProtonConnection, ProtonReceiver, ResourceIdentifier, ProtonDelivery, Message)}.
     *
     * @param con The AMQP connection that the link is part of.
     * @param receiver The ProtonReceiver that has already been created for this endpoint.
     * @param targetAddress The resource identifier for this endpoint (see {@link ResourceIdentifier} for details).
     */
    @Override
    public final void onLinkAttach(final ProtonConnection con, final ProtonReceiver receiver,
            final ResourceIdentifier targetAddress) {
        if (ProtonQoS.AT_MOST_ONCE.equals(receiver.getRemoteQoS())) {
            logger.debug("client wants to use AT MOST ONCE delivery mode for {} endpoint, this is not supported.",
                    getName());
            receiver.setCondition(
                    condition(AmqpError.PRECONDITION_FAILED.toString(), "endpoint requires AT_LEAST_ONCE QoS"));
            receiver.close();
        } else {

            logger.debug("establishing link for receiving messages from client [{}]",
                    MessageHelper.getLinkName(receiver));
            receiver.setQoS(ProtonQoS.AT_LEAST_ONCE).setAutoAccept(true) // settle received messages if the handler succeeds
                    .setPrefetch(receiverLinkCredit).handler((delivery, message) -> {
                        handleMessage(con, receiver, targetAddress, delivery, message);
                    }).closeHandler(clientDetached -> onLinkDetach(clientDetached.result())).open();
        }
    }

    /**
     * Handles a request message received from a client.
     * <p>
     * The message gets rejected if
     * <ul>
     * <li>the message does not pass {@linkplain #passesFormalVerification(ResourceIdentifier, Message) formal verification}
     * or</li>
     * <li>the client is not {@linkplain #isAuthorized(HonoUser, ResourceIdentifier, String) authorized to execute the operation}
     * indicated by the message's <em>subject</em> or</li>
     * <li>its payload cannot be parsed</li>
     * </ul>
     * 
     * @param con The connection with the client.
     * @param receiver The link over which the message has been received.
     * @param targetAddress The address the message is sent to.
     * @param delivery The message's delivery status.
     * @param message The message.
     */
    protected final void handleMessage(final ProtonConnection con, final ProtonReceiver receiver,
            final ResourceIdentifier targetAddress, ProtonDelivery delivery, Message message) {

        Future<Void> requestTracker = Future.future();
        requestTracker.setHandler(s -> {
            if (s.succeeded()) {
                ProtonHelper.accepted(delivery, true);
            } else if (s.cause() instanceof AmqpErrorException) {
                AmqpErrorException cause = (AmqpErrorException) s.cause();
                MessageHelper.rejected(delivery, cause.asErrorCondition());
            } else {
                logger.debug("error processing request [resource: {}, op: {}]: {}", targetAddress,
                        message.getSubject(), s.cause().getMessage());
                MessageHelper.rejected(delivery,
                        ProtonHelper.condition(AmqpError.INTERNAL_ERROR, "internal error"));
            }
        });

        if (passesFormalVerification(targetAddress, message)) {

            final HonoUser clientPrincipal = Constants.getClientPrincipal(con);
            isAuthorized(clientPrincipal, targetAddress, message.getSubject()).compose(authorized -> {
                logger.debug("client [{}] is {}authorized to {}:{}", clientPrincipal.getName(),
                        authorized ? "" : "not ", targetAddress, message.getSubject());
                if (authorized) {
                    try {
                        processRequest(message, targetAddress, Constants.getClientPrincipal(con));
                        requestTracker.complete();
                    } catch (DecodeException e) {
                        requestTracker.fail(new AmqpErrorException(AmqpError.DECODE_ERROR, "malformed payload"));
                    }
                } else {
                    requestTracker.fail(new AmqpErrorException(AmqpError.UNAUTHORIZED_ACCESS, "unauthorized"));
                }
            }, requestTracker);
        } else {
            requestTracker.fail(new AmqpErrorException(AmqpError.DECODE_ERROR, "malformed payload"));
        }
    }

    /**
     * Checks if the client is authorized to execute a given operation.
     * 
     * This method is invoked for every request message received from a client.
     * <p>
     * This default implementation simply delegates to {@link AuthorizationService#isAuthorized(HonoUser, ResourceIdentifier, String)}.
     * <p>
     * Subclasses may override this method in order to do more sophisticated checks.
     * 
     * @param clientPrincipal The client.
     * @param resource The resource the operation belongs to.
     * @param operation The operation.
     * @return The outcome of the check.
     */
    protected Future<Boolean> isAuthorized(final HonoUser clientPrincipal, final ResourceIdentifier resource,
            final String operation) {
        return getAuthorizationService().isAuthorized(clientPrincipal, resource, operation);
    }

    /**
     * Configure and check the sender link of the endpoint.
     * The sender link is used for the response to a received request and is driven by the vertx event bus.
     * It listens to the provided resource identifier of the endpoint as vertx event address and then sends the
     * constructed response.
     * Since the response is endpoint specific, it is an abstract method {@link #getAmqpReply(io.vertx.core.eventbus.Message)} and needs to be implemented
     * by the subclass.
     *
     * @param con The AMQP connection that the link is part of.
     * @param sender The ProtonSender that has already been created for this endpoint.
     * @param replyToAddress The resource identifier for the responses of this endpoint (see {@link ResourceIdentifier} for details).
     *                      Note that the reply address is different for each client and is passed in during link creation.
     */
    @Override
    public final void onLinkAttach(final ProtonConnection con, final ProtonSender sender,
            final ResourceIdentifier replyToAddress) {
        if (replyToAddress.getResourceId() == null) {
            logger.debug(
                    "link target provided in client's link ATTACH must not be null, but must match pattern \"{}/<tenant>/<reply-address>\" instead",
                    getName());
            sender.setCondition(condition(AmqpError.INVALID_FIELD.toString(), String.format(
                    "link target must not be null but must have the following format %s/<tenant>/<reply-address>",
                    getName())));
            sender.close();
        } else {
            logger.debug("establishing sender link with client [{}]", MessageHelper.getLinkName(sender));
            final MessageConsumer<JsonObject> replyConsumer = vertx.eventBus().consumer(replyToAddress.toString(),
                    message -> {
                        // TODO check for correct session here...?
                        logger.trace("forwarding reply to client: {}", message.body());
                        final Message amqpReply = getAmqpReply(message);
                        sender.send(amqpReply);
                    });

            sender.closeHandler(senderClosed -> {
                replyConsumer.unregister();
                senderClosed.result().close();
                final String linkName = MessageHelper.getLinkName(sender);
                logger.debug("receiver closed link [{}], removing associated event bus consumer [{}]", linkName,
                        replyConsumer.address());
            });

            sender.setQoS(ProtonQoS.AT_LEAST_ONCE).open();
        }
    }
}