StandardInterfaceHandler.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.annotation;
import com.lucimber.dbus.connection.AbstractInboundHandler;
import com.lucimber.dbus.connection.Context;
import com.lucimber.dbus.message.InboundMessage;
import com.lucimber.dbus.message.InboundMethodCall;
import com.lucimber.dbus.message.OutboundError;
import com.lucimber.dbus.message.OutboundMethodReturn;
import com.lucimber.dbus.type.DBusDict;
import com.lucimber.dbus.type.DBusSignature;
import com.lucimber.dbus.type.DBusString;
import com.lucimber.dbus.type.DBusType;
import com.lucimber.dbus.type.DBusUInt32;
import com.lucimber.dbus.type.DBusVariant;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler that implements standard D-Bus interfaces using reflection and annotations.
*
* <p>This handler automatically provides server-side implementations for:
*
* <ul>
* <li>org.freedesktop.DBus.Introspectable - generates XML from annotations
* <li>org.freedesktop.DBus.Properties - accesses annotated properties
* <li>org.freedesktop.DBus.Peer - standard implementation
* </ul>
*
* <p><strong>Relationship to ServiceProxy:</strong>
*
* <ul>
* <li>{@code StandardInterfaceHandler} - Server-side handler for implementing D-Bus services
* <li>{@code ServiceProxy} - Client-side proxy for calling remote D-Bus services
* </ul>
*
* <p>Example usage:
*
* <pre>{@code
* @DBusInterface("com.example.MyService")
* public class MyService {
* @DBusProperty
* private String version = "1.0.0";
*
* @DBusMethod
* public String echo(String message) {
* return message;
* }
* }
*
* // Register the handler
* MyService service = new MyService();
* StandardInterfaceHandler handler = new StandardInterfaceHandler(
* "/com/example/MyService", service);
* connection.getPipeline().addLast("standard", handler);
* }</pre>
*/
public class StandardInterfaceHandler extends AbstractInboundHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(StandardInterfaceHandler.class);
private final String objectPath;
private final Object targetObject;
private final Map<String, Map<String, Field>> properties = new ConcurrentHashMap<>();
private final AtomicInteger serialCounter = new AtomicInteger(1);
private String interfaceName;
/**
* Creates a handler for the given object at the specified path.
*
* @param objectPath the D-Bus object path
* @param targetObject the object implementing the interfaces
*/
public StandardInterfaceHandler(String objectPath, Object targetObject) {
this.objectPath = objectPath;
this.targetObject = targetObject;
// Scan annotations
scanAnnotations(targetObject.getClass());
}
@Override
public void handleInboundMessage(Context ctx, InboundMessage msg) {
if (!(msg instanceof InboundMethodCall)) {
ctx.propagateInboundMessage(msg);
return;
}
InboundMethodCall call = (InboundMethodCall) msg;
// Check if this call is for our object
if (!objectPath.equals(call.getObjectPath().toString())) {
ctx.propagateInboundMessage(msg);
return;
}
String interfaceName = call.getInterfaceName().map(DBusString::toString).orElse("");
String memberName = call.getMember().toString();
try {
switch (interfaceName) {
case "org.freedesktop.DBus.Introspectable":
handleIntrospectable(ctx, call, memberName);
break;
case "org.freedesktop.DBus.Properties":
handleProperties(ctx, call, memberName);
break;
case "org.freedesktop.DBus.Peer":
handlePeer(ctx, call, memberName);
break;
default:
// Not handled by this handler
ctx.propagateInboundMessage(msg);
}
} catch (Exception e) {
LOGGER.error("Error handling method call", e);
sendError(ctx, call, "org.freedesktop.DBus.Error.Failed", e.getMessage());
}
}
private void handleIntrospectable(Context ctx, InboundMethodCall call, String memberName) {
if ("Introspect".equals(memberName)) {
String xml = generateIntrospectionXml();
OutboundMethodReturn reply =
OutboundMethodReturn.Builder.create()
.withSerial(DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.withBody(
DBusSignature.valueOf("s"),
Arrays.asList(DBusString.valueOf(xml)))
.build();
sendReply(ctx, reply);
} else {
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.UnknownMethod",
"Unknown method: " + memberName);
}
}
private void handleProperties(Context ctx, InboundMethodCall call, String memberName) {
List<DBusType> args = call.getPayload() != null ? call.getPayload() : Arrays.asList();
switch (memberName) {
case "Get":
if (args.size() >= 2) {
String iface = ((DBusString) args.get(0)).toString();
String prop = ((DBusString) args.get(1)).toString();
try {
DBusVariant value = getPropertyValue(iface, prop);
OutboundMethodReturn reply =
OutboundMethodReturn.Builder.create()
.withSerial(
DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.withBody(DBusSignature.valueOf("v"), Arrays.asList(value))
.build();
sendReply(ctx, reply);
} catch (Exception e) {
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.UnknownProperty",
"Unknown property: " + prop);
}
} else {
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.InvalidArgs",
"Get requires interface and property name");
}
break;
case "GetAll":
if (!args.isEmpty()) {
String iface = ((DBusString) args.get(0)).toString();
try {
Map<String, DBusVariant> allProps = getAllProperties(iface);
DBusDict<DBusString, DBusVariant> result =
new DBusDict<>(DBusSignature.valueOf("a{sv}"));
for (Map.Entry<String, DBusVariant> entry : allProps.entrySet()) {
result.put(DBusString.valueOf(entry.getKey()), entry.getValue());
}
OutboundMethodReturn reply =
OutboundMethodReturn.Builder.create()
.withSerial(
DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.withBody(
DBusSignature.valueOf("a{sv}"),
Arrays.asList(result))
.build();
sendReply(ctx, reply);
} catch (Exception e) {
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.Failed",
"Failed to get properties: " + e.getMessage());
}
} else {
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.InvalidArgs",
"GetAll requires interface name");
}
break;
default:
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.UnknownMethod",
"Unknown method: " + memberName);
}
}
private void handlePeer(Context ctx, InboundMethodCall call, String memberName) {
switch (memberName) {
case "Ping":
OutboundMethodReturn reply =
OutboundMethodReturn.Builder.create()
.withSerial(DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.build();
sendReply(ctx, reply);
break;
case "GetMachineId":
String machineId = getMachineId();
OutboundMethodReturn idReply =
OutboundMethodReturn.Builder.create()
.withSerial(DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.withBody(
DBusSignature.valueOf("s"),
Arrays.asList(DBusString.valueOf(machineId)))
.build();
sendReply(ctx, idReply);
break;
default:
sendError(
ctx,
call,
"org.freedesktop.DBus.Error.UnknownMethod",
"Unknown method: " + memberName);
}
}
private void scanAnnotations(Class<?> clazz) {
DBusInterface ifaceAnnotation = clazz.getAnnotation(DBusInterface.class);
if (ifaceAnnotation != null) {
this.interfaceName = ifaceAnnotation.value();
Map<String, Field> ifaceProps = new HashMap<>();
properties.put(this.interfaceName, ifaceProps);
// Scan fields for properties
for (Field field : clazz.getDeclaredFields()) {
DBusProperty propAnnotation = field.getAnnotation(DBusProperty.class);
if (propAnnotation != null) {
String propName =
propAnnotation.name().isEmpty()
? field.getName()
: propAnnotation.name();
field.setAccessible(true);
ifaceProps.put(propName, field);
}
}
}
}
private DBusVariant getPropertyValue(String iface, String prop) throws Exception {
Map<String, Field> ifaceProps = properties.get(iface);
if (ifaceProps == null) {
throw new IllegalArgumentException("Unknown interface: " + iface);
}
Field field = ifaceProps.get(prop);
if (field == null) {
throw new IllegalArgumentException("Unknown property: " + prop);
}
Object value = field.get(targetObject);
// Simple type conversion - extend as needed
if (value instanceof String) {
return DBusVariant.valueOf(DBusString.valueOf((String) value));
} else if (value instanceof Integer) {
return DBusVariant.valueOf(com.lucimber.dbus.type.DBusInt32.valueOf((Integer) value));
} else if (value instanceof Boolean) {
return DBusVariant.valueOf(com.lucimber.dbus.type.DBusBoolean.valueOf((Boolean) value));
} else if (value != null) {
// Convert to string as fallback
return DBusVariant.valueOf(DBusString.valueOf(value.toString()));
}
throw new UnsupportedOperationException(
"Type conversion not implemented for: "
+ (value != null ? value.getClass() : "null"));
}
private Map<String, DBusVariant> getAllProperties(String iface) throws Exception {
Map<String, Field> ifaceProps = properties.get(iface);
if (ifaceProps == null) {
return new HashMap<>();
}
Map<String, DBusVariant> result = new HashMap<>();
for (Map.Entry<String, Field> entry : ifaceProps.entrySet()) {
try {
result.put(entry.getKey(), getPropertyValue(iface, entry.getKey()));
} catch (Exception e) {
// Skip properties that fail to read
LOGGER.debug("Failed to read property: " + entry.getKey(), e);
}
}
return result;
}
private String generateIntrospectionXml() {
StringBuilder xml = new StringBuilder();
xml.append(
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n");
xml.append("\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n");
xml.append("<node>\n");
// Add standard interfaces
xml.append(" <interface name=\"org.freedesktop.DBus.Introspectable\">\n");
xml.append(" <method name=\"Introspect\">\n");
xml.append(" <arg name=\"xml_data\" type=\"s\" direction=\"out\"/>\n");
xml.append(" </method>\n");
xml.append(" </interface>\n");
xml.append(" <interface name=\"org.freedesktop.DBus.Properties\">\n");
xml.append(" <method name=\"Get\">\n");
xml.append(" <arg name=\"interface_name\" type=\"s\" direction=\"in\"/>\n");
xml.append(" <arg name=\"property_name\" type=\"s\" direction=\"in\"/>\n");
xml.append(" <arg name=\"value\" type=\"v\" direction=\"out\"/>\n");
xml.append(" </method>\n");
xml.append(" <method name=\"GetAll\">\n");
xml.append(" <arg name=\"interface_name\" type=\"s\" direction=\"in\"/>\n");
xml.append(" <arg name=\"properties\" type=\"a{sv}\" direction=\"out\"/>\n");
xml.append(" </method>\n");
xml.append(" </interface>\n");
xml.append(" <interface name=\"org.freedesktop.DBus.Peer\">\n");
xml.append(" <method name=\"Ping\"/>\n");
xml.append(" <method name=\"GetMachineId\">\n");
xml.append(" <arg name=\"machine_uuid\" type=\"s\" direction=\"out\"/>\n");
xml.append(" </method>\n");
xml.append(" </interface>\n");
// Add custom interface if available
if (interfaceName != null) {
xml.append(" <interface name=\"").append(interfaceName).append("\">\n");
// Add properties
Map<String, Field> ifaceProps = properties.get(interfaceName);
if (ifaceProps != null) {
for (String propName : ifaceProps.keySet()) {
xml.append(" <property name=\"")
.append(propName)
.append("\" type=\"v\" access=\"read\"/>\n");
}
}
xml.append(" </interface>\n");
}
xml.append("</node>\n");
return xml.toString();
}
private String getMachineId() {
try {
if (Files.exists(Paths.get("/etc/machine-id"))) {
return Files.readString(Paths.get("/etc/machine-id")).trim();
} else if (Files.exists(Paths.get("/var/lib/dbus/machine-id"))) {
return Files.readString(Paths.get("/var/lib/dbus/machine-id")).trim();
}
} catch (Exception e) {
// Ignore
}
return "0123456789abcdef0123456789abcdef";
}
private void sendReply(Context ctx, OutboundMethodReturn reply) {
CompletableFuture<Void> future = new CompletableFuture<>();
ctx.propagateOutboundMessage(reply, future);
}
private void sendError(
Context ctx, InboundMethodCall call, String errorName, String errorMessage) {
OutboundError error =
OutboundError.Builder.create()
.withSerial(DBusUInt32.valueOf(serialCounter.getAndIncrement()))
.withReplySerial(call.getSerial())
.withErrorName(DBusString.valueOf(errorName))
.withBody(
DBusSignature.valueOf("s"),
Arrays.asList(DBusString.valueOf(errorMessage)))
.build();
CompletableFuture<Void> future = new CompletableFuture<>();
ctx.propagateOutboundMessage(error, future);
}
@Override
protected Logger getLogger() {
return LOGGER;
}
}