From f2ae5a826662ce216036bab1ef1144cca44b297c Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Fri, 29 Sep 2023 00:06:24 +0100 Subject: [PATCH 1/6] Makes it possible to map DBUS properties directly to setter / getter methods in any `DBusInterface` --- .../dbus/RemoteInvocationHandler.java | 14 + .../dbus/annotations/DBusProperty.java | 4 +- .../dbus/connections/AbstractConnection.java | 273 +++++++++++++----- .../dbus/messages/ExportedObject.java | 93 +++++- .../dbus/utils/DBusNamingUtil.java | 28 ++ .../freedesktop/dbus/utils/PropertyRef.java | 98 +++++++ .../ExportObjectWithProperties.java | 55 ++++ .../properties/InterfaceWithProperties.java | 32 ++ .../properties/ObjectWithProperties.java | 54 ++++ .../generator/InterfaceCodeGenerator.java | 55 +++- .../generator/InterfaceCodeGeneratorTest.java | 2 +- 11 files changed, 612 insertions(+), 96 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java create mode 100644 dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java create mode 100644 dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java create mode 100644 dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java index f2d96e9b4..51ff9837f 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java @@ -1,5 +1,7 @@ package org.freedesktop.dbus; +import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.annotations.MethodNoReply; import org.freedesktop.dbus.connections.AbstractConnection; import org.freedesktop.dbus.errors.Error; @@ -9,10 +11,12 @@ import org.freedesktop.dbus.exceptions.NotConnected; import org.freedesktop.dbus.interfaces.CallbackHandler; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.Message; import org.freedesktop.dbus.messages.MethodCall; import org.freedesktop.dbus.utils.DBusNamingUtil; import org.freedesktop.dbus.utils.LoggingHelper; +import org.freedesktop.dbus.utils.PropertyRef; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,6 +88,16 @@ public Object invoke(Object _proxy, Method _method, Object[] _args) throws Throw } } else if (_method.getName().equals("toString")) { return remote.toString(); + } else if (_method.getAnnotation(DBusProperty.class) != null) { + var name = DBusNamingUtil.getPropertyName(_method); + var access = PropertyRef.accessForMethod(_method); + if (access == Access.READ) { + var propGetMethod = Properties.class.getMethod("Get", String.class, String.class); + return executeRemoteMethod(remote, propGetMethod, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name); + } else { + var propSetMethod = Properties.class.getMethod("Set", String.class, String.class, Object.class); + return executeRemoteMethod(remote, propSetMethod, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name, _args[0]); + } } return executeRemoteMethod(remote, _method, conn, CALL_TYPE_SYNC, null, _args); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java index c815c6e45..997a6abc5 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java @@ -30,7 +30,7 @@ * @see org.freedesktop.dbus.interfaces.DBusInterface * @see org.freedesktop.dbus.TypeRef */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(DBusProperties.class) public @interface DBusProperty { @@ -40,7 +40,7 @@ * * @return name */ - String name(); + String name() default ""; /** * type of the property, in case of complex types please create custom interface that extends {@link org.freedesktop.dbus.TypeRef} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index 127fda4f9..0d09df53c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -1,6 +1,8 @@ package org.freedesktop.dbus.connections; import org.freedesktop.dbus.*; +import org.freedesktop.dbus.annotations.DBusProperties; +import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.impl.BaseConnectionBuilder; @@ -12,8 +14,10 @@ import org.freedesktop.dbus.exceptions.*; import org.freedesktop.dbus.interfaces.*; import org.freedesktop.dbus.messages.*; +import org.freedesktop.dbus.types.Variant; import org.freedesktop.dbus.utils.LoggingHelper; import org.freedesktop.dbus.utils.NameableThreadFactory; +import org.freedesktop.dbus.utils.PropertyRef; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -748,6 +752,7 @@ void handleMessage(Message _message) throws DBusException { } } + @SuppressWarnings("unchecked") private void handleMessage(final MethodCall _methodCall) throws DBusException { logger.debug("Handling incoming method call: {}", _methodCall); @@ -795,11 +800,113 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { logger.trace(" {} => {}", mt, exportObject.getMethods().get(mt)); } } - meth = exportObject.getMethods().get(new MethodTuple(_methodCall.getName(), _methodCall.getSig())); - if (null == meth) { - sendMessage(new Error(_methodCall, new UnknownMethod(String.format( + + var params = _methodCall.getParameters(); + if (params.length == 2 && params[0] instanceof String + && params[1] instanceof String + && params[0].equals(_methodCall.getInterface()) + && _methodCall.getName().equals("Get")) { + // 'Get' This MIGHT be a property reference + var propertyRef = new PropertyRef((String) params[1], null, Access.READ); + var propMeth = exportObject.getPropertyMethods().get(propertyRef); + if (propMeth != null) { + // This IS a property reference + var object = exportObject.getObject().get(); + + receivingService.execMethodCallHandler(() -> { + _methodCall.setArgs(new Object[0]); + invokeMethodAndReply(_methodCall, propMeth, object, 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED)); + }); + + return; + } + } else if (params.length == 3 + && params[0] instanceof String + && params[1] instanceof String + && params[0].equals(_methodCall.getInterface()) + && _methodCall.getName().equals("Set")) { + // 'Set' This MIGHT be a property reference + + var propertyRef = new PropertyRef((String) params[1], null, Access.WRITE); + var propMeth = exportObject.getPropertyMethods().get(propertyRef); + if (propMeth != null) { + // This IS a property reference + var object = exportObject.getObject().get(); + var type = PropertyRef.typeForMethod(propMeth); + var val = params[2] instanceof Variant ? ((Variant) params[2]).getValue() : params[2]; + receivingService.execMethodCallHandler(() -> { + try { + _methodCall.setArgs(Marshalling.deSerializeParameters(new Object[] {val}, new Type[] {type}, this)); + invokeMethodAndReply(_methodCall, propMeth, object, 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED)); + } catch (Exception _ex) { + logger.debug("", _ex); + handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); + return; + } + }); + + return; + } + } else if (params.length == 1 && params[0] instanceof String + && params[0].equals(_methodCall.getInterface()) + && _methodCall.getName().equals("GetAll")) { + // 'GetAll' + var allPropertyMethods = exportObject.getPropertyMethods().entrySet(); + var object = exportObject.getObject().get(); + + if (meth == null && object instanceof DBusProperties) { + /* If the object implements DBusProperties as well, we + * need the actual GetAll method as well + */ + meth = exportObject.getMethods().get(new MethodTuple(_methodCall.getName(), _methodCall.getSig())); + if (null == meth) { + sendMessage(new Error(_methodCall, new UnknownMethod(String.format( "The method `%s.%s' does not exist on this object.", _methodCall.getInterface(), _methodCall.getName())))); - return; + return; + } + } + var originalMeth = meth; + + receivingService.execMethodCallHandler(() -> { + var resultMap = new HashMap>(); + for (var propEn : allPropertyMethods) { + var propMeth = propEn.getValue(); + try { + _methodCall.setArgs(new Object[0]); + var val = invokeMethod(_methodCall, propMeth, object); + resultMap.put(propEn.getKey().getName(), new Variant<>(val)); + } catch (Throwable _ex) { + logger.debug("", _ex); + handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); + return; + } + } + + if (object instanceof DBusProperties) { + resultMap.putAll((Map>) setupAndInvoke(_methodCall, originalMeth, object, true)); + } + + try { + invokedMethodReply(_methodCall, originalMeth, resultMap); + } catch (DBusExecutionException _ex) { + logger.debug("", _ex); + handleException(_methodCall, _ex); + } catch (Throwable _ex) { + logger.debug("", _ex); + handleException(_methodCall, + new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", + _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()))); + } + }); + } + + if (meth == null) { + meth = exportObject.getMethods().get(new MethodTuple(_methodCall.getName(), _methodCall.getSig())); + if (null == meth) { + sendMessage(new Error(_methodCall, new UnknownMethod(String.format( + "The method `%s.%s' does not exist on this object.", _methodCall.getInterface(), _methodCall.getName())))); + return; + } } o = exportObject.getObject().get(); } @@ -811,84 +918,102 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { } // now execute it - final Method me = meth; - final Object ob = o; - final boolean noreply = 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED); - final DBusCallInfo info = new DBusCallInfo(_methodCall); - final AbstractConnection conn = this; - - logger.trace("Adding Runnable for method {}", meth); - Runnable r = new Runnable() { + queueInvokeMethod(_methodCall, meth, o); + } - @Override - public void run() { - logger.debug("Running method {} for remote call", me); + private void queueInvokeMethod(final MethodCall _methodCall, Method _meth, final Object _ob) { + logger.trace("Adding Runnable for method {}", _meth); + var noReply = 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED); + receivingService.execMethodCallHandler(() -> { + setupAndInvoke(_methodCall, _meth, _ob, noReply); + }); + } + private Object setupAndInvoke(final MethodCall _methodCall, Method _meth, final Object _ob, final boolean _noReply) { + logger.debug("Running method {} for remote call", _meth); + try { + Type[] ts = _meth.getGenericParameterTypes(); + var params2 = _methodCall.getParameters(); + _methodCall.setArgs(Marshalling.deSerializeParameters(params2, ts, this)); + LoggingHelper.logIf(logger.isTraceEnabled(), () -> { try { - Type[] ts = me.getGenericParameterTypes(); - _methodCall.setArgs(Marshalling.deSerializeParameters(_methodCall.getParameters(), ts, conn)); - LoggingHelper.logIf(logger.isTraceEnabled(), () -> { - try { - logger.trace("Deserialised {} to types {}", Arrays.deepToString(_methodCall.getParameters()), Arrays.deepToString(ts)); - } catch (Exception _ex) { - logger.trace("Error getting method call parameters", _ex); - } - }); + var params3 = _methodCall.getParameters(); + logger.trace("Deserialised {} to types {}", Arrays.deepToString(params3), Arrays.deepToString(ts)); } catch (Exception _ex) { - logger.debug("", _ex); - handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); - return; + logger.trace("Error getting method call parameters", _ex); } + }); + } catch (Exception _ex) { + logger.debug("", _ex); + handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); + return null; + } + + return invokeMethodAndReply(_methodCall, _meth, _ob, _noReply); + } + private Object invokeMethodAndReply(final MethodCall _methodCall, final Method _me, final Object _ob, final boolean _noreply) { + try { + var result = invokeMethod(_methodCall, _me, _ob); + if (!_noreply) { + invokedMethodReply(_methodCall, _me, result); + } + return result; + } catch (DBusExecutionException _ex) { + logger.debug("", _ex); + handleException(_methodCall, _ex); + } catch (Throwable _ex) { + logger.debug("", _ex); + handleException(_methodCall, + new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", + _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()))); + } + return null; + } + + private void invokedMethodReply(final MethodCall _methodCall, final Method _me, Object _result) + throws DBusException { + MethodReturn reply; + if (Void.TYPE.equals(_me.getReturnType())) { + reply = new MethodReturn(_methodCall, null); + } else { + StringBuffer sb = new StringBuffer(); + for (String s : Marshalling.getDBusType(_me.getGenericReturnType())) { + sb.append(s); + } + Object[] nr = Marshalling.convertParameters(new Object[] { + _result + }, new Type[] { + _me.getGenericReturnType() + }, this); + + reply = new MethodReturn(_methodCall, sb.toString(), nr); + } + sendMessage(reply); + } + + private Object invokeMethod(final MethodCall _methodCall, final Method _me, final Object _ob) + throws DBusException, IllegalAccessException, Throwable { + var info = new DBusCallInfo(_methodCall); + INFOMAP.put(Thread.currentThread(), info); + try { + LoggingHelper.logIf(logger.isTraceEnabled(), () -> { try { - INFOMAP.put(Thread.currentThread(), info); - Object result; - try { - LoggingHelper.logIf(logger.isTraceEnabled(), () -> { - try { - logger.trace("Invoking Method: {} on {} with parameters {}", me, ob, Arrays.deepToString(_methodCall.getParameters())); - } catch (DBusException _ex) { - logger.trace("Error getting parameters from method call", _ex); - } - }); - - result = me.invoke(ob, _methodCall.getParameters()); - } catch (InvocationTargetException _ex) { - logger.debug(_ex.getMessage(), _ex); - throw _ex.getCause(); - } - INFOMAP.remove(Thread.currentThread()); - if (!noreply) { - MethodReturn reply; - if (Void.TYPE.equals(me.getReturnType())) { - reply = new MethodReturn(_methodCall, null); - } else { - StringBuffer sb = new StringBuffer(); - for (String s : Marshalling.getDBusType(me.getGenericReturnType())) { - sb.append(s); - } - Object[] nr = Marshalling.convertParameters(new Object[] { - result - }, new Type[] { - me.getGenericReturnType() - }, conn); - - reply = new MethodReturn(_methodCall, sb.toString(), nr); - } - conn.sendMessage(reply); - } - } catch (DBusExecutionException _ex) { - logger.debug("", _ex); - handleException(_methodCall, _ex); - } catch (Throwable _ex) { - logger.debug("", _ex); - handleException(_methodCall, - new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", - _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()))); + var params4 = _methodCall.getParameters(); + logger.trace("Invoking Method: {} on {} with parameters {}", _me, _ob, Arrays.deepToString(params4)); + } catch (DBusException _ex) { + logger.trace("Error getting parameters from method call", _ex); } - } - }; - receivingService.execMethodCallHandler(r); + }); + + var params5 = _methodCall.getParameters(); + return _me.invoke(_ob, params5); + } catch (InvocationTargetException _ex) { + logger.debug(_ex.getMessage(), _ex); + throw _ex.getCause(); + } finally { + INFOMAP.remove(Thread.currentThread()); + } } /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java index 64218ccbf..4fa422fcd 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java @@ -10,6 +10,7 @@ import org.freedesktop.dbus.annotations.DBusMemberName; import org.freedesktop.dbus.annotations.DBusProperties; import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.connections.AbstractConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -17,6 +18,7 @@ import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.Peer; import org.freedesktop.dbus.utils.DBusNamingUtil; +import org.freedesktop.dbus.utils.PropertyRef; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; @@ -29,6 +31,7 @@ import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; @@ -41,6 +44,7 @@ public class ExportedObject { private final Map methods = new HashMap<>(); + private final Map propertyMethods = new HashMap<>(); private final String introspectionData; private final Reference object; @@ -96,31 +100,43 @@ protected String generateAnnotationsXml(AnnotatedElement _c) { * @throws DBusException in case of unknown data types */ protected String generatePropertyXml(DBusProperty _property) throws DBusException { - Class propertyTypeClass = _property.type(); + return generatePropertyXml(_property.name(), _property.type(), _property.access()); + } + + /** + * Generates the introspection data for the single property. + * + * @param _propertyName name + * @param _propertyTypeClass type class + * @param _access type access + * @return xml with property definition + * @throws DBusException in case of unknown data types + */ + protected String generatePropertyXml(String _propertyName, Class _propertyTypeClass, Access _access) throws DBusException { String propertyTypeString; - if (TypeRef.class.isAssignableFrom(propertyTypeClass)) { - Type actualType = Arrays.stream(propertyTypeClass.getGenericInterfaces()) + if (TypeRef.class.isAssignableFrom(_propertyTypeClass)) { + Type actualType = Arrays.stream(_propertyTypeClass.getGenericInterfaces()) .filter(t -> t instanceof ParameterizedType) .map(t -> (ParameterizedType) t) .filter(t -> TypeRef.class.equals(t.getRawType())) .map(t -> t.getActualTypeArguments()[0]) // TypeRef has one generic argument .findFirst() .orElseThrow(() -> - new DBusException("Could not read TypeRef type for property '" + _property.name() + "'") + new DBusException("Could not read TypeRef type for property '" + _propertyName + "'") ); propertyTypeString = Marshalling.getDBusType(new Type[]{actualType}); - } else if (List.class.equals(propertyTypeClass)) { + } else if (List.class.equals(_propertyTypeClass)) { // default non generic list types propertyTypeString = "av"; - } else if (Map.class.equals(propertyTypeClass)) { + } else if (Map.class.equals(_propertyTypeClass)) { // default non generic map type propertyTypeString = "a{vv}"; } else { - propertyTypeString = Marshalling.getDBusType(new Type[]{propertyTypeClass}); + propertyTypeString = Marshalling.getDBusType(new Type[]{_propertyTypeClass}); } - String access = _property.access().getAccessName(); - return ""; + String access = _access.getAccessName(); + return ""; } /** @@ -131,17 +147,61 @@ protected String generatePropertyXml(DBusProperty _property) throws DBusExceptio * @throws DBusException in case of unknown data types */ protected String generatePropertiesXml(Class _clz) throws DBusException { - StringBuilder xml = new StringBuilder(); - DBusProperties properties = _clz.getAnnotation(DBusProperties.class); + var xml = new StringBuilder(); + var map = new HashMap(); + var properties = _clz.getAnnotation(DBusProperties.class); if (properties != null) { for (DBusProperty property : properties.value()) { - xml.append(" ").append(generatePropertyXml(property)).append("\n"); + if (property.name().equals("")) { + throw new DBusException("Missing ''name'' on class DBUS property"); + } + if (map.containsKey(property.name())) { + throw new DBusException(MessageFormat.format( + "Property ''{0}'' defined multiple times.", property.name())); + } + map.put(property.name(), new PropertyRef(property)); } } - DBusProperty property = _clz.getAnnotation(DBusProperty.class); + + var property = _clz.getAnnotation(DBusProperty.class); if (property != null) { - xml.append(" ").append(generatePropertyXml(property)).append("\n"); + if (property.name().equals("")) { + throw new DBusException("Missing ''name'' on class DBUS property"); + } + if (map.containsKey(property.name())) { + throw new DBusException(MessageFormat.format( + "Property ''{0}'' defined multiple times.", property.name())); + } + map.put(property.name(), new PropertyRef(property)); + } + + for (var method : _clz.getDeclaredMethods()) { + var propertyAnnot = method.getAnnotation(DBusProperty.class); + if (propertyAnnot != null) { + var name = DBusNamingUtil.getPropertyName(method); + var access = PropertyRef.accessForMethod(method); + PropertyRef.checkMethod(method); + var type = PropertyRef.typeForMethod(method); + var ref = new PropertyRef(name, type, access); + propertyMethods.put(ref, method); + if (map.containsKey(name)) { + var existing = map.get(name); + if (access.equals(existing.getAccess())) { + throw new DBusException(MessageFormat.format( + "Property ''{0}'' has access mode ''{1}'' defined multiple times.", name, access)); + } else { + map.put(name, new PropertyRef(name, type, Access.READ_WRITE)); + } + } else { + map.put(name, ref); + } + } + } + + for (var ref : map.values()) { + xml.append(" ").append(generatePropertyXml(ref.getName(), ref.getType(), ref.getAccess())).append("\n"); } + return xml.toString(); } @@ -323,6 +383,10 @@ public Map getMethods() { return methods; } + public Map getPropertyMethods() { + return propertyMethods; + } + public Reference getObject() { return object; } @@ -334,6 +398,7 @@ public String getIntrospectiondata() { public static boolean isExcluded(Method _meth) { return !Modifier.isPublic(_meth.getModifiers()) || _meth.getAnnotation(DBusIgnore.class) != null + || _meth.getAnnotation(DBusProperty.class) != null || _meth.getName().equals("getObjectPath") && _meth.getReturnType().equals(String.class) && _meth.getParameterCount() == 0; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java index 658916ccb..63ae98699 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java @@ -2,6 +2,7 @@ import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusMemberName; +import org.freedesktop.dbus.annotations.DBusProperty; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -49,6 +50,33 @@ public static String getMethodName(Method _method) { return _method.getName(); } + /** + * Get a property name for a method (annotated with {@link DBusProperty}. These + * would typically be setter / getter type methods. If {@link DBusProperty#name()} is + * provided, that will take precedence. + * + * @param _method input method + * @return property name + * @see DBusMemberName + */ + public static String getPropertyName(Method _method) { + Objects.requireNonNull(_method, "method must not be null"); + + if (_method.isAnnotationPresent(DBusProperty.class)) { + String defName = _method.getAnnotation(DBusProperty.class).name(); + if (!"".equals(defName)) { + return defName; + } + } + String name = _method.getName(); + if ((name.startsWith("get") && !"get".equals(name)) || (name.startsWith("set") && !"set".equals(name))) { + name = name.substring(3); + } else if (name.startsWith("is") && !"is".equals(name)) { + name = name.substring(2); + } + return name; + } + /** * Get DBus signal name for specified signal class. * diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java new file mode 100644 index 000000000..6aa95ace3 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java @@ -0,0 +1,98 @@ +package org.freedesktop.dbus.utils; + +import java.lang.reflect.Method; +import java.util.Objects; + +// CHECKSTYLE:OFF +/* TODO Impossible to correct. Whichever way around these imports are, checkstyle complains */ +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.types.Variant; +import org.freedesktop.dbus.annotations.DBusProperty; +// CHECKSTYLE:ON + +/** + * Contains the same information as a {@link DBusProperty}, but as a POJO. Use + * internally when dealing with properties that are derived methods annotated + * with said annotation. + */ +public final class PropertyRef { + + private final String name; + private final Class type; + private final DBusProperty.Access access; + + public PropertyRef(String _name, Class _type, Access _access) { + super(); + this.name = _name; + this.type = _type; + this.access = _access; + } + + public PropertyRef(DBusProperty _property) { + this(_property.name(), _property.type(), _property.access()); + } + + @Override + public int hashCode() { + return Objects.hash(access, name); + } + + @Override + public boolean equals(Object _obj) { + if (this == _obj) { + return true; + } + if (_obj == null) { + return false; + } + if (getClass() != _obj.getClass()) { + return false; + } + PropertyRef other = (PropertyRef) _obj; + return access == other.access && Objects.equals(name, other.name); + } + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + public DBusProperty.Access getAccess() { + return access; + } + + public static Access accessForMethod(Method _method) { + var annotation = _method.getAnnotation(DBusProperty.class); + var access = _method.getName().startsWith("set") ? Access.WRITE : Access.READ; + if (annotation.access().equals(Access.READ) || annotation.access().equals(Access.WRITE)) { + access = annotation.access(); + } + return access; + } + + public static Class typeForMethod(Method _method) { + var annotation = _method.getAnnotation(DBusProperty.class); + var type = annotation.type(); + if (type == null || type.equals(Variant.class)) { + if (accessForMethod(_method) == Access.READ) { + return _method.getReturnType(); + } else { + return _method.getParameterTypes()[0]; + } + } + return type; + } + + public static void checkMethod(Method _method) { + var access = accessForMethod(_method); + if (access == Access.READ && (_method.getParameterCount() > 0 || _method.getReturnType().equals(void.class))) { + throw new IllegalArgumentException("READ properties must have zero parameters, and not return void."); + } + if (access == Access.WRITE && (_method.getParameterCount() != 1 || !_method.getReturnType().equals(void.class))) { + throw new IllegalArgumentException("WRITE properties must have exaclty 1 parameter, and return void."); + } + } +} diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java new file mode 100644 index 000000000..ee9a2e897 --- /dev/null +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java @@ -0,0 +1,55 @@ +package com.github.hypfvieh.dbus.examples.properties; + +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.interfaces.Properties; + +/** + * Sample which demonstrates on how to use an alternative mechanism to + * provide DBus properties directly using getter and setter methods. + * This means you do not have to implement support yourself by + * implementing {@link Properties#Get(String, String)} etc. + * + * @author brettsmith / hypfvieh + * + * @since 4.3.1 - 2023-09-28 + */ +public final class ExportObjectWithProperties { + + private ExportObjectWithProperties() {} + + public static void main(String[] _args) throws Exception { + try (var conn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + // create object + var obj = new ObjectWithProperties(); + obj.setMyProperty("My property value"); + + conn.requestBusName("com.acme"); + + // export to bus + conn.exportObject(obj); + + try (DBusConnection innerConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + // get the exported object + InterfaceWithProperties myObject = innerConn.getRemoteObject("com.acme", "/com/acme/ObjectWithProperties", InterfaceWithProperties.class); + + // Print hello from 'parent' object + System.out.println("> " + myObject.sayHello()); + + // Print value of property from object via its getter + System.out.println("> " + myObject.getMyProperty()); + + // Change values of property on object via their setters + myObject.setMyProperty("A new value!"); + myObject.setMyOtherProperty(true); + myObject.setMyAltProperty(987); + + } + + // Prove value of property on original object + System.out.println("This should say 'A new value!': " + obj.getMyProperty()); + System.out.println("This should say 'true': " + obj.isMyOtherProperty()); + System.out.println("This should say '987': " + obj.getMyAltProperty()); + } + } +} diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java new file mode 100644 index 000000000..5c3f4c008 --- /dev/null +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java @@ -0,0 +1,32 @@ +package com.github.hypfvieh.dbus.examples.properties; + +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; + +@DBusInterfaceName("com.acme.InterfaceWithProperties") +public interface InterfaceWithProperties extends DBusInterface { + + String sayHello(); + + int getJustAnInteger(); + + @DBusProperty(access = Access.READ, name = "MyProperty") + String getMyProperty(); + + @DBusProperty(access = Access.WRITE, name = "MyProperty") + void setMyProperty(String _property); + + @DBusProperty(access = Access.READ, name = "ZZZZZZZ") + long getMyAltProperty(); + + @DBusProperty(access = Access.WRITE, name = "ZZZZZZZ") + void setMyAltProperty(long _property); + + @DBusProperty + boolean isMyOtherProperty(); + + @DBusProperty + void setMyOtherProperty(boolean _property); +} diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java new file mode 100644 index 000000000..6864c1049 --- /dev/null +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java @@ -0,0 +1,54 @@ +package com.github.hypfvieh.dbus.examples.properties; + +public class ObjectWithProperties implements InterfaceWithProperties { + + private String myProperty = "Initial value"; + private boolean myOtherProperty; + private long myAltProperty = 123; + + @Override + public String getObjectPath() { + return "/com/acme/ObjectWithProperties"; + } + + @Override + public String sayHello() { + return "Hello!"; + } + + @Override + public long getMyAltProperty() { + return myAltProperty; + } + + @Override + public void setMyAltProperty(long _myAltProperty) { + this.myAltProperty = _myAltProperty; + } + + @Override + public int getJustAnInteger() { + return 99; + } + + @Override + public String getMyProperty() { + return myProperty; + } + + @Override + public void setMyProperty(String _property) { + myProperty = _property; + } + + @Override + public boolean isMyOtherProperty() { + return myOtherProperty; + } + + @Override + public void setMyOtherProperty(boolean _property) { + myOtherProperty = _property; + } + +} diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index 2bb3e622e..cc921787d 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -2,6 +2,7 @@ import org.freedesktop.dbus.Tuple; import org.freedesktop.dbus.TypeRef; +import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.Position; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -48,12 +49,17 @@ public class InterfaceCodeGenerator { private final String introspectionData; private final boolean disableFilter; + private final String forcePackageName; + private final boolean propertyMethods; - public InterfaceCodeGenerator(boolean _disableFilter, String _introspectionData, String _objectPath, String _busName) { + public InterfaceCodeGenerator(boolean _disableFilter, String _introspectionData, String _objectPath, String _busName, String _packageName, boolean _propertyMethods) { disableFilter = _disableFilter; introspectionData = _introspectionData; nodeName = _objectPath; busName = Util.isBlank(_busName) ? "*" : _busName; + forcePackageName = _packageName; + propertyMethods = _propertyMethods; + System.out.println(forcePackageName + "/" + propertyMethods); } /** @@ -141,7 +147,8 @@ private Map extractAll(Element _ife) throws IOException, DBusExcep String interfaceName = _ife.getAttribute("name"); Map fqcn = DbusInterfaceToFqcn.toFqcn(interfaceName); - String packageName = fqcn.get(DbusInterfaceToFqcn.PACKAGENAME); + String originalPackageName = fqcn.get(DbusInterfaceToFqcn.PACKAGENAME); + String packageName = forcePackageName == null ? originalPackageName : forcePackageName; String className = fqcn.get(DbusInterfaceToFqcn.CLASSNAME); logger.info("Creating interface: {}.{}", packageName, className); @@ -153,6 +160,10 @@ private Map extractAll(Element _ife) throws IOException, DBusExcep interfaceClass.setPackageName(packageName); interfaceClass.setDbusPackageName(fqcn.get(DbusInterfaceToFqcn.DBUS_INTERFACE_NAME)); interfaceClass.setClassName(className); + if (forcePackageName != null) { + interfaceClass.getAnnotations().add(new AnnotationInfo(DBusInterfaceName.class, + "\"" + originalPackageName + "." + className + "\"")); + } interfaceClass.setExtendClass(DBusInterface.class.getName()); List additionalClasses = new ArrayList<>(); @@ -392,8 +403,29 @@ private List extractProperties(Element _propertyElement, Class String annotationParams = "name = \"" + attrName + "\", " + "type = " + clzzName + ".class, " + "access = " + DBusProperty.Access.class.getSimpleName() + "." + access; - AnnotationInfo annotationInfo = new AnnotationInfo(DBusProperty.class, annotationParams); - _clzBldr.getAnnotations().add(annotationInfo); + + if (propertyMethods) { + if (DBusProperty.Access.READ.getAccessName().equals(attrAccess) + || DBusProperty.Access.READ_WRITE.getAccessName().equals(attrAccess)) { + ClassMethod classMethod = new ClassMethod( + ("boolean".equalsIgnoreCase(clzzName) ? "is" : "get") + attrName, clzzName, false); + _clzBldr.getMethods().add(classMethod); + classMethod.getAnnotations().add("@" + DBusProperty.class.getSimpleName()); + _clzBldr.getImports().add(DBusProperty.class.getName()); + } + + if (DBusProperty.Access.WRITE.getAccessName().equals(attrAccess) + || DBusProperty.Access.READ_WRITE.getAccessName().equals(attrAccess)) { + ClassMethod classMethod = new ClassMethod("set" + attrName, "void", false); + classMethod.getArguments().add(new MemberOrArgument(attrName.substring(0, 1).toLowerCase() + attrName.substring(1), clzzName)); + _clzBldr.getMethods().add(classMethod); + classMethod.getAnnotations().add("@" + DBusProperty.class.getSimpleName()); + _clzBldr.getImports().add(DBusProperty.class.getName()); + } + } else { + AnnotationInfo annotationInfo = new AnnotationInfo(DBusProperty.class, annotationParams); + _clzBldr.getAnnotations().add(annotationInfo); + } return additionalClasses; } @@ -493,6 +525,8 @@ public static void main(String[] _args) { String objectPath = null; String inputFile = null; boolean noFilter = false; + boolean propertyMethods = false; + String forcePackageName = null; for (int i = 0; i < _args.length; i++) { String p = _args[i]; @@ -507,6 +541,15 @@ public static void main(String[] _args) { System.exit(0); } else if ("--all".equals(p) || "-a".equals(p)) { noFilter = true; + } else if ("--propertyMethods".equals(p) || "-m".equals(p)) { + propertyMethods = true; + } else if ("--package".equals(p) || "-p".equals(p)) { + if (_args.length > i) { + forcePackageName = _args[++i]; + } else { + printHelp(); + System.exit(0); + } } else if ("--version".equals(p) || "-v".equals(p)) { version(); System.exit(0); @@ -574,7 +617,7 @@ public static void main(String[] _args) { System.exit(1); } - InterfaceCodeGenerator ci2 = new InterfaceCodeGenerator(noFilter, introspectionData, objectPath, busName); + InterfaceCodeGenerator ci2 = new InterfaceCodeGenerator(noFilter, introspectionData, objectPath, busName, forcePackageName, propertyMethods); try { Map analyze = ci2.analyze(ignoreDtd); @@ -603,8 +646,10 @@ private static void printHelp() { System.out.println(" --system | -y Use SYSTEM DBus"); System.out.println(" --session | -s Use SESSION DBus"); System.out.println(" --outputDir | -o Use as output directory for all generated files"); + System.out.println(" --packageName | -p Use as the Java package instead of using the DBus namespace."); System.out.println(" --inputFile | -i Use as XML introspection input file instead of querying DBus"); System.out.println(" --all | -a Create all classes for given bus name (do not filter)"); + System.out.println(" --propertyMethods | -m Generate setter/getter methods for properties"); System.out.println(""); System.out.println(" --enable-dtd-validation Enable DTD validation of introspection XML"); System.out.println(" --version Show version information"); diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java index 60439402b..9b811c1a3 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java @@ -19,7 +19,7 @@ static InterfaceCodeGenerator loadDBusXmlFile(File _inputFile, String _objectPat if (!StringUtils.isBlank(_busName)) { String introspectionData = Util.readFileToString(_inputFile); - InterfaceCodeGenerator ci2 = new InterfaceCodeGenerator(false, introspectionData, _objectPath, _busName); + InterfaceCodeGenerator ci2 = new InterfaceCodeGenerator(false, introspectionData, _objectPath, _busName, null, false); return ci2; } else { fail("No valid busName given"); From 42cf75db6c8737c358a798ed20c968047be3557f Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Sat, 30 Sep 2023 09:59:09 +0100 Subject: [PATCH 2/6] A few bug fixes and requested changes. * Enums (and other non-primitive types?) not working. * GetAll not working * Removed all usages of `var` --- .../dbus/RemoteInvocationHandler.java | 36 +++-- .../dbus/connections/AbstractConnection.java | 129 ++++++++++-------- .../exceptions/DBusExecutionException.java | 9 ++ .../dbus/messages/ExportedObject.java | 22 +-- .../freedesktop/dbus/utils/PropertyRef.java | 10 +- .../ExportObjectWithProperties.java | 8 +- .../properties/InterfaceWithProperties.java | 10 ++ .../properties/ObjectWithProperties.java | 10 ++ 8 files changed, 146 insertions(+), 88 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java index 51ff9837f..4fade47fb 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java @@ -89,14 +89,16 @@ public Object invoke(Object _proxy, Method _method, Object[] _args) throws Throw } else if (_method.getName().equals("toString")) { return remote.toString(); } else if (_method.getAnnotation(DBusProperty.class) != null) { - var name = DBusNamingUtil.getPropertyName(_method); - var access = PropertyRef.accessForMethod(_method); + String name = DBusNamingUtil.getPropertyName(_method); + Access access = PropertyRef.accessForMethod(_method); if (access == Access.READ) { - var propGetMethod = Properties.class.getMethod("Get", String.class, String.class); - return executeRemoteMethod(remote, propGetMethod, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name); + Method propGetMethod = Properties.class.getMethod("Get", String.class, String.class); + return executeRemoteMethod(remote, propGetMethod, + new Type[] {_method.getGenericReturnType()}, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name); } else { - var propSetMethod = Properties.class.getMethod("Set", String.class, String.class, Object.class); - return executeRemoteMethod(remote, propSetMethod, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name, _args[0]); + Method propSetMethod = Properties.class.getMethod("Set", String.class, String.class, Object.class); + return executeRemoteMethod(remote, propSetMethod, + new Type[] {_method.getGenericReturnType()}, conn, CALL_TYPE_SYNC, null, DBusNamingUtil.getInterfaceName(_method.getDeclaringClass()), name, _args[0]); } } @@ -106,6 +108,10 @@ public Object invoke(Object _proxy, Method _method, Object[] _args) throws Throw // CHECKSTYLE:ON public static Object convertRV(String _sig, Object[] _rp, Method _m, AbstractConnection _conn) throws DBusException { + return convertRV(_sig, _rp, new Type[] {_m.getGenericReturnType()}, _m, _conn); + } + + public static Object convertRV(String _sig, Object[] _rp, Type[] _types, Method _m, AbstractConnection _conn) throws DBusException { Class c = _m.getReturnType(); Object[] rp = _rp; if (rp == null) { @@ -119,12 +125,10 @@ public static Object convertRV(String _sig, Object[] _rp, Method _m, AbstractCon LoggingHelper.logIf(LOGGER.isTraceEnabled(), () -> LOGGER.trace("Converting return parameters from {} to type {}", Arrays.deepToString(_rp), _m.getGenericReturnType())); - rp = Marshalling.deSerializeParameters(rp, new Type[] { - _m.getGenericReturnType() - }, _conn); + rp = Marshalling.deSerializeParameters(rp, _types, _conn); } catch (Exception _ex) { LOGGER.debug("Wrong return type.", _ex); - throw new DBusException(String.format("Wrong return type (failed to de-serialize correct types: %s )", _ex.getMessage())); + throw new DBusException(String.format("Wrong return type (failed to de-serialize correct types: %s )", _ex.getMessage()), _ex); } } @@ -154,7 +158,13 @@ public static Object convertRV(String _sig, Object[] _rp, Method _m, AbstractCon } } - public static Object executeRemoteMethod(RemoteObject _ro, Method _m, AbstractConnection _conn, int _syncmethod, CallbackHandler _callback, Object... _args) throws DBusException { + public static Object executeRemoteMethod(final RemoteObject _ro, final Method _m, + final AbstractConnection _conn, final int _syncmethod, final CallbackHandler _callback, Object... _args) throws DBusException { + return executeRemoteMethod(_ro, _m, new Type[] {_m.getGenericReturnType()}, _conn, _syncmethod, _callback, _args); + } + + public static Object executeRemoteMethod(final RemoteObject _ro, final Method _m, + final Type[] _types, final AbstractConnection _conn, final int _syncmethod, final CallbackHandler _callback, Object... _args) throws DBusException { Type[] ts = _m.getGenericParameterTypes(); String sig = null; Object[] args = _args; @@ -221,10 +231,10 @@ public static Object executeRemoteMethod(RemoteObject _ro, Method _m, AbstractCo } try { - return convertRV(reply.getSig(), reply.getParameters(), _m, _conn); + return convertRV(reply.getSig(), reply.getParameters(), _types, _m, _conn); } catch (DBusException _ex) { LOGGER.debug("", _ex); - throw new DBusExecutionException(_ex.getMessage()); + throw new DBusExecutionException(_ex.getMessage(), _ex); } } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index 0d09df53c..9b50f0083 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -13,6 +13,7 @@ import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.*; import org.freedesktop.dbus.interfaces.*; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.*; import org.freedesktop.dbus.types.Variant; import org.freedesktop.dbus.utils.LoggingHelper; @@ -801,17 +802,17 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { } } - var params = _methodCall.getParameters(); + Object[] params = _methodCall.getParameters(); if (params.length == 2 && params[0] instanceof String && params[1] instanceof String - && params[0].equals(_methodCall.getInterface()) && _methodCall.getName().equals("Get")) { + // 'Get' This MIGHT be a property reference - var propertyRef = new PropertyRef((String) params[1], null, Access.READ); - var propMeth = exportObject.getPropertyMethods().get(propertyRef); + PropertyRef propertyRef = new PropertyRef((String) params[1], null, Access.READ); + Method propMeth = exportObject.getPropertyMethods().get(propertyRef); if (propMeth != null) { // This IS a property reference - var object = exportObject.getObject().get(); + Object object = exportObject.getObject().get(); receivingService.execMethodCallHandler(() -> { _methodCall.setArgs(new Object[0]); @@ -823,17 +824,16 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { } else if (params.length == 3 && params[0] instanceof String && params[1] instanceof String - && params[0].equals(_methodCall.getInterface()) && _methodCall.getName().equals("Set")) { // 'Set' This MIGHT be a property reference - var propertyRef = new PropertyRef((String) params[1], null, Access.WRITE); - var propMeth = exportObject.getPropertyMethods().get(propertyRef); + PropertyRef propertyRef = new PropertyRef((String) params[1], null, Access.WRITE); + Method propMeth = exportObject.getPropertyMethods().get(propertyRef); if (propMeth != null) { // This IS a property reference - var object = exportObject.getObject().get(); - var type = PropertyRef.typeForMethod(propMeth); - var val = params[2] instanceof Variant ? ((Variant) params[2]).getValue() : params[2]; + Object object = exportObject.getObject().get(); + Class type = PropertyRef.typeForMethod(propMeth); + Object val = params[2] instanceof Variant ? ((Variant) params[2]).getValue() : params[2]; receivingService.execMethodCallHandler(() -> { try { _methodCall.setArgs(Marshalling.deSerializeParameters(new Object[] {val}, new Type[] {type}, this)); @@ -848,56 +848,69 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { return; } } else if (params.length == 1 && params[0] instanceof String - && params[0].equals(_methodCall.getInterface()) && _methodCall.getName().equals("GetAll")) { // 'GetAll' - var allPropertyMethods = exportObject.getPropertyMethods().entrySet(); - var object = exportObject.getObject().get(); - - if (meth == null && object instanceof DBusProperties) { - /* If the object implements DBusProperties as well, we - * need the actual GetAll method as well - */ - meth = exportObject.getMethods().get(new MethodTuple(_methodCall.getName(), _methodCall.getSig())); - if (null == meth) { - sendMessage(new Error(_methodCall, new UnknownMethod(String.format( - "The method `%s.%s' does not exist on this object.", _methodCall.getInterface(), _methodCall.getName())))); - return; - } - } - var originalMeth = meth; - - receivingService.execMethodCallHandler(() -> { - var resultMap = new HashMap>(); - for (var propEn : allPropertyMethods) { - var propMeth = propEn.getValue(); + Set> allPropertyMethods = exportObject.getPropertyMethods().entrySet(); + /* If there are no property methods on this object, just process as normal */ + if (!allPropertyMethods.isEmpty()) { + Object object = exportObject.getObject().get(); + + if (meth == null && object instanceof DBusProperties) { + meth = exportObject.getMethods().get(new MethodTuple(_methodCall.getName(), _methodCall.getSig())); + if (null == meth) { + sendMessage(new Error(_methodCall, new UnknownMethod(String.format( + "The method `%s.%s' does not exist on this object.", _methodCall.getInterface(), _methodCall.getName())))); + return; + } + } else if (meth == null) { try { - _methodCall.setArgs(new Object[0]); - var val = invokeMethod(_methodCall, propMeth, object); - resultMap.put(propEn.getKey().getName(), new Variant<>(val)); - } catch (Throwable _ex) { + meth = Properties.class.getDeclaredMethod("GetAll", String.class); + } catch (NoSuchMethodException | SecurityException _ex) { logger.debug("", _ex); - handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); - return; + handleException(_methodCall, + new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", + _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()))); } } - if (object instanceof DBusProperties) { - resultMap.putAll((Map>) setupAndInvoke(_methodCall, originalMeth, object, true)); - } + Method originalMeth = meth; - try { - invokedMethodReply(_methodCall, originalMeth, resultMap); - } catch (DBusExecutionException _ex) { - logger.debug("", _ex); - handleException(_methodCall, _ex); - } catch (Throwable _ex) { - logger.debug("", _ex); - handleException(_methodCall, + receivingService.execMethodCallHandler(() -> { + Map> resultMap = new HashMap<>(); + for (Entry propEn : allPropertyMethods) { + Method propMeth = propEn.getValue(); + if (propEn.getKey().getAccess() == Access.READ) { + try { + _methodCall.setArgs(new Object[0]); + Object val = invokeMethod(_methodCall, propMeth, object); + resultMap.put(propEn.getKey().getName(), new Variant<>(val)); + } catch (Throwable _ex) { + logger.debug("", _ex); + handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); + return; + } + } + } + + if (object instanceof DBusProperties) { + resultMap.putAll((Map>) setupAndInvoke(_methodCall, originalMeth, object, true)); + } + + try { + invokedMethodReply(_methodCall, originalMeth, resultMap); + } catch (DBusExecutionException _ex) { + logger.debug("", _ex); + handleException(_methodCall, _ex); + } catch (Throwable _ex) { + logger.debug("", _ex); + handleException(_methodCall, new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()))); - } - }); + } + }); + + return; + } } if (meth == null) { @@ -923,7 +936,7 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { private void queueInvokeMethod(final MethodCall _methodCall, Method _meth, final Object _ob) { logger.trace("Adding Runnable for method {}", _meth); - var noReply = 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED); + boolean noReply = 1 == (_methodCall.getFlags() & Message.Flags.NO_REPLY_EXPECTED); receivingService.execMethodCallHandler(() -> { setupAndInvoke(_methodCall, _meth, _ob, noReply); }); @@ -933,11 +946,11 @@ private Object setupAndInvoke(final MethodCall _methodCall, Method _meth, final logger.debug("Running method {} for remote call", _meth); try { Type[] ts = _meth.getGenericParameterTypes(); - var params2 = _methodCall.getParameters(); + Object[] params2 = _methodCall.getParameters(); _methodCall.setArgs(Marshalling.deSerializeParameters(params2, ts, this)); LoggingHelper.logIf(logger.isTraceEnabled(), () -> { try { - var params3 = _methodCall.getParameters(); + Object[] params3 = _methodCall.getParameters(); logger.trace("Deserialised {} to types {}", Arrays.deepToString(params3), Arrays.deepToString(ts)); } catch (Exception _ex) { logger.trace("Error getting method call parameters", _ex); @@ -954,7 +967,7 @@ private Object setupAndInvoke(final MethodCall _methodCall, Method _meth, final private Object invokeMethodAndReply(final MethodCall _methodCall, final Method _me, final Object _ob, final boolean _noreply) { try { - var result = invokeMethod(_methodCall, _me, _ob); + Object result = invokeMethod(_methodCall, _me, _ob); if (!_noreply) { invokedMethodReply(_methodCall, _me, result); } @@ -994,19 +1007,19 @@ private void invokedMethodReply(final MethodCall _methodCall, final Method _me, private Object invokeMethod(final MethodCall _methodCall, final Method _me, final Object _ob) throws DBusException, IllegalAccessException, Throwable { - var info = new DBusCallInfo(_methodCall); + DBusCallInfo info = new DBusCallInfo(_methodCall); INFOMAP.put(Thread.currentThread(), info); try { LoggingHelper.logIf(logger.isTraceEnabled(), () -> { try { - var params4 = _methodCall.getParameters(); + Object[] params4 = _methodCall.getParameters(); logger.trace("Invoking Method: {} on {} with parameters {}", _me, _ob, Arrays.deepToString(params4)); } catch (DBusException _ex) { logger.trace("Error getting parameters from method call", _ex); } }); - var params5 = _methodCall.getParameters(); + Object[] params5 = _methodCall.getParameters(); return _me.invoke(_ob, params5); } catch (InvocationTargetException _ex) { logger.debug(_ex.getMessage(), _ex); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/exceptions/DBusExecutionException.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/exceptions/DBusExecutionException.java index 0a6376c85..a730ba501 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/exceptions/DBusExecutionException.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/exceptions/DBusExecutionException.java @@ -17,6 +17,15 @@ public DBusExecutionException(String _message) { super(_message); } + /** + * Create an exception with the specified message + * @param _message message + * @param _cause cause + */ + public DBusExecutionException(String _message, Throwable _cause) { + super(_message, _cause); + } + public void setType(String _type) { this.type = _type; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java index 4fa422fcd..e72e7d511 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java @@ -147,9 +147,9 @@ protected String generatePropertyXml(String _propertyName, Class _propertyTyp * @throws DBusException in case of unknown data types */ protected String generatePropertiesXml(Class _clz) throws DBusException { - var xml = new StringBuilder(); - var map = new HashMap(); - var properties = _clz.getAnnotation(DBusProperties.class); + StringBuilder xml = new StringBuilder(); + Map map = new HashMap<>(); + DBusProperties properties = _clz.getAnnotation(DBusProperties.class); if (properties != null) { for (DBusProperty property : properties.value()) { if (property.name().equals("")) { @@ -163,7 +163,7 @@ protected String generatePropertiesXml(Class _clz) throws DBusException { } } - var property = _clz.getAnnotation(DBusProperty.class); + DBusProperty property = _clz.getAnnotation(DBusProperty.class); if (property != null) { if (property.name().equals("")) { throw new DBusException("Missing ''name'' on class DBUS property"); @@ -175,17 +175,17 @@ protected String generatePropertiesXml(Class _clz) throws DBusException { map.put(property.name(), new PropertyRef(property)); } - for (var method : _clz.getDeclaredMethods()) { - var propertyAnnot = method.getAnnotation(DBusProperty.class); + for (Method method : _clz.getDeclaredMethods()) { + DBusProperty propertyAnnot = method.getAnnotation(DBusProperty.class); if (propertyAnnot != null) { - var name = DBusNamingUtil.getPropertyName(method); - var access = PropertyRef.accessForMethod(method); + String name = DBusNamingUtil.getPropertyName(method); + Access access = PropertyRef.accessForMethod(method); PropertyRef.checkMethod(method); - var type = PropertyRef.typeForMethod(method); - var ref = new PropertyRef(name, type, access); + Class type = PropertyRef.typeForMethod(method); + PropertyRef ref = new PropertyRef(name, type, access); propertyMethods.put(ref, method); if (map.containsKey(name)) { - var existing = map.get(name); + PropertyRef existing = map.get(name); if (access.equals(existing.getAccess())) { throw new DBusException(MessageFormat.format( "Property ''{0}'' has access mode ''{1}'' defined multiple times.", name, access)); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java index 6aa95ace3..f56c197f1 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java @@ -65,8 +65,8 @@ public DBusProperty.Access getAccess() { } public static Access accessForMethod(Method _method) { - var annotation = _method.getAnnotation(DBusProperty.class); - var access = _method.getName().startsWith("set") ? Access.WRITE : Access.READ; + DBusProperty annotation = _method.getAnnotation(DBusProperty.class); + Access access = _method.getName().startsWith("set") ? Access.WRITE : Access.READ; if (annotation.access().equals(Access.READ) || annotation.access().equals(Access.WRITE)) { access = annotation.access(); } @@ -74,8 +74,8 @@ public static Access accessForMethod(Method _method) { } public static Class typeForMethod(Method _method) { - var annotation = _method.getAnnotation(DBusProperty.class); - var type = annotation.type(); + DBusProperty annotation = _method.getAnnotation(DBusProperty.class); + Class type = annotation.type(); if (type == null || type.equals(Variant.class)) { if (accessForMethod(_method) == Access.READ) { return _method.getReturnType(); @@ -87,7 +87,7 @@ public static Class typeForMethod(Method _method) { } public static void checkMethod(Method _method) { - var access = accessForMethod(_method); + Access access = accessForMethod(_method); if (access == Access.READ && (_method.getParameterCount() > 0 || _method.getReturnType().equals(void.class))) { throw new IllegalArgumentException("READ properties must have zero parameters, and not return void."); } diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java index ee9a2e897..745b55429 100644 --- a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java @@ -1,5 +1,6 @@ package com.github.hypfvieh.dbus.examples.properties; +import com.github.hypfvieh.dbus.examples.properties.InterfaceWithProperties.Color; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.interfaces.Properties; @@ -21,7 +22,7 @@ private ExportObjectWithProperties() {} public static void main(String[] _args) throws Exception { try (var conn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { // create object - var obj = new ObjectWithProperties(); + ObjectWithProperties obj = new ObjectWithProperties(); obj.setMyProperty("My property value"); conn.requestBusName("com.acme"); @@ -38,11 +39,15 @@ public static void main(String[] _args) throws Exception { // Print value of property from object via its getter System.out.println("> " + myObject.getMyProperty()); + System.out.println("> " + myObject.isMyOtherProperty()); + System.out.println("> " + myObject.getMyAltProperty()); + System.out.println("> " + myObject.getColor()); // Change values of property on object via their setters myObject.setMyProperty("A new value!"); myObject.setMyOtherProperty(true); myObject.setMyAltProperty(987); + myObject.setColor(Color.GREEN); } @@ -50,6 +55,7 @@ public static void main(String[] _args) throws Exception { System.out.println("This should say 'A new value!': " + obj.getMyProperty()); System.out.println("This should say 'true': " + obj.isMyOtherProperty()); System.out.println("This should say '987': " + obj.getMyAltProperty()); + System.out.println("This should say 'GREEN': " + obj.getColor()); } } } diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java index 5c3f4c008..d154c6d42 100644 --- a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java @@ -8,6 +8,10 @@ @DBusInterfaceName("com.acme.InterfaceWithProperties") public interface InterfaceWithProperties extends DBusInterface { + enum Color { + RED, GREEN, BLUE + } + String sayHello(); int getJustAnInteger(); @@ -29,4 +33,10 @@ public interface InterfaceWithProperties extends DBusInterface { @DBusProperty void setMyOtherProperty(boolean _property); + + @DBusProperty + Color getColor(); + + @DBusProperty + void setColor(Color _color); } diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java index 6864c1049..5fd7ce314 100644 --- a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ObjectWithProperties.java @@ -5,6 +5,7 @@ public class ObjectWithProperties implements InterfaceWithProperties { private String myProperty = "Initial value"; private boolean myOtherProperty; private long myAltProperty = 123; + private Color color = Color.RED; @Override public String getObjectPath() { @@ -51,4 +52,13 @@ public void setMyOtherProperty(boolean _property) { myOtherProperty = _property; } + @Override + public Color getColor() { + return color; + } + + @Override + public void setColor(Color _color) { + this.color = _color; + } } From 3eac8467d11d2ce38c87580dbcd8a2dd536226bb Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Sat, 30 Sep 2023 10:46:17 +0100 Subject: [PATCH 3/6] Created separate annotation for new method of binding DBus properties, `@DBusBoundProperty`. The existing `@DBusProperty` has also been returned to it's previous configuration with `name()` becoming a mandatory attribute and only allowed on types. --- .../dbus/RemoteInvocationHandler.java | 4 +- .../dbus/annotations/DBusBoundProperty.java | 82 +++++++++++++++++++ .../dbus/annotations/DBusProperty.java | 7 +- .../dbus/messages/ExportedObject.java | 11 +-- .../dbus/utils/DBusNamingUtil.java | 16 ++-- .../freedesktop/dbus/utils/PropertyRef.java | 12 +-- .../properties/InterfaceWithProperties.java | 18 ++-- .../generator/InterfaceCodeGenerator.java | 11 +-- 8 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusBoundProperty.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java index 4fade47fb..51e7d588a 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/RemoteInvocationHandler.java @@ -1,6 +1,6 @@ package org.freedesktop.dbus; -import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.annotations.MethodNoReply; import org.freedesktop.dbus.connections.AbstractConnection; @@ -88,7 +88,7 @@ public Object invoke(Object _proxy, Method _method, Object[] _args) throws Throw } } else if (_method.getName().equals("toString")) { return remote.toString(); - } else if (_method.getAnnotation(DBusProperty.class) != null) { + } else if (_method.getAnnotation(DBusBoundProperty.class) != null) { String name = DBusNamingUtil.getPropertyName(_method); Access access = PropertyRef.accessForMethod(_method); if (access == Access.READ) { diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusBoundProperty.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusBoundProperty.java new file mode 100644 index 000000000..52fbbd6d1 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusBoundProperty.java @@ -0,0 +1,82 @@ +package org.freedesktop.dbus.annotations; + +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.interfaces.Properties; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Binds a setter or getter method to a DBus property, in + * a similar manner to the familiar JavaBeans pattern. + *

+ * Using this annotation means you do not need to implement the {@link Properties} + * interface and provide your own handling of {@link Properties#Get(String, String)}, + * {@link Properties#GetAll(String)} and {@link Properties#Set(String, String, Object)}. + *

+ * Each DBus property should map to either one or two methods. If it has + * {@link DBusBoundProperty#access()} of {@link Access#READ} then a single getter + * method should be created with no parameters. The type of property will be determined by + * the return type of the method, and the name of the property will be derived from the method name, + * with either the get or the is stripped off. + *

+ * {@literal @}DBusBoundProperty
+ * public String getMyStringProperty();
+ *
+ * {@literal @}DBusBoundProperty
+ * public boolean isMyBooleanProperty();
+ * 
+ * If it has {@link DBusBoundProperty#access()} of {@link Access#WRITE} then a single setter + * method should be created with a single parameter and no return type. The type of the property + * will be determined by that parameter, and the name of the property will be derived from the + * method name, with either the get or the is stripped off. + *
+ * {@literal @}DBusBoundProperty
+ * public void setMyStringProperty(String _property);
+ * 
+ * If it has {@link DBusBoundProperty#access()} of {@link Access#READ_WRITE}, the both of + * the above methods should be provided. + *

+ * Any of the name, type and access attributes that would + * normally be automatically determined, may be overridden using the corresponding annotation attributes. + *

+ * It is allowed if you wish to mix use of {@link DBusProperty} and {@link DBusBoundProperty} as + * long as individual properties are not repeated. + * + * @see org.freedesktop.dbus.interfaces.DBusInterface + * @see org.freedesktop.dbus.annotations.DBusProperty + * @see org.freedesktop.dbus.annotations.DBusProperties + * @see org.freedesktop.dbus.TypeRef + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DBusBoundProperty { + + /** + * Property name. If not supplied, the property name will be inferred from the method. See + * class documentation for semantics. + * + * @return name + */ + String name() default ""; + + /** + * Type of the property, in case of complex types please create custom interface that extends {@link org.freedesktop.dbus.TypeRef}. + * If not supplied, then the type will be inferred from either the return value of a getter, or + * the first parameter of a setter. + * + * @return type + */ + Class type() default Void.class; + + /** + * Property access type. When {@link Access#READ_WRITE}, the access will be inferred from + * the method name, whether it is a setter or a getter. + * + * @return access + */ + Access access() default Access.READ_WRITE; + +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java index 997a6abc5..16a095589 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/annotations/DBusProperty.java @@ -26,11 +26,14 @@ * } * } * + *

As an alternative to this annotation, you might consider using {@link DBusBoundProperty}. This + * allows you to achieve the same results with less code.

. * * @see org.freedesktop.dbus.interfaces.DBusInterface + * @see org.freedesktop.dbus.annotations.DBusBoundProperty * @see org.freedesktop.dbus.TypeRef */ -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(DBusProperties.class) public @interface DBusProperty { @@ -40,7 +43,7 @@ * * @return name */ - String name() default ""; + String name(); /** * type of the property, in case of complex types please create custom interface that extends {@link org.freedesktop.dbus.TypeRef} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java index e72e7d511..232b11166 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/ExportedObject.java @@ -5,6 +5,7 @@ import org.freedesktop.dbus.StrongReference; import org.freedesktop.dbus.Tuple; import org.freedesktop.dbus.TypeRef; +import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusIgnore; import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusMemberName; @@ -152,9 +153,6 @@ protected String generatePropertiesXml(Class _clz) throws DBusException { DBusProperties properties = _clz.getAnnotation(DBusProperties.class); if (properties != null) { for (DBusProperty property : properties.value()) { - if (property.name().equals("")) { - throw new DBusException("Missing ''name'' on class DBUS property"); - } if (map.containsKey(property.name())) { throw new DBusException(MessageFormat.format( "Property ''{0}'' defined multiple times.", property.name())); @@ -165,9 +163,6 @@ protected String generatePropertiesXml(Class _clz) throws DBusException { DBusProperty property = _clz.getAnnotation(DBusProperty.class); if (property != null) { - if (property.name().equals("")) { - throw new DBusException("Missing ''name'' on class DBUS property"); - } if (map.containsKey(property.name())) { throw new DBusException(MessageFormat.format( "Property ''{0}'' defined multiple times.", property.name())); @@ -176,7 +171,7 @@ protected String generatePropertiesXml(Class _clz) throws DBusException { } for (Method method : _clz.getDeclaredMethods()) { - DBusProperty propertyAnnot = method.getAnnotation(DBusProperty.class); + DBusBoundProperty propertyAnnot = method.getAnnotation(DBusBoundProperty.class); if (propertyAnnot != null) { String name = DBusNamingUtil.getPropertyName(method); Access access = PropertyRef.accessForMethod(method); @@ -398,7 +393,7 @@ public String getIntrospectiondata() { public static boolean isExcluded(Method _meth) { return !Modifier.isPublic(_meth.getModifiers()) || _meth.getAnnotation(DBusIgnore.class) != null - || _meth.getAnnotation(DBusProperty.class) != null + || _meth.getAnnotation(DBusBoundProperty.class) != null || _meth.getName().equals("getObjectPath") && _meth.getReturnType().equals(String.class) && _meth.getParameterCount() == 0; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java index 63ae98699..74946b224 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusNamingUtil.java @@ -1,8 +1,8 @@ package org.freedesktop.dbus.utils; +import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusMemberName; -import org.freedesktop.dbus.annotations.DBusProperty; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -51,8 +51,8 @@ public static String getMethodName(Method _method) { } /** - * Get a property name for a method (annotated with {@link DBusProperty}. These - * would typically be setter / getter type methods. If {@link DBusProperty#name()} is + * Get a property name for a method (annotated with {@link DBusBoundProperty}. These + * would typically be setter / getter type methods. If {@link DBusBoundProperty#name()} is * provided, that will take precedence. * * @param _method input method @@ -62,16 +62,18 @@ public static String getMethodName(Method _method) { public static String getPropertyName(Method _method) { Objects.requireNonNull(_method, "method must not be null"); - if (_method.isAnnotationPresent(DBusProperty.class)) { - String defName = _method.getAnnotation(DBusProperty.class).name(); + if (_method.isAnnotationPresent(DBusBoundProperty.class)) { + String defName = _method.getAnnotation(DBusBoundProperty.class).name(); if (!"".equals(defName)) { return defName; } } String name = _method.getName(); - if ((name.startsWith("get") && !"get".equals(name)) || (name.startsWith("set") && !"set".equals(name))) { + String lowerCaseName = name.toLowerCase(); + if ((lowerCaseName.startsWith("get") && !"get".equals(lowerCaseName)) + || (lowerCaseName.startsWith("set") && !"set".equals(lowerCaseName))) { name = name.substring(3); - } else if (name.startsWith("is") && !"is".equals(name)) { + } else if (lowerCaseName.startsWith("is") && !"is".equals(lowerCaseName)) { name = name.substring(2); } return name; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java index f56c197f1..073d4677d 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/PropertyRef.java @@ -6,12 +6,12 @@ // CHECKSTYLE:OFF /* TODO Impossible to correct. Whichever way around these imports are, checkstyle complains */ import org.freedesktop.dbus.annotations.DBusProperty.Access; -import org.freedesktop.dbus.types.Variant; +import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusProperty; // CHECKSTYLE:ON /** - * Contains the same information as a {@link DBusProperty}, but as a POJO. Use + * Contains the same information as a {@link DBusBoundProperty}, but as a POJO. Use * internally when dealing with properties that are derived methods annotated * with said annotation. */ @@ -65,8 +65,8 @@ public DBusProperty.Access getAccess() { } public static Access accessForMethod(Method _method) { - DBusProperty annotation = _method.getAnnotation(DBusProperty.class); - Access access = _method.getName().startsWith("set") ? Access.WRITE : Access.READ; + DBusBoundProperty annotation = _method.getAnnotation(DBusBoundProperty.class); + Access access = _method.getName().toLowerCase().startsWith("set") ? Access.WRITE : Access.READ; if (annotation.access().equals(Access.READ) || annotation.access().equals(Access.WRITE)) { access = annotation.access(); } @@ -74,9 +74,9 @@ public static Access accessForMethod(Method _method) { } public static Class typeForMethod(Method _method) { - DBusProperty annotation = _method.getAnnotation(DBusProperty.class); + DBusBoundProperty annotation = _method.getAnnotation(DBusBoundProperty.class); Class type = annotation.type(); - if (type == null || type.equals(Variant.class)) { + if (type == null || type.equals(Void.class)) { if (accessForMethod(_method) == Access.READ) { return _method.getReturnType(); } else { diff --git a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java index d154c6d42..38f4576dc 100644 --- a/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java +++ b/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/InterfaceWithProperties.java @@ -1,7 +1,7 @@ package com.github.hypfvieh.dbus.examples.properties; +import org.freedesktop.dbus.annotations.DBusBoundProperty; 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; @@ -16,27 +16,27 @@ enum Color { int getJustAnInteger(); - @DBusProperty(access = Access.READ, name = "MyProperty") + @DBusBoundProperty(access = Access.READ, name = "MyProperty") String getMyProperty(); - @DBusProperty(access = Access.WRITE, name = "MyProperty") + @DBusBoundProperty(access = Access.WRITE, name = "MyProperty") void setMyProperty(String _property); - @DBusProperty(access = Access.READ, name = "ZZZZZZZ") + @DBusBoundProperty(access = Access.READ, name = "ZZZZZZZ") long getMyAltProperty(); - @DBusProperty(access = Access.WRITE, name = "ZZZZZZZ") + @DBusBoundProperty(access = Access.WRITE, name = "ZZZZZZZ") void setMyAltProperty(long _property); - @DBusProperty + @DBusBoundProperty boolean isMyOtherProperty(); - @DBusProperty + @DBusBoundProperty void setMyOtherProperty(boolean _property); - @DBusProperty + @DBusBoundProperty Color getColor(); - @DBusProperty + @DBusBoundProperty void setColor(Color _color); } diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index cc921787d..ecf0ebce9 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -2,6 +2,7 @@ import org.freedesktop.dbus.Tuple; import org.freedesktop.dbus.TypeRef; +import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.Position; @@ -410,8 +411,8 @@ private List extractProperties(Element _propertyElement, Class ClassMethod classMethod = new ClassMethod( ("boolean".equalsIgnoreCase(clzzName) ? "is" : "get") + attrName, clzzName, false); _clzBldr.getMethods().add(classMethod); - classMethod.getAnnotations().add("@" + DBusProperty.class.getSimpleName()); - _clzBldr.getImports().add(DBusProperty.class.getName()); + classMethod.getAnnotations().add("@" + DBusBoundProperty.class.getSimpleName()); + _clzBldr.getImports().add(DBusBoundProperty.class.getName()); } if (DBusProperty.Access.WRITE.getAccessName().equals(attrAccess) @@ -419,8 +420,8 @@ private List extractProperties(Element _propertyElement, Class ClassMethod classMethod = new ClassMethod("set" + attrName, "void", false); classMethod.getArguments().add(new MemberOrArgument(attrName.substring(0, 1).toLowerCase() + attrName.substring(1), clzzName)); _clzBldr.getMethods().add(classMethod); - classMethod.getAnnotations().add("@" + DBusProperty.class.getSimpleName()); - _clzBldr.getImports().add(DBusProperty.class.getName()); + classMethod.getAnnotations().add("@" + DBusBoundProperty.class.getSimpleName()); + _clzBldr.getImports().add(DBusBoundProperty.class.getName()); } } else { AnnotationInfo annotationInfo = new AnnotationInfo(DBusProperty.class, annotationParams); @@ -649,7 +650,7 @@ private static void printHelp() { System.out.println(" --packageName | -p Use as the Java package instead of using the DBus namespace."); System.out.println(" --inputFile | -i Use as XML introspection input file instead of querying DBus"); System.out.println(" --all | -a Create all classes for given bus name (do not filter)"); - System.out.println(" --propertyMethods | -m Generate setter/getter methods for properties"); + System.out.println(" --boundProperties | -b Generate setter/getter methods for properties"); System.out.println(""); System.out.println(" --enable-dtd-validation Enable DTD validation of introspection XML"); System.out.println(" --version Show version information"); From edadcfb146b77e918056cc3e58e8746b67a18c84 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Tue, 3 Oct 2023 22:06:11 +0100 Subject: [PATCH 4/6] Added detailed Maven site page for handling DBus properties using both techniques. --- src/site/markdown/properties.md | 331 ++++++++++++++++++++++++++++++++ src/site/site.xml | 1 + 2 files changed, 332 insertions(+) create mode 100644 src/site/markdown/properties.md diff --git a/src/site/markdown/properties.md b/src/site/markdown/properties.md new file mode 100644 index 000000000..15ede2c69 --- /dev/null +++ b/src/site/markdown/properties.md @@ -0,0 +1,331 @@ +# Properties + +[DBus Properties](https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties) can be accessed +and exported using one of two methods. + + * [Directly Bound To Setters And Getters](#using-the-exported-object). + * [Implementing the Properties Interface](#implementing-the-properties-interface). + +## Directly Bound To Setters And Getters + +Since version 5 of `dbus-java`, it has been possible to directly map DBus properties to Java +methods. By convention this is done using a JavaBeans like pattern. + +This method is likely preferable in most cases, as it has a number of advantages over the +legacy method described below. + + * Simpler code when exporting services, no need to implement `Get()`, `Set()` and `GetAll()` from `org.freedesktop.DBus.Properties`. In fact, you do not need to implement this interface at all. + * Simpler code when accessing services, no casting or unwrapping `Variant`. + * More natural. Java programmers are familiar with this pattern. + * Makes it much easier to share interfaces between client and server code. + +This feature is activated by the `@DBusBoundProperty` annotation. + +### The Interface + +Consider the following interface. + +``` +package com.acme; + +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.annotations.DBusBoundProperty; + +public interface MyInterface extends DBusInterface { + + String sayHello(); + + @DBusBoundProperty(access = Access.READ, name = "MyProperty") + String getMyProperty(); + + @DBusBoundProperty(access = Access.WRITE, name = "MyProperty") + void setMyProperty(String _property); + + @DBusBoundProperty(access = Access.READ, name = "ZZZZZZZ") + long getMyAltProperty(); + + @DBusBoundProperty(access = Access.WRITE, name = "ZZZZZZZ") + void setMyAltProperty(long _property); + + @DBusBoundProperty + boolean isMyOtherProperty(); + + @DBusBoundProperty + void setMyOtherProperty(boolean _property); +} +``` + +When instances of the interface are exported, just a single method call will be visible. +All other attributes of the interface will be exported as properties instead. + +Every DBus property has 3 optional attributes. It's `name`, it's `type` and it's `access` mode. +All 3 of these attributes may optionally be supplied to the annotation, but for most cases it +is sufficient to omit all attributes and for dbus-java to infer these from the method +signature. + + * The name of the property will be derived from the method name, with the leading `get`, + `set` or `is` removed (case insensitively). + * The access mode of the property will be determined by whether the method is a `set` or + and `get` or `is`. + * The type of the property will be derived from either the return value or the first (and only) parameter. + + +*All getters should have zero arguments and a return value, and all setters should have +a single argument, and no return value* + +If you are just interested in using a 3rd party service that exports your target object, you then +can just skip the next section and go straight to [Using The Exported Object](#using-the-exported-object) below. + +However if you wish to export your own service that uses properties, then read on. + +### The Implementation + +Expanding on the above example, we will now provide the implementation of this interface +and export it for clients to use. + +```java +package com.acme; + +public class MyObject implements MyInterface { + + private String myProperty = "Initial value"; + private boolean myOtherProperty; + private long myAltProperty = 123; + + @Override + public String getObjectPath() { + return "/com/acme/MyObject"; + } + + @Override + public String sayHello() { + return "Hello!"; + } + + @Override + public long getMyAltProperty() { + return myAltProperty; + } + + @Override + public void setMyAltProperty(long _myAltProperty) { + this.myAltProperty = _myAltProperty; + } + + @Override + public String getMyProperty() { + return myProperty; + } + + @Override + public void setMyProperty(String _property) { + myProperty = _property; + } + + @Override + public boolean isMyOtherProperty() { + return myOtherProperty; + } + + @Override + public void setMyOtherProperty(boolean _property) { + myOtherProperty = _property; + } + + @Override + public boolean isRemote() { + /* Whenever you are implementing an object, always return false */ + return false; + } +} + +``` + +### Exporting The Object + +You would export the object the same as normal. + +```java +package com.acme; + +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; + +public class ExportMyObject { + + ExportMyObject() throws DBusException { + DBusConnection conn = DBusConnectionBuilder.forSessionBus().build(); + conn.requestBusName( "com.acme" ); + + MyObject obj = new MyObject(); + + conn.exportObject( obj.getObjectPath(), obj ); + } + + + public static void main(String[] args) throws DBusException { + new ExportMyObject(); + } +} +``` + +### Using The Exported Object + +And finally making use of the exported interface in client code. + +```java +package com.acme; + +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; + +public class ImportMyObject { + + ImportMyObject() throws DBusException { + DBusConnection conn = DBusConnectionBuilder.forSessionBus().build(); + + MyInterface obj = conn.getRemoteObject("com.acme", "/com/acme/MyObject", MyInterface.class); + + System.out.println("My property: " + obj.getMyProperty()); + System.out.println("My Alt property: " + obj.getMyAltProperty()); + System.out.println("My Other property: " + obj.isMyOtherProperty()); + } + + public static void main(String[] args) throws DBusException { + new ImportMyObject(); + } +} +``` + +## Implementing the Properties Interface + +As an alternative to the above, you can use DBus's `org.freedesktop.dbus.Properties` interface. + +If you are exporting your own service, this means that you `extends Properties` in your interface, +and provide the required implementations of the `Get()`, `GetAll()` and `Set()` methods in your +concrete class. + +If you are using an exported interface in client code, then you access your remote object as normal, +and invoke the `Get()`, `GetAll()` or `Set()` methods on that object. + +The above example, re-written to use `@DBusProperty`, would look like this. + +``` +package com.acme; + +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.types.Variant; + +@DBusProperty(access = Access.READ_WRITE, name = "MyProperty", type = String.class) +@DBusProperty(access = Access.READ_WRITE, name = "ZZZZZZZ", type = String.class) +@DBusProperty(access = Access.READ_WRITE, name = "MyOtherProperty", type = Boolean.class) +public interface MyInterface extends DBusInterface, Properties { + + String sayHello(); +} +``` + +While this interface is smaller than the above, there is a bit more to do in the implementation, +or when you need to access these properties. + +```java +package com.acme; + +public class MyObject implements MyInterface { + + private String myProperty = "Initial value"; + private boolean myOtherProperty; + private long myAltProperty = 123; + + @Override + public String getObjectPath() { + return "/com/acme/MyObject"; + } + + @Override + public String sayHello() { + return "Hello!"; + } + + @Override + public A Get(String _interfaceName, String _propertyName) { + if(_propertyName.equals("MyProperty")) { + return (A)myProperty; + } + else if(_propertyName.equals("ZZZZZZZ")) { + return (A)myAltProperty; + } + else if(_propertyName.equals("MyOtherProperty")) { + return (A)myOtherProperty; + } + else + throw new IllegalArgumentException(); + } + + @Override + public void Set(String _interfaceName, String _propertyName, A _value) { + if(_propertyName.equals("MyProperty")) { + myProperty = (String)_value; + } + else if(_propertyName.equals("ZZZZZZZ")) { + myAltProperty = (String)_value; + } + else if(_propertyName.equals("MyOtherProperty")) { + myOtherProperty = (Boolean)_value; + } + else + throw new IllegalArgumentException(); + } + + @Override + public Map> GetAll(String _interfaceName) { + Map> all = new HashMap<>(); + all.put("MyProperty", new Variant<>(myProperty)); + all.put("ZZZZZZZ", new Variant<>(myAltProperty)); + all.put("MyOtherProperty", new Variant<>(myOtherProperty)); + return all; + } + + @Override + public boolean isRemote() { + /* Whenever you are implementing an object, always return false */ + return false; + } +} +``` + +And finally making use of these properties in client code. + +```java +package com.acme; + +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.types.Variant; + +public class ImportMyObject { + + ImportMyObject() throws DBusException { + DBusConnection conn = DBusConnectionBuilder.forSessionBus().build(); + + MyInterface obj = conn.getRemoteObject(MyInterface.class); + + System.out.println("My property: " + + ((Variant)obj.Get("MyProperty")).getValue()); + System.out.println("My Alt property: " + + ((Variant)obj.Get("ZZZZZZZ")).getValue()); + System.out.println("My Other property: " + + ((Variant)obj.Get("MyOtherProperty")).getValue()); + } + + public static void main(String[] args) throws DBusException { + new ImportMyObject(); + } +} +``` \ No newline at end of file diff --git a/src/site/site.xml b/src/site/site.xml index 52c19abea..1b4b051f0 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -33,6 +33,7 @@ + From a537f608dbc9ee7ba0232ba51828134b8802f74f Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Tue, 3 Oct 2023 22:25:39 +0100 Subject: [PATCH 5/6] Tweaked example code --- src/site/markdown/properties.md | 61 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/site/markdown/properties.md b/src/site/markdown/properties.md index 15ede2c69..72f189a3f 100644 --- a/src/site/markdown/properties.md +++ b/src/site/markdown/properties.md @@ -14,8 +14,9 @@ methods. By convention this is done using a JavaBeans like pattern. This method is likely preferable in most cases, as it has a number of advantages over the legacy method described below. - * Simpler code when exporting services, no need to implement `Get()`, `Set()` and `GetAll()` from `org.freedesktop.DBus.Properties`. In fact, you do not need to implement this interface at all. - * Simpler code when accessing services, no casting or unwrapping `Variant`. + * Simpler code when exporting services, no need to implement `Get()`, `Set()` and `GetAll()` from `org.freedesktop.DBus.Properties`. + In fact, you do not need to implement this interface at all. + * Simpler code when accessing properties on services, no casting or accessing properties by string names. * More natural. Java programmers are familiar with this pattern. * Makes it much easier to share interfaces between client and server code. @@ -217,9 +218,9 @@ The above example, re-written to use `@DBusProperty`, would look like this. package com.acme; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.interfaces.Properties; -import org.freedesktop.dbus.types.Variant; @DBusProperty(access = Access.READ_WRITE, name = "MyProperty", type = String.class) @DBusProperty(access = Access.READ_WRITE, name = "ZZZZZZZ", type = String.class) @@ -252,34 +253,32 @@ public class MyObject implements MyInterface { return "Hello!"; } + @SuppressWarnings("unchecked") @Override public A Get(String _interfaceName, String _propertyName) { - if(_propertyName.equals("MyProperty")) { - return (A)myProperty; - } - else if(_propertyName.equals("ZZZZZZZ")) { - return (A)myAltProperty; - } - else if(_propertyName.equals("MyOtherProperty")) { - return (A)myOtherProperty; - } - else + if ("MyProperty".equals(_propertyName)) { + return (A) new Variant<>(myProperty); + } else if ("ZZZZZZZ".equals(_propertyName)) { + return (A) new Variant<>(myAltProperty); + } else if ("MyOtherProperty".equals(_propertyName)) { + return (A) new Variant<>(myOtherProperty); + } else { throw new IllegalArgumentException(); + } } + @SuppressWarnings("unchecked") @Override public void Set(String _interfaceName, String _propertyName, A _value) { - if(_propertyName.equals("MyProperty")) { - myProperty = (String)_value; - } - else if(_propertyName.equals("ZZZZZZZ")) { - myAltProperty = (String)_value; - } - else if(_propertyName.equals("MyOtherProperty")) { - myOtherProperty = (Boolean)_value; - } - else + if ("MyProperty".equals(_propertyName)) { + myProperty = ((Variant) _value).getValue(); + } else if ("ZZZZZZZ".equals(_propertyName)) { + myAltProperty = ((Variant) _value).getValue(); + } else if ("MyOtherProperty".equals(_propertyName)) { + myOtherProperty = ((Variant) _value).getValue(); + } else { throw new IllegalArgumentException(); + } } @Override @@ -314,14 +313,14 @@ public class ImportMyObject { ImportMyObject() throws DBusException { DBusConnection conn = DBusConnectionBuilder.forSessionBus().build(); - MyInterface obj = conn.getRemoteObject(MyInterface.class); - - System.out.println("My property: " + - ((Variant)obj.Get("MyProperty")).getValue()); - System.out.println("My Alt property: " + - ((Variant)obj.Get("ZZZZZZZ")).getValue()); - System.out.println("My Other property: " + - ((Variant)obj.Get("MyOtherProperty")).getValue()); + MyInterface obj = conn.getRemoteObject("com.acme", "/com/acme/MyObject", MyInterface.class); + + System.out.println("My property: " + + (String) obj.Get("com.acme.MyInterface", "MyProperty")); + System.out.println("My Alt property: " + + (Long) obj.Get("com.acme.MyInterface", "ZZZZZZZ")); + System.out.println("My Other property: " + + (Boolean) obj.Get("com.acme.MyInterface", "MyOtherProperty")); } public static void main(String[] args) throws DBusException { From 5a32fd2b7184f4f4f6ba12bfbeaf8be2c3eb07e9 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Wed, 4 Oct 2023 23:42:58 +0100 Subject: [PATCH 6/6] Initial test for bound properties. --- .../dbus/connections/AbstractConnection.java | 6 +- .../dbus/test/BoundPropertiesTest.java | 253 ++++++++++++++++++ 2 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index 9b50f0083..0018b6f85 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -876,14 +876,14 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { Method originalMeth = meth; receivingService.execMethodCallHandler(() -> { - Map> resultMap = new HashMap<>(); + Map resultMap = new HashMap<>(); for (Entry propEn : allPropertyMethods) { Method propMeth = propEn.getValue(); if (propEn.getKey().getAccess() == Access.READ) { try { _methodCall.setArgs(new Object[0]); - Object val = invokeMethod(_methodCall, propMeth, object); - resultMap.put(propEn.getKey().getName(), new Variant<>(val)); + Object val = invokeMethod(_methodCall, propMeth, object); + resultMap.put(propEn.getKey().getName(), val); } catch (Throwable _ex) { logger.debug("", _ex); handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java new file mode 100644 index 000000000..76889a6ee --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java @@ -0,0 +1,253 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.TypeRef; +import org.freedesktop.dbus.annotations.DBusBoundProperty; +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.test.helper.structs.SampleStruct; +import org.freedesktop.dbus.types.UInt32; +import org.freedesktop.dbus.types.Variant; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class BoundPropertiesTest extends AbstractDBusBaseTest { + + @Test + public void testProperties() throws IOException, DBusException, InterruptedException { + try (DBusConnection conn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + MyObject obj = new MyObject(); + + conn.requestBusName("com.acme"); + conn.exportObject(obj); + + try (DBusConnection innerConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + MyInterface myObject = innerConn.getRemoteObject("com.acme", "/com/acme/MyObject", MyInterface.class); + + assertEquals("Hello!", myObject.sayHello()); + assertEquals("Initial value", myObject.getMyProperty()); + assertFalse(myObject.isMyOtherProperty()); + assertEquals(123, myObject.getMyAltProperty()); + assertEquals(AnEnum.ABC, myObject.getAnEnum()); + assertArrayEquals(new String[] {"Item 1", "Item 2"}, myObject.getArrayOfStuff()); + assertEquals(Arrays.asList(1, 3, 5, 7, 11), myObject.getAList()); + assertEquals(Map.of("Key 1", 123L, "Key 2", Long.MAX_VALUE, "Key 3", Long.MIN_VALUE), myObject.getAMap()); + + SampleStruct struct = myObject.getStruct(); + assertEquals("A", struct.getStringValue()); + assertEquals(new UInt32(123), struct.getInt32Value()); + assertEquals(new Variant<>(true), struct.getVariantValue()); + + myObject.setMyProperty("New value"); + myObject.setMyOtherProperty(true); + myObject.setMyAltProperty(987); + myObject.setAnEnum(AnEnum.DEF); + myObject.setArrayOfStuff(new String[] {"Another Item A", "Another Item B", "Another Item C"}); + SampleStruct struct2 = new SampleStruct("XXXX", new UInt32(999), new Variant<>(false)); + myObject.setStruct(struct2); + +// TODO these two fail with exceptions +// myObject.setAList(Arrays.asList(999, 998, 997, 996)); +// myObject.setAMap(Map.of("Key 4", 567L, "Key 5", Long.MAX_VALUE / 2, "Key 6", Long.MIN_VALUE / 2)); + + Thread.sleep(100000000L); + + assertEquals(myObject.getMyProperty(), "New value"); + assertTrue(myObject.isMyOtherProperty()); + assertEquals(myObject.getMyAltProperty(), 987); + assertEquals(myObject.getAnEnum(), AnEnum.DEF); + assertArrayEquals(new String[] {"Another Item A", "Another Item B", "Another Item C"}, myObject.getArrayOfStuff()); + struct2 = myObject.getStruct(); + assertEquals("XXXX", struct2.getStringValue()); + assertEquals(new UInt32(999), struct2.getInt32Value()); + assertEquals(new Variant<>(false), struct2.getVariantValue()); + +// TODO these two will fail until setAList and setAMap above work +// assertEquals(Arrays.asList(999, 998, 997, 996), myObject.getAList()); +// assertEquals(Map.of("Key 4", 567L, "Key 5", Long.MAX_VALUE / 2, "Key 6", Long.MIN_VALUE / 2), myObject.getAMap()); + + } + } + } + + interface StringList extends TypeRef> { + + } + + interface LongMap extends TypeRef> { + + } + + public enum AnEnum { + ABC, DEF, GHI, JKL + } + + @DBusInterfaceName("com.acme.MyInterface") + public interface MyInterface extends DBusInterface { + + String sayHello(); + + @DBusBoundProperty(access = Access.READ, name = "MyProperty") + String getMyProperty(); + + @DBusBoundProperty(access = Access.WRITE, name = "MyProperty") + void setMyProperty(String _property); + + @DBusBoundProperty(access = Access.READ, name = "ZZZZZZZ") + long getMyAltProperty(); + + @DBusBoundProperty(access = Access.WRITE, name = "ZZZZZZZ") + void setMyAltProperty(long _property); + + @DBusBoundProperty + boolean isMyOtherProperty(); + + @DBusBoundProperty + void setMyOtherProperty(boolean _property); + + @DBusBoundProperty + AnEnum getAnEnum(); + + @DBusBoundProperty + void setAnEnum(AnEnum _anEnum); + + @DBusBoundProperty + void setStruct(SampleStruct _struct); + + @DBusBoundProperty + SampleStruct getStruct(); + + @DBusBoundProperty + String[] getArrayOfStuff(); + + @DBusBoundProperty + void setArrayOfStuff(String[] _arrayOfStuff); + + @DBusBoundProperty(type = LongMap.class) + Map getAMap(); + + @DBusBoundProperty(type = LongMap.class) + void setAMap(Map _aMap); + + @DBusBoundProperty(type = StringList.class) + List getAList(); + + @DBusBoundProperty(type = StringList.class) + void setAList(List _aList); + } + + public class MyObject implements MyInterface { + + private String myProperty = "Initial value"; + private boolean myOtherProperty; + private long myAltProperty = 123; + private AnEnum anEnum = AnEnum.ABC; + private SampleStruct struct = new SampleStruct("A", new UInt32(123), new Variant<>(true)); + private String[] arrayOfStuff = new String[] {"Item 1", "Item 2"}; + private Map aMap = Map.of("Key 1", 123L, "Key 2", Long.MAX_VALUE, "Key 3", Long.MIN_VALUE); + private List aList = Arrays.asList(1, 3, 5, 7, 11); + + @Override + public List getAList() { + return aList; + } + + @Override + public void setAList(List _aList) { + this.aList = _aList; + } + + @Override + public SampleStruct getStruct() { + return struct; + } + + @Override + public void setStruct(SampleStruct _struct) { + this.struct = _struct; + } + + @Override + public AnEnum getAnEnum() { + return anEnum; + } + + @Override + public void setAnEnum(AnEnum _anEnum) { + this.anEnum = _anEnum; + } + + @Override + public String getObjectPath() { + return "/com/acme/MyObject"; + } + + @Override + public String sayHello() { + return "Hello!"; + } + + @Override + public long getMyAltProperty() { + return myAltProperty; + } + + @Override + public void setMyAltProperty(long _myAltProperty) { + this.myAltProperty = _myAltProperty; + } + + @Override + public String getMyProperty() { + return myProperty; + } + + @Override + public void setMyProperty(String _property) { + myProperty = _property; + } + + @Override + public boolean isMyOtherProperty() { + return myOtherProperty; + } + + @Override + public void setMyOtherProperty(boolean _property) { + myOtherProperty = _property; + } + + @Override + public boolean isRemote() { + /* Whenever you are implementing an object, always return false */ + return false; + } + + @Override + public String[] getArrayOfStuff() { + return arrayOfStuff; + } + + @Override + public void setArrayOfStuff(String[] _arrayOfStuff) { + this.arrayOfStuff = _arrayOfStuff; + } + + @Override + public Map getAMap() { + return aMap; + } + + @Override + public void setAMap(Map _aMap) { + this.aMap = _aMap; + } + } +}