DBusPromise.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.util;
import com.lucimber.dbus.message.InboundError;
import com.lucimber.dbus.message.InboundMessage;
import com.lucimber.dbus.message.InboundMethodReturn;
import com.lucimber.dbus.type.DBusString;
import com.lucimber.dbus.type.DBusType;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Promise-style utilities for working with D-Bus asynchronous operations.
*
* <p>This class provides a fluent API for handling D-Bus responses with better error handling and
* type conversion.
*
* <p>Example usage:
*
* <pre>{@code
* DBusPromise.from(connection.sendRequest(methodCall))
* .timeout(Duration.ofSeconds(5))
* .mapReturn(payload -> payload.get(0))
* .as(DBusString.class)
* .thenAccept(result -> System.out.println("Result: " + result))
* .exceptionally(error -> {
* System.err.println("Failed: " + error.getMessage());
* return null;
* });
* }</pre>
*/
public class DBusPromise<T> {
private final CompletionStage<T> stage;
private DBusPromise(CompletionStage<T> stage) {
this.stage = stage;
}
/**
* Creates a DBusPromise from a D-Bus message completion stage.
*
* @param messageStage the completion stage from a D-Bus request
* @return a new DBusPromise
*/
public static DBusPromise<InboundMessage> from(CompletionStage<InboundMessage> messageStage) {
return new DBusPromise<>(messageStage);
}
/**
* Creates a DBusPromise from a value.
*
* @param value the value
* @param <U> the value type
* @return a completed DBusPromise
*/
public static <U> DBusPromise<U> completed(U value) {
return new DBusPromise<>(CompletableFuture.completedFuture(value));
}
/**
* Creates a failed DBusPromise.
*
* @param error the error
* @param <U> the value type
* @return a failed DBusPromise
*/
public static <U> DBusPromise<U> failed(Throwable error) {
CompletableFuture<U> future = new CompletableFuture<>();
future.completeExceptionally(error);
return new DBusPromise<>(future);
}
/**
* Applies a timeout to the operation.
*
* @param duration the timeout duration
* @return a new DBusPromise with timeout
*/
public DBusPromise<T> timeout(Duration duration) {
CompletableFuture<T> future = stage.toCompletableFuture();
CompletableFuture<T> timeoutFuture = new CompletableFuture<>();
future.whenComplete(
(result, error) -> {
if (error != null) {
timeoutFuture.completeExceptionally(error);
} else {
timeoutFuture.complete(result);
}
});
// Schedule timeout
CompletableFuture.delayedExecutor(duration.toMillis(), TimeUnit.MILLISECONDS)
.execute(
() ->
timeoutFuture.completeExceptionally(
new DBusTimeoutException(
"Operation timed out after " + duration)));
return new DBusPromise<>(timeoutFuture);
}
/**
* Maps the result using a function.
*
* @param mapper the mapping function
* @param <U> the new type
* @return a new DBusPromise with the mapped value
*/
public <U> DBusPromise<U> map(Function<? super T, ? extends U> mapper) {
return new DBusPromise<>(stage.thenApply(mapper));
}
/**
* Maps an InboundMessage to its return payload. Automatically handles error responses.
*
* @return a new DBusPromise with the payload
*/
public DBusPromise<List<DBusType>> mapReturn() {
return new DBusPromise<>(
stage.thenApply(
value -> {
if (!(value instanceof InboundMessage)) {
throw new IllegalStateException(
"Expected InboundMessage but got " + value.getClass());
}
InboundMessage message = (InboundMessage) value;
if (message instanceof InboundError) {
InboundError error = (InboundError) message;
String errorMessage = "";
if (error.getPayload() != null && !error.getPayload().isEmpty()) {
DBusType firstArg = error.getPayload().get(0);
if (firstArg instanceof DBusString) {
errorMessage = ((DBusString) firstArg).getDelegate();
}
}
throw new DBusErrorException(
error.getErrorName().toString(), errorMessage);
}
if (message instanceof InboundMethodReturn) {
return ((InboundMethodReturn) message).getPayload();
}
throw new IllegalStateException(
"Unexpected message type: " + message.getClass());
}));
}
/**
* Maps the first element of a payload list.
*
* @param type the expected D-Bus type class
* @param <U> the D-Bus type
* @return a new DBusPromise with the first element
*/
@SuppressWarnings("unchecked")
public <U extends DBusType> DBusPromise<U> firstAs(Class<U> type) {
return map(
value -> {
if (value instanceof List) {
List<?> list = (List<?>) value;
if (!list.isEmpty()) {
Object first = list.get(0);
if (type.isInstance(first)) {
return (U) first;
}
throw new ClassCastException(
"Expected "
+ type.getSimpleName()
+ " but got "
+ first.getClass().getSimpleName());
}
throw new IllegalStateException("Empty payload");
}
throw new IllegalStateException("Expected List but got " + value.getClass());
});
}
/**
* Casts the result to a specific type.
*
* @param type the target type class
* @param <U> the target type
* @return a new DBusPromise with the cast value
*/
@SuppressWarnings("unchecked")
public <U> DBusPromise<U> as(Class<U> type) {
return map(
value -> {
if (type.isInstance(value)) {
return (U) value;
}
throw new ClassCastException(
"Expected "
+ type.getSimpleName()
+ " but got "
+ value.getClass().getSimpleName());
});
}
/**
* Handles both success and failure cases.
*
* @param action the action to perform
* @return a new DBusPromise
*/
public DBusPromise<Void> thenAccept(java.util.function.Consumer<? super T> action) {
return new DBusPromise<>(stage.thenAccept(action));
}
/**
* Handles exceptions.
*
* @param fn the exception handler
* @return a new DBusPromise
*/
public DBusPromise<T> exceptionally(Function<Throwable, ? extends T> fn) {
return new DBusPromise<>(stage.exceptionally(fn));
}
/**
* Converts to a CompletableFuture.
*
* @return the underlying CompletableFuture
*/
public CompletableFuture<T> toCompletableFuture() {
return stage.toCompletableFuture();
}
/**
* Gets the result synchronously.
*
* @return the result
* @throws Exception if the operation fails
*/
public T get() throws Exception {
return stage.toCompletableFuture().get();
}
/**
* Gets the result synchronously with a timeout.
*
* @param timeout the timeout value
* @param unit the timeout unit
* @return the result
* @throws Exception if the operation fails or times out
*/
public T get(long timeout, TimeUnit unit) throws Exception {
return stage.toCompletableFuture().get(timeout, unit);
}
/** Exception thrown when a D-Bus operation times out. */
public static class DBusTimeoutException extends RuntimeException {
public DBusTimeoutException(String message) {
super(message);
}
}
/** Exception thrown when a D-Bus error is received. */
public static class DBusErrorException extends RuntimeException {
private final String errorName;
public DBusErrorException(String errorName, String message) {
super(message);
this.errorName = errorName;
}
public String getErrorName() {
return errorName;
}
}
}