hypfvieh / dbus-java

Improved version of java DBus library provided by freedesktop.org (https://dbus.freedesktop.org/doc/dbus-java/)
https://hypfvieh.github.io/dbus-java/
MIT License
185 stars 73 forks source link

Unexpected variant unwrapping with ModemManager property #209

Closed mattdibi closed 1 year ago

mattdibi commented 1 year ago

Hello there,

I have a weird issue with a ModemManager property that is being unexpectedly unwrapped when retrieved.

I'm trying to get the data from the Bearer object Properties on my Raspberry Pi.

Using busctl I get the expected output and signature:

pi@raspberrypi:~ $ sudo busctl get-property org.freedesktop.ModemManager1 /org/freedesktop/ModemManager1/Bearer/0 org.freedesktop.ModemManager1.Bearer Properties
a{sv} 4 "profile-id" i -1 "apn" s "mobile.vodafone.it" "allowed-auth" u 63 "ip-type" u 1

I've generated the following code for the Bearer object using the ModemManager introspection data.

package org.freedesktop.modemmanager1;

import java.util.Map;

import org.freedesktop.dbus.TypeRef;
import org.freedesktop.dbus.annotations.DBusInterfaceName;
import org.freedesktop.dbus.annotations.DBusProperty;
import org.freedesktop.dbus.annotations.DBusProperty.Access;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.types.UInt32;
import org.freedesktop.dbus.types.Variant;

/**
 * Auto-generated class.
 */
@DBusInterfaceName("org.freedesktop.ModemManager1.Bearer")
@DBusProperty(name = "Interface", type = String.class, access = Access.READ)
@DBusProperty(name = "Connected", type = Boolean.class, access = Access.READ)
@DBusProperty(name = "Suspended", type = Boolean.class, access = Access.READ)
@DBusProperty(name = "Ip4Config", type = Bearer.PropertyIp4ConfigType.class, access = Access.READ)
@DBusProperty(name = "Ip6Config", type = Bearer.PropertyIp6ConfigType.class, access = Access.READ)
@DBusProperty(name = "Stats", type = Bearer.PropertyStatsType.class, access = Access.READ)
@DBusProperty(name = "IpTimeout", type = UInt32.class, access = Access.READ)
@DBusProperty(name = "BearerType", type = UInt32.class, access = Access.READ)
@DBusProperty(name = "Properties", type = Bearer.PropertyPropertiesType.class, access = Access.READ)
public interface Bearer extends DBusInterface {

    public void Connect();

    public void Disconnect();

    public static interface PropertyIp4ConfigType extends TypeRef<Map<String, Variant<?>>> {

    }

    public static interface PropertyIp6ConfigType extends TypeRef<Map<String, Variant<?>>> {

    }

    public static interface PropertyStatsType extends TypeRef<Map<String, Variant<?>>> {

    }

    public static interface PropertyPropertiesType extends TypeRef<Map<String, Variant<?>>> {

    }
}

Then I created the following simple example to trigger the behaviour:

package org.eclipse.kura.NMTest;

import java.io.IOException;
import java.util.Map;

import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.interfaces.Properties;
import org.freedesktop.dbus.types.Variant;
public class App {
    public static void main( String[] args ) throws DBusException {
        try (DBusConnection dbusConn = DBusConnection.getConnection(DBusConnection.DEFAULT_SYSTEM_BUS_ADDRESS)) {
            Properties bearerProperties = dbusConn.getRemoteObject("org.freedesktop.ModemManager1",
                    "/org/freedesktop/ModemManager1/Bearer/0", Properties.class);

            Map<String, Variant<?>> properties = bearerProperties.Get("org.freedesktop.ModemManager1.Bearer", "Properties");

            System.out.println("Properties: " + properties.toString());
            System.out.println("Apn: " + properties.get("apn"));
            System.out.println("Apn: " + properties.get("apn").getValue());

        } catch (IOException _ex) {
            _ex.printStackTrace();
        }
    }
}

When I run the above code I get:

Properties: { profile-id => -1,apn => mobile.vodafone.it,allowed-auth => 63,ip-type => 1 }
Apn: mobile.vodafone.it
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to org.freedesktop.dbus.types.Variant
        at org.eclipse.kura.NMTest.App.main(App.java:21)

I expected to find a Variant not a String inside the Map!

All the Properties content get casted to the final Java type instead of being a Variant. Is this supposed to happen? How should I access the data without using a Object type?


Versions used for testing:

hypfvieh commented 1 year ago

I cannot check what's happening due to lack of proper setup. But I know that Variants may be unwrapped in some cases.

A Variant is nothing else than a container which holds the value the and type of the value. Dbus-java will convert that to the desired type. Therefore a Map<String, Object> should probably work. In any case you will have some expectation of what type you will get and have to check if the value matches that expectation. When using Variant it would be checking which type, creating the proper instance etc. When getting the unwrapped value as Object you have to some instanceof checking.

For me your setup looks similar to NetworkManagerExample (line 60-70).

mattdibi commented 1 year ago

But I know that Variants may be unwrapped in some cases.

That's what caught me off guard.

As you pointed out I'm in a similar situation to the NetworkManager example, but there you found yourself with a Map<String, Variant<?>>... in the ModemManager case I expected to find a Map<String, Variant<?>> but I find myself with the already unwrapped variants in the map.

Is this due to ModemManager providing additional metadata in its DBus messages that allows for this unwrapping to take place (not actually sure this is technically possible, I'm not a dbus expert)? Is there a way to avoid this unwrapping to take place to have consistent behaviour between services (I'm using both NetoworkManager and ModemManager in my codebase and this inconsistency is somewhat weird)?

Therefore a Map<String, Object> should probably work.

This is what I ended up doing, even though I'm not liking since the behaviour is not consistent with other services.

hypfvieh commented 1 year ago

The conversion is caused due to the different return types. NetworkManager will return a List of Map (aa{sv}) when e.g. asking for "AddressData".

Dbus-java will only check the first level of returned value to unwrap Variant. The returned object in for "AddressData" is a List, the List contains a Map, so there is nothing to unwrap.

In your case the return value is just a Map. I assume that the code in Marshalling line 610 will be called. This will than call the deSerializeParameter method on key and value of the map which then will unwrap the Variant.

There is no way to change this behavior.