InternalTailHandler.java

/*
 * SPDX-FileCopyrightText: 2023-2025 Lucimber UG
 * SPDX-License-Identifier: Apache-2.0
 */
package com.lucimber.dbus.connection;

import com.lucimber.dbus.message.InboundError;
import com.lucimber.dbus.message.InboundMessage;
import com.lucimber.dbus.message.InboundMethodCall;
import com.lucimber.dbus.message.InboundMethodReturn;
import com.lucimber.dbus.message.OutboundError;
import com.lucimber.dbus.type.DBusSignature;
import com.lucimber.dbus.type.DBusString;
import com.lucimber.dbus.type.DBusType;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Internal tail handler that processes messages that reach the end of the pipeline.
 *
 * <p>This handler is automatically added to the tail of every pipeline and serves as a final
 * fallback for unhandled messages. It handles method calls by sending appropriate error replies and
 * logs other unhandled messages.
 *
 * <p>For unhandled method calls that expect a reply, this handler will automatically send a {@code
 * org.freedesktop.DBus.Error.Failed} error response to inform the caller that no handler was able
 * to process the request.
 *
 * @see Pipeline
 * @see Context
 * @since 1.0.0
 */
final class InternalTailHandler extends AbstractDuplexHandler
        implements InboundHandler, OutboundHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(InternalTailHandler.class);
    private static final DBusString NOT_HANDLED_ERROR =
            DBusString.valueOf("org.freedesktop.DBus.Error.Failed");

    @Override
    protected Logger getLogger() {
        return LOGGER;
    }

    /**
     * Handles inbound messages that reach the end of the pipeline.
     *
     * <p>This method processes different types of messages:
     *
     * <ul>
     *   <li>Method calls expecting a reply: sends an error response
     *   <li>Method calls not expecting a reply: logs and ignores
     *   <li>Method returns and errors: logs as warnings since they should have been handled
     *   <li>Signals and other messages: logs and ignores
     * </ul>
     *
     * @param ctx the {@link Context} this handler is bound to
     * @param msg the {@link InboundMessage} being processed
     * @since 1.0.0
     */
    @Override
    public void handleInboundMessage(Context ctx, InboundMessage msg) {
        Objects.requireNonNull(msg);

        if (msg instanceof InboundMethodCall methodCall) {
            if (methodCall.isReplyExpected()) {
                sendErrorReply(ctx, methodCall);
            } else {
                LOGGER.debug(
                        "Unhandled InboundMethodCall without reply expectation: {}", methodCall);
            }
        } else if (msg instanceof InboundMethodReturn || msg instanceof InboundError) {
            LOGGER.warn("Received unhandled reply message that was not intercepted: {}", msg);
        } else {
            LOGGER.debug("Ignoring unhandled inbound signal or unknown message: {}", msg);
        }
    }

    /**
     * Sends an error reply for an unhandled method call.
     *
     * <p>This method constructs a standard D-Bus error response indicating that no handler was able
     * to process the method call.
     *
     * @param ctx the {@link Context} this handler is bound to
     * @param call the {@link InboundMethodCall} that was not handled
     * @since 1.0.0
     */
    private void sendErrorReply(Context ctx, InboundMethodCall call) {
        LOGGER.debug("Sending error reply for unhandled method call: {}", call);

        DBusSignature signature = DBusSignature.valueOf("s");
        List<DBusType> payload =
                List.of(DBusString.valueOf("No handler was able to process the request."));

        OutboundError error =
                OutboundError.Builder.create()
                        .withSerial(ctx.getConnection().getNextSerial())
                        .withReplySerial(call.getSerial())
                        .withErrorName(NOT_HANDLED_ERROR)
                        .withDestination(call.getSender())
                        .withBody(signature, payload)
                        .build();

        CompletableFuture<Void> future = new CompletableFuture<>();
        future.exceptionally(
                cause -> {
                    LOGGER.error(
                            "Failed to send fallback error reply for serial {}",
                            call.getSerial(),
                            cause);
                    return null;
                });
        ctx.propagateOutboundMessage(error, future);
    }
}