ServiceProxy.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.proxy;
import com.lucimber.dbus.annotation.DBusInterface;
import com.lucimber.dbus.annotation.DBusMethod;
import com.lucimber.dbus.connection.Connection;
import com.lucimber.dbus.message.InboundError;
import com.lucimber.dbus.message.InboundMessage;
import com.lucimber.dbus.message.InboundMethodReturn;
import com.lucimber.dbus.message.OutboundMethodCall;
import com.lucimber.dbus.type.DBusObjectPath;
import com.lucimber.dbus.type.DBusString;
import com.lucimber.dbus.type.DBusType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
/**
* Factory for creating dynamic proxies for D-Bus service clients.
*
* <p>This class simplifies D-Bus method calls by allowing you to define a Java interface with
* annotations and automatically handling the underlying D-Bus communication.
*
* <p><strong>Note:</strong> ServiceProxy is designed for simple client-side request/response
* scenarios only. It does not support:
*
* <ul>
* <li>Receiving D-Bus signals
* <li>Implementing D-Bus services (use {@link
* com.lucimber.dbus.annotation.StandardInterfaceHandler} instead)
* <li>Complex argument marshalling (currently limited to methods without arguments)
* </ul>
*
* <p><strong>Relationship to StandardInterfaceHandler:</strong>
*
* <ul>
* <li>{@code ServiceProxy} - Client-side proxy for calling remote D-Bus services
* <li>{@code StandardInterfaceHandler} - Server-side handler for implementing D-Bus services
* </ul>
*
* <p>Example usage:
*
* <pre>{@code
* @DBusInterface("org.freedesktop.DBus")
* public interface DBusService {
* @DBusMethod("ListNames")
* CompletableFuture<String[]> listNames();
*
* @DBusMethod("GetId")
* String getId();
* }
*
* // Create proxy
* DBusService service = ServiceProxy.create(
* connection,
* "org.freedesktop.DBus",
* "/org/freedesktop/DBus",
* DBusService.class
* );
*
* // Use it - synchronous
* String id = service.getId();
*
* // Or asynchronous
* service.listNames().thenAccept(names -> {
* for (String name : names) {
* System.out.println(name);
* }
* });
* }</pre>
*/
public final class ServiceProxy {
private ServiceProxy() {
// Factory class
}
/**
* Creates a proxy instance for the specified D-Bus service interface.
*
* @param connection the D-Bus connection
* @param destination the D-Bus service name (e.g., "org.freedesktop.DBus")
* @param objectPath the object path (e.g., "/org/freedesktop/DBus")
* @param interfaceClass the Java interface class with D-Bus annotations
* @param <T> the interface type
* @return a proxy instance implementing the interface
*/
@SuppressWarnings("unchecked")
public static <T> T create(
final Connection connection,
final String destination,
final String objectPath,
final Class<T> interfaceClass) {
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("Class must be an interface");
}
DBusInterface dbusInterface = interfaceClass.getAnnotation(DBusInterface.class);
if (dbusInterface == null) {
throw new IllegalArgumentException("Interface must be annotated with @DBusInterface");
}
InvocationHandler handler =
new DBusInvocationHandler(
connection, destination, objectPath, dbusInterface.value());
return (T)
Proxy.newProxyInstance(
interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, handler);
}
/**
* Creates a proxy with automatic interface name detection. Uses the @DBusInterface annotation
* value as the destination.
*
* @param connection the D-Bus connection
* @param objectPath the object path
* @param interfaceClass the Java interface class with D-Bus annotations
* @param <T> the interface type
* @return a proxy instance implementing the interface
*/
public static <T> T create(
final Connection connection, final String objectPath, final Class<T> interfaceClass) {
DBusInterface dbusInterface = interfaceClass.getAnnotation(DBusInterface.class);
if (dbusInterface == null) {
throw new IllegalArgumentException("Interface must be annotated with @DBusInterface");
}
return create(connection, dbusInterface.value(), objectPath, interfaceClass);
}
private static class DBusInvocationHandler implements InvocationHandler {
private final Connection connection;
private final String destination;
private final String objectPath;
private final String interfaceName;
DBusInvocationHandler(
final Connection connection,
final String destination,
final String objectPath,
final String interfaceName) {
this.connection = connection;
this.destination = destination;
this.objectPath = objectPath;
this.interfaceName = interfaceName;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Handle Object methods
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
DBusMethod dbusMethod = method.getAnnotation(DBusMethod.class);
if (dbusMethod == null) {
throw new UnsupportedOperationException(
"Method " + method.getName() + " is not annotated with @DBusMethod");
}
String methodName = dbusMethod.name().isEmpty() ? method.getName() : dbusMethod.name();
// Build the D-Bus method call
OutboundMethodCall.Builder callBuilder =
OutboundMethodCall.Builder.create()
.withPath(DBusObjectPath.valueOf(objectPath))
.withInterface(DBusString.valueOf(interfaceName))
.withMember(DBusString.valueOf(methodName))
.withDestination(DBusString.valueOf(destination))
.withReplyExpected(true);
// TODO: Add argument marshalling based on method parameters
// For now, this handles methods without arguments
OutboundMethodCall call = callBuilder.build();
// Determine if method returns CompletableFuture/CompletionStage
Class<?> returnType = method.getReturnType();
if (CompletableFuture.class.isAssignableFrom(returnType)
|| CompletionStage.class.isAssignableFrom(returnType)) {
// Return the future directly
return connection
.sendRequest(call)
.thenApply(response -> unmarshalResponse(response, method))
.toCompletableFuture();
} else {
// Synchronous call - block and wait
InboundMessage response =
connection
.sendRequest(call)
.toCompletableFuture()
.get(30, TimeUnit.SECONDS);
return unmarshalResponse(response, method);
}
}
private Object unmarshalResponse(InboundMessage response, Method method) {
if (response instanceof InboundError) {
InboundError error = (InboundError) response;
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 DBusException(
"D-Bus error: " + error.getErrorName() + " - " + errorMessage);
}
if (response instanceof InboundMethodReturn) {
InboundMethodReturn methodReturn = (InboundMethodReturn) response;
List<DBusType> payload = methodReturn.getPayload();
// TODO: Implement proper unmarshalling based on method return type
// For now, return null for void methods
if (method.getReturnType() == void.class || method.getReturnType() == Void.class) {
return null;
}
// Simple case: single return value
if (!payload.isEmpty()) {
DBusType value = payload.get(0);
// TODO: Convert D-Bus type to Java type based on method signature
return value;
}
}
return null;
}
}
/** Exception thrown when D-Bus operations fail. */
public static class DBusException extends RuntimeException {
public DBusException(String message) {
super(message);
}
public DBusException(String message, Throwable cause) {
super(message, cause);
}
}
}