All Downloads are FREE. Search and download functionalities are using the official Maven repository.

at.yawk.dbus.client.CallSiteBuilder Maven / Gradle / Ivy

The newest version!
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

package at.yawk.dbus.client;

import at.yawk.dbus.client.annotation.*;
import at.yawk.dbus.client.error.PatternResponseValidator;
import at.yawk.dbus.client.error.ResponseValidator;
import at.yawk.dbus.client.request.Request;
import at.yawk.dbus.client.request.RequestExecutor;
import at.yawk.dbus.client.request.Response;
import at.yawk.dbus.databind.DataBinder;
import at.yawk.dbus.databind.binder.Binder;
import at.yawk.dbus.databind.binder.PrimitiveAnnotationBinderTransformer;
import at.yawk.dbus.databind.binder.TypeUtil;
import at.yawk.dbus.protocol.MatchRule;
import at.yawk.dbus.protocol.MessageType;
import at.yawk.dbus.protocol.object.BasicObject;
import at.yawk.dbus.protocol.object.DbusObject;
import at.yawk.dbus.protocol.object.ObjectPathObject;
import at.yawk.dbus.protocol.object.StringObject;
import at.yawk.dbus.protocol.type.BasicType;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
 * @author yawkat
 */
@ToString(of = {
        "messageType",
        "objectPath",
        "interfaceName",
        "member",
        "destination",
        "arguments",
}, doNotUseGetters = true)
@Slf4j
class CallSiteBuilder implements Request {
    @Getter String bus;
    MessageType messageType;
    String objectPath;
    String interfaceName;
    String member;
    String destination;
    List actions = new ArrayList<>();
    @Getter List arguments = new ArrayList<>();
    List responseValidators = new ArrayList<>();

    boolean markedWithListener;
    Consumer> listener;

    // todo: properly support array returns
    Binder returnBinder;
    /**
     * Unwrap the returned variant before passing to the return binder; used by property get
     */
    boolean unwrapReturnVariant;

    // todo: bake these if possible
    ObjectPathObject objectPathObject;
    StringObject interfaceObject;
    StringObject memberObject;
    StringObject destinationObject;

    int timeout = -1;
    TimeUnit timeoutUnit;

    /**
     * @param childTransient If set to {@code true}, the returned call site will not be decorated from anything but
     *                       {@link #decorateFromCall(Object[])}.
     */
    CallSiteBuilder createChild(boolean childTransient) {
        CallSiteBuilder child = new CallSiteBuilder();
        child.bus = bus;
        child.messageType = messageType;

        child.objectPath = objectPath;
        child.objectPathObject = objectPathObject;

        child.interfaceName = interfaceName;
        child.interfaceObject = interfaceObject;

        child.destination = destination;
        child.destinationObject = destinationObject;

        child.member = member;
        child.memberObject = memberObject;

        child.actions = childTransient ? actions : new ArrayList<>(actions);
        child.responseValidators = childTransient ? responseValidators : new ArrayList<>(responseValidators);
        child.arguments = new ArrayList<>(arguments);
        child.markedWithListener = markedWithListener;
        child.returnBinder = returnBinder;
        child.unwrapReturnVariant = unwrapReturnVariant;

        child.timeout = timeout;
        child.timeoutUnit = timeoutUnit;
        return child;
    }

    @Override
    public MessageType getType() {
        return messageType;
    }

    @Override
    public ObjectPathObject getObjectPath() {
        if (objectPathObject == null) { objectPathObject = ObjectPathObject.create(objectPath); }
        return objectPathObject;
    }

    @Override
    public StringObject getInterfaceName() {
        if (interfaceObject == null) { interfaceObject = StringObject.create(interfaceName); }
        return interfaceObject;
    }

    @Override
    public StringObject getMember() {
        if (memberObject == null) { memberObject = StringObject.create(member); }
        return memberObject;
    }

    @Nullable
    @Override
    public StringObject getDestination() {
        if (destination == null) { return null; }
        if (destinationObject == null) { destinationObject = StringObject.create(destination); }
        return destinationObject;
    }

    void decorateFromClass(Class clazz) {
        decorateFromAnnotations(clazz);
    }

    @SuppressWarnings("unchecked")
    void decorateFromMethod(DataBinder dataBinder, Method method) {
        decorateFromAnnotations(method);

        Type[] genericParameterTypes = method.getGenericParameterTypes();

        if (markedWithListener) {
            if (genericParameterTypes.length != 1) {
                throw new IllegalArgumentException("Invalid parameter count on listener " + method);
            }
            int listenerParameterIndex = 0;

            Type listenerParameter = genericParameterTypes[listenerParameterIndex];
            Class raw = TypeUtil.getRawType(listenerParameter);
            if (raw == Runnable.class) {
                actions.add((site, args) -> {
                    Runnable runnable = (Runnable) args[listenerParameterIndex];
                    site.listener = o -> runnable.run();
                });
            } else if (raw == Consumer.class) {
                actions.add((site, args) -> {
                    Consumer consumer = (Consumer) args[listenerParameterIndex];
                    site.listener = o -> consumer.accept(decodeReply(o));
                });
                returnBinder = dataBinder.getBinder(
                        TypeUtil.getTypeVariable(listenerParameter, Consumer.class, "T"));
            } else {
                throw new IllegalArgumentException("Unsupported listener type " + raw.getName());
            }

            if (method.getReturnType() != void.class) {
                throw new IllegalArgumentException("Unsupported return type on listener method " + method);
            }

        } else {
            for (int i = 0; i < genericParameterTypes.length; i++) {
                Type parameter = genericParameterTypes[i];
                Annotation[] annotations = method.getParameterAnnotations()[i];
                Binder binder = dataBinder.getBinder(parameter, Arrays.asList(annotations));
                final int finalI = i;
                actions.add((site, args) -> site.arguments.add(binder.encode(args[finalI])));
            }

            if (method.getReturnType() != void.class) {
                returnBinder = dataBinder.getBinder(method.getGenericReturnType(), method);
                if (unwrapReturnVariant) {
                    returnBinder = PrimitiveAnnotationBinderTransformer.transformBinder(
                            returnBinder, BasicType.VARIANT);
                }
            }
        }
    }

    private void decorateFromAnnotations(AnnotatedElement element) {
        ifPresent(element, Listener.class, a -> {
            markedWithListener = true;
            if (messageType == null) {
                messageType = MessageType.SIGNAL;
            }
        });
        ifPresent(element, Destination.class, a -> this.destination = a.value());
        ifPresent(element, Interface.class, a -> this.interfaceName = a.value());
        ifPresent(element, Member.class, a -> this.member = a.value());
        ifPresent(element, ObjectPath.class, a -> this.objectPath = a.value());
        ifPresent(element, SubInterface.class, a -> this.interfaceName += '.' + a.value());
        ifPresent(element, Bus.class, a -> this.bus = a.value());
        ifPresent(element, SystemBus.class, a -> this.bus = "system");
        ifPresent(element, SessionBus.class, a -> this.bus = "session");
        ifPresent(element, Call.class, a -> this.messageType = MessageType.METHOD_CALL);
        ifPresent(element, Signal.class, a -> this.messageType = MessageType.SIGNAL);
        ifPresent(element, GetProperty.class, a -> {
            this.messageType = MessageType.METHOD_CALL;
            this.unwrapReturnVariant = true;

            actions.add((site, args) -> {
                site.arguments.add(BasicObject.createString(site.interfaceName));
                site.arguments.add(BasicObject.createString(site.member));
                site.interfaceName = "org.freedesktop.DBus.Properties";
                site.member = "Get";
            });
        });
        ifPresent(element, ExceptionMapping.class, this::decorateFromExceptionMapping);
        ifPresent(element, ExceptionMapping.RepeatableExceptionMapping.class, rep -> {
            for (ExceptionMapping mapping : rep.value()) {
                decorateFromExceptionMapping(mapping);
            }
        });
        ifPresent(element, Timeout.class, to -> {
            this.timeout = to.value();
            this.timeoutUnit = to.unit();
        });
    }

    @SneakyThrows
    private void decorateFromExceptionMapping(ExceptionMapping mapping) {
        Pattern pattern = mapping.pattern().isEmpty() ?
                Pattern.compile(mapping.value(), Pattern.LITERAL) :
                Pattern.compile(mapping.pattern());

        PatternResponseValidator validator = new PatternResponseValidator(pattern, mapping.exception());
        addValidator(validator);
    }

    void addValidator(ResponseValidator validator) {
        responseValidators.add(0, validator);
    }

    void decorateFromCall(Object[] args) throws Exception {
        for (CallSiteAction action : actions) {
            action.apply(this, args);
        }
    }

    Object submit(RequestExecutor executor) throws Exception {
        log.trace("Submitting call site {}", this);
        if (markedWithListener) {
            assert listener != null;
            MatchRule rule = new MatchRule();
            rule.setMessageType(getType());
            rule.setPath(getObjectPath());
            rule.setInterfaceName(interfaceName);
            // this breaks some listens and shouldn't really be used anyway
            //rule.setDestination(destination);
            rule.setMember(member);
            executor.listen(bus, rule, listener);
            return null;
        } else {
            assert listener == null;
            Response response = timeout > 0 ?
                    executor.execute(this, timeout, timeoutUnit) :
                    executor.execute(this);
            for (ResponseValidator validator : responseValidators) {
                validator.validate(response);
            }
            List reply = response.getReply();
            if (returnBinder == null) {
                return null; // void
            } else {
                return decodeReply(reply);
            }
        }
    }

    private Object decodeReply(List reply) {
        return returnBinder.decode(reply.get(0));
    }

    private static  void ifPresent(AnnotatedElement element, Class annotationClass,
                                                         Consumer consumer) {
        A annotation = element.getAnnotation(annotationClass);
        if (annotation != null) {
            consumer.accept(annotation);
        }
    }

    private interface CallSiteAction {
        void apply(CallSiteBuilder site, Object[] methodArgs) throws Exception;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy