org.openremote.client.shell.flowcontrol.FlowControlPresenter.java Source code

Java tutorial

Introduction

Here is the source code for org.openremote.client.shell.flowcontrol.FlowControlPresenter.java

Source

/*
 * Copyright 2015, OpenRemote Inc.
 *
 * See the CONTRIBUTORS.txt file in the distribution for a
 * full listing of individual contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.openremote.client.shell.flowcontrol;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.http.client.Response;
import elemental.js.util.JsArrayOfString;
import elemental.js.util.JsMapFromStringTo;
import jsinterop.annotations.JsType;
import org.openremote.client.event.*;
import org.openremote.client.shared.RequestPresenter;
import org.openremote.client.shared.Timeout;
import org.openremote.client.shared.View;
import org.openremote.client.shell.FlowCodec;
import org.openremote.client.shell.NodeCodec;
import org.openremote.shared.event.*;
import org.openremote.shared.event.FlowLoadEvent;
import org.openremote.shared.event.bus.VetoEventException;
import org.openremote.shared.event.client.*;
import org.openremote.shared.flow.Flow;
import org.openremote.shared.flow.FlowDependency;
import org.openremote.shared.flow.Node;
import org.openremote.shared.func.Callback;
import org.openremote.shared.func.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Locale;

import static org.openremote.client.shared.Timeout.debounce;

@JsType
public class FlowControlPresenter extends RequestPresenter<FlowControlPresenter.FlowControlView> {

    private static final Logger LOG = LoggerFactory.getLogger(FlowControlPresenter.class);

    @JsType(isNative = true)
    public interface FlowControlView extends View {
        void toggleFlowControl();
    }

    private static final FlowCodec FLOW_CODEC = GWT.create(FlowCodec.class);
    private static final NodeCodec NODE_CODEC = GWT.create(NodeCodec.class);

    public Flow flow;
    public String flowControlTitle;
    public boolean flowDirty = false;
    public boolean flowUnsaved;
    public FlowStatusDetail flowStatusDetail;
    public FlowDependency[] flowSuperDependencies;
    public FlowDependency[] flowSubDependencies;
    public String selectedNodeId;

    public FlowControlPresenter(FlowControlView view) {
        super(view);

        addListener(ShortcutEvent.class, event -> {
            if (flow == null)
                return;
            if (event.getKey() == 82) {
                getView().toggleFlowControl();
            } else if (event.getKey() == 83) {
                redeployFlow();
            }
        });

        addPrepareListener(ShellCloseEvent.class, this::vetoIfDirty);

        addPrepareListener(FlowEditEvent.class, this::vetoIfDirty);

        addListener(FlowEditEvent.class, event -> {

            flow = event.getFlow();
            notifyPath("flow");

            setFlowUnsaved(event.isUnsaved());
            setFlowDirty(flowUnsaved);

            setDependencies();

            setFlowStatusDetail(new FlowStatusDetail("REQUESTING FLOW STATUS..."));
            dispatch(new ServerSendEvent(new FlowRequestStatusEvent(flow.getId())));

            sendConsoleRefresh(flow);
        });

        addListener(FlowDeletedEvent.class, event -> {
            flow = null;
            notifyPathNull("flow");
            sendConsoleRefresh(null);
        });

        addListener(NodeSelectedEvent.class, event -> {
            selectedNodeId = event.getNodeId();
            notifyPath("selectedNodeId");
        });

        addListener(SessionOpenedEvent.class, event -> {
            dispatch(new ServerSendEvent(new FlowRequestStatusEvent()));
        });

        addListener(FlowStatusEvent.class, event -> {
            if (event.matches(flow)) {
                setFlowStatusDetail(new FlowStatusDetail(event.getPhase()));
            }
        });

        addListener(FlowDeploymentFailureEvent.class, event -> {
            // TODO Better flow deployment failure handling
            if (!event.getPhase().equals(FlowDeploymentPhase.NOT_FOUND)) {
                dispatch(new ShowFailureEvent(
                        "Deployment failure during phase: " + event.getPhase() + " - " + event.getMessage(),
                        10000));
            }

            if (event.matches(flow)) {
                setFlowStatusDetail(new FlowStatusDetail(event));
            }
        });

        addListener(FlowRuntimeFailureEvent.class, event -> {
            // TODO should probably do more when a flow fails at runtime
            dispatch(new ShowFailureEvent(event.getMessage(), 10000));
        });

        addListener(FlowModifiedEvent.class, event -> {
            setFlowStatusDetail(flowStatusDetail);
            setFlowDirty(true);
            if (event.isNotifyConsole()) {
                sendConsoleRefresh(event.getFlow());
            }
        });

        addListener(NodeCreateEvent.class, event -> {
            if (event.matches(flow)) {
                new CreateNodeProcedure(flow.getId(), event.getNodeType(), node -> {

                    if (event.getLabel() != null && event.getLabel().length() > 0) {
                        node.setLabel(event.getLabel());
                    }

                    // TODO Awful
                    if (event.getNodeProperties() != null && event.getNodeProperties().length() > 0) {
                        JsMapFromStringTo existingProperties = node.getProperties() != null
                                ? JsonUtils.safeEval(node.getProperties())
                                : JsMapFromStringTo.create();
                        JsMapFromStringTo additionalProperties = JsonUtils.safeEval(event.getNodeProperties());
                        JsArrayOfString additionalKeys = additionalProperties.keys();
                        for (int i = 0; i < additionalKeys.length(); i++) {
                            String key = additionalKeys.get(i);
                            existingProperties.put(key, additionalProperties.get(key));
                        }
                        node.setProperties(JsonUtils.stringify(existingProperties));
                    }

                    new AddNodeProcedure(event.getPositionX(), event.getPositionY(),
                            event.isApplyPositionAsProperties(), true).accept(node);
                }).call();
            }
        });

        addListener(SubflowNodeCreateEvent.class, event -> {
            if (event.matches(flow)) {
                // Can't add current flow as subflow
                if (flow.getId().equals(event.getSubflowId())) {
                    return;
                }
                new CreateSubflowNodeProcedure(flow.getId(), event.getSubflowId(), new AddNodeProcedure(
                        event.getPositionX(), event.getPositionY(), event.isApplyPositionAsProperties(), true))
                                .call();
            }
        });

        addListener(NodeSelectedEvent.class, event -> {
            if (flow != null) {
                Node node = flow.findNode(event.getNodeId());
                if (node != null) {
                    dispatch(new NodeEditEvent(flow, node));
                }
            }
        });

        addListener(ConsoleWidgetModifiedEvent.class, event -> {
            if (flow != null) {
                Node node = flow.findNode(event.getNodeId());
                if (node != null) {
                    node.setProperties(event.getNodeProperties());
                    dispatch(new FlowModifiedEvent(flow, false));
                    if (node.getId().equals(selectedNodeId)) {
                        dispatch(new NodePropertiesRefreshEvent(node.getId()));
                    }
                }
            }
        });

        addListener(NodeDeleteEvent.class, event -> {
            new DeleteNodeProcedure(event.getNode()).call();
        });

        addListener(NodeDeletedEvent.class, event -> {
            if (event.getNode().getId().equals(selectedNodeId)) {
                selectedNodeId = null;
                notifyPathNull("selectedNodeId");
            }
        });

        addListener(NodeDuplicateEvent.class, event -> {
            if (event.matches(flow)) {
                new DuplicateNodeProcedure(flow.getId(), event.getNode()).call();
            }
        });
    }

    /* ################################################################################# */

    public void flowPropertyChanged() {
        flowDirty = true; // Assume this is dirty, now other listeners might run
        debounce("FlowPropertyChange", () -> {
            if (flowDirty) { // If it's still dirty (after other listeners executed), tell the user
                dispatch(new FlowModifiedEvent(flow, false));
            }
        }, 500);
    }

    public void editFlowDependency(Flow dependency) {
        dispatch(new FlowLoadEvent(dependency.getId()));
    }

    public void startFlow() {
        dispatch(new ServerSendEvent(new FlowDeployEvent(flow.getId())));
    }

    public void stopFlow() {
        dispatch(new ServerSendEvent(new FlowStopEvent(flow.getId())));
    }

    public void exportFlow() {
        Flow dupe = copyFlow();
        new UpdateDependenciesProcedure(dupe, true, () -> {
            LOG.info(FLOW_CODEC.encode(dupe).toString());
            dispatch(new ShowInfoEvent("Export complete, see your browsers console."));
        }).call();
    }

    public void redeployFlow() {
        new SaveFlowProcedure(copyFlow(), flowUnsaved, savedFlow -> {
            dispatch(new ServerSendEvent(new FlowStopEvent(savedFlow.getId())));
            dispatch(new ServerSendEvent(new FlowDeployEvent(savedFlow.getId())));
        }).call();
    }

    public void saveFlow() {
        new SaveFlowProcedure(copyFlow(), flowUnsaved).call();
    }

    public void deleteFlow() {
        new DeleteFlowProcedure(copyFlow()).call();
    }

    /* ################################################################################# */

    protected void vetoIfDirty(Event event) {
        if (flowDirty) {
            dispatchDirtyConfirmation(() -> {
                setFlowDirty(false);
                dispatch(event);
            });
            throw new VetoEventException();
        }
    }

    protected void dispatchDirtyConfirmation(Callback confirmAction) {
        dispatch(new ConfirmationEvent("Unsaved Changes",
                "You have edited the current flow and not redeployed/saved the changes. Continue without saving changes?",
                confirmAction));
    }

    protected Flow copyFlow() {
        return FLOW_CODEC.decode(FLOW_CODEC.encode(flow));
    }

    protected void setDependencies() {
        flowSuperDependencies = flow.getSuperDependencies();
        notifyPath("flowSuperDependencies", flowSuperDependencies);

        flowSubDependencies = flow.getSubDependencies();
        notifyPath("flowSubDependencies", flowSubDependencies);
    }

    protected void setFlowUnsaved(boolean unsaved) {
        flowUnsaved = unsaved;
        notifyPath("flowUnsaved", flowUnsaved);
    }

    protected void setFlowDirty(boolean dirty) {
        flowDirty = dirty;
        notifyPath("flowDirty", flowDirty);
    }

    protected void setFlowStatusDetail(FlowStatusDetail flowStatusDetail) {
        this.flowStatusDetail = flowStatusDetail;
        notifyPath("flowStatusDetail");

        if (flowUnsaved) {
            flowControlTitle = flow.getDefaultedLabel() + " (New)";
        } else {
            flowControlTitle = flow.getDefaultedLabel();
        }
        notifyPath("flowControlTitle", flowControlTitle);
    }

    protected void sendConsoleRefresh(Flow flow) {
        if (flow == null) {
            dispatch(new ConsoleRefreshEvent());
            return;
        }

        Flow dupe = copyFlow();
        new UpdateDependenciesProcedure(dupe, true, () -> Timeout.debounce("Console Refresh",
                () -> dispatch(new ConsoleRefreshEvent(dupe, selectedNodeId)), 20)).call();
    }

    /* ################################################################################# */

    protected class AddNodeProcedure implements Consumer<Node> {

        final double positionX;
        final double positionY;
        final boolean applyPositionAsProperties; // Or editor settings
        final boolean transformPosition;

        public AddNodeProcedure(double positionX, double positionY, boolean applyPositionAsProperties,
                boolean transformPosition) {
            this.positionX = positionX;
            this.positionY = positionY;
            this.applyPositionAsProperties = applyPositionAsProperties;
            this.transformPosition = transformPosition;
        }

        @Override
        public void accept(Node node) {
            // TODO this is a bit awkward, we might want to clean this up along with the zoom factor/transformposition
            if (applyPositionAsProperties) {
                JsMapFromStringTo existingProperties = JsonUtils.safeEval(node.getProperties());
                existingProperties.put("positionX", positionX);
                existingProperties.put("positionY", positionY);
                String nodeProperties = JsonUtils.stringify(existingProperties);
                node.setProperties(nodeProperties);
            }

            flow.addNode(node);
            dispatch(new FlowModifiedEvent(flow, true));

            if (applyPositionAsProperties) {
                // TODO what's the default location on the editor canvas when you drop it on the console?
                dispatch(new NodeAddedEvent(flow, node, 250, 250, true));
            } else {
                dispatch(new NodeAddedEvent(flow, node, positionX, positionY, transformPosition));
            }
        }
    }

    protected class CreateNodeProcedure implements Callback {

        final String currentFlowId;
        final String nodeType;
        final Consumer<Node> success;

        public CreateNodeProcedure(String currentFlowId, String nodeType, Consumer<Node> success) {
            this.currentFlowId = currentFlowId;
            this.nodeType = nodeType;
            this.success = success;
        }

        @Override
        public void call() {
            LOG.debug("Creating node in flow: " + nodeType);

            ObjectResponseCallback<Node> responseCallback = new ObjectResponseCallback<Node>("Create node",
                    NODE_CODEC) {
                @Override
                protected void onResponse(Node node) {
                    if (flow != null && flow.getId().equals(currentFlowId)) {
                        success.accept(node);
                    }
                }
            };

            sendRequest(false, true, resource("catalog", "node", nodeType).get(), responseCallback);
        }
    }

    protected class DeleteNodeProcedure implements Callback {

        final Node node;
        final boolean isWiringNode;

        public DeleteNodeProcedure(Node node) {
            this.node = node;
            this.isWiringNode = node.isOfTypeConsumerOrProducer() && flow.hasDirectWiredSuperDependencies();
        }

        @Override
        public void call() {
            flow.removeNode(node);

            if (isWiringNode) { // Let's see if there is anything broken now, update dependencies tree
                Flow dupe = copyFlow();
                new UpdateDependenciesProcedure(dupe, false, () -> {
                    if (flow != null && flow.getId().equals(dupe.getId())) {
                        flow.setSuperDependencies(dupe.getSuperDependencies());
                        flow.setSubDependencies(dupe.getSubDependencies());
                        setDependencies();
                    }
                    dispatch(new ShowInfoEvent("Reloaded dependencies of flow '" + flow.getDefaultedLabel() + "'"));
                    dispatch(new FlowModifiedEvent(flow, true));
                    dispatch(new NodeDeletedEvent(flow, node));
                }).call();
            } else {
                dispatch(new FlowModifiedEvent(flow, true));
                dispatch(new NodeDeletedEvent(flow, node));
            }
        }
    }

    protected class DuplicateNodeProcedure implements Callback {

        final String currentFlowId;
        final Node node;

        public DuplicateNodeProcedure(String currentFlowId, Node node) {
            this.currentFlowId = currentFlowId;
            this.node = node;
        }

        @Override
        public void call() {

            ObjectResponseCallback<Node> responseCallback = new ObjectResponseCallback<Node>("Duplicate node",
                    NODE_CODEC) {
                @Override
                protected void onResponse(Node dupe) {
                    if (flow != null && flow.getId().equals(currentFlowId)) {
                        dupe.getEditorSettings().setPositionX(dupe.getEditorSettings().getPositionX() + 20);
                        dupe.getEditorSettings().setPositionY(dupe.getEditorSettings().getPositionY() + 20);
                        new AddNodeProcedure(dupe.getEditorSettings().getPositionX(),
                                dupe.getEditorSettings().getPositionY(), false, false).accept(dupe);
                    }
                }
            };

            sendRequest(false, true, resource("flow", "duplicate", "node").post().json(NODE_CODEC.encode(node)),
                    responseCallback);
        }
    }

    protected class CreateSubflowNodeProcedure implements Callback {

        final String currentFlowId;
        final String subflowId;
        final Consumer<Node> success;

        public CreateSubflowNodeProcedure(String currentFlowId, String subflowId, Consumer<Node> success) {
            this.currentFlowId = currentFlowId;
            this.subflowId = subflowId;
            this.success = success;
        }

        @Override
        public void call() {
            LOG.debug("Creating subflow in flow designer: " + subflowId);

            ObjectResponseCallback<Node> responseCallback = new ObjectResponseCallback<Node>("Create subflow node",
                    NODE_CODEC) {
                @Override
                protected void onResponse(Node subflowNode) {
                    createSuccess(subflowNode);
                }
            };

            sendRequest(false, true, resource("flow", subflowId, "subflow").get(), responseCallback);
        }

        protected void createSuccess(Node subflowNode) {
            if (flow != null && flow.getId().equals(currentFlowId)) {
                // Add the subflow node to a temporary copy so we don't affect the
                // flow we are editing until we are done with resolution
                Flow flowCopy = copyFlow();
                if (subflowNode != null)
                    flowCopy.addNode(subflowNode);

                new UpdateDependenciesProcedure(flowCopy, false, () -> {
                    if (flow != null && flow.getId().equals(currentFlowId)) {
                        flow.setSuperDependencies(flowCopy.getSuperDependencies());
                        flow.setSubDependencies(flowCopy.getSubDependencies());
                        setDependencies();
                    }
                    success.accept(subflowNode);
                }).call();
            }
        }
    }

    protected class SaveFlowProcedure implements Callback {

        final Flow flowToSave;
        final boolean flowIsUnsaved;
        final Consumer<Flow> success;

        public SaveFlowProcedure(Flow flowToSave, boolean flowIsUnsaved) {
            this(flowToSave, flowIsUnsaved, null);
        }

        public SaveFlowProcedure(Flow flowToSave, boolean flowIsUnsaved, Consumer<Flow> success) {
            this.flowToSave = flowToSave;
            this.flowIsUnsaved = flowIsUnsaved;
            this.success = success;
        }

        @Override
        public void call() {
            Callback saveCallback;
            if (flowIsUnsaved) {
                saveCallback = () -> {
                    // Don't send dependencies to the server
                    flowToSave.clearDependencies();

                    StatusResponseCallback responseCallback = new StatusResponseCallback("Save new flow", 201) {
                        @Override
                        protected void onResponse(Response response) {
                            saveSuccess();
                            if (success != null)
                                success.accept(flowToSave);
                        }
                    };

                    sendRequest(resource("flow").post().json(FLOW_CODEC.encode(flowToSave)), responseCallback);
                };
            } else {
                saveCallback = () -> {
                    // Don't send dependencies to the server
                    flowToSave.clearDependencies();

                    StatusResponseCallback responseCallback = new StatusResponseCallback("Save flow", 204) {
                        @Override
                        protected void onResponse(Response response) {
                            saveSuccess();
                            if (success != null)
                                success.accept(flowToSave);
                        }
                    };

                    sendRequest(resource("flow", flowToSave.getId()).put().json(FLOW_CODEC.encode(flowToSave)),
                            responseCallback);
                };
            }

            new CheckDependenciesProcedure(flowToSave, false, affectedFlows -> {
                if (affectedFlows == null) {
                    saveCallback.call();
                } else {
                    String confirmationText = "Are you sure you want to save changes to flow '"
                            + flowToSave.getDefaultedLabel() + "'? " + affectedFlows;
                    dispatch(new ConfirmationEvent("Save Breaking Changes", confirmationText, saveCallback));
                }
            }).call();
        }

        protected void saveSuccess() {
            if (flow != null && flow.getId().equals(flowToSave.getId())) {
                setFlowUnsaved(false);
                setFlowDirty(false);
                sendConsoleRefresh(flow);
            }
            dispatch(new FlowSavedEvent(flowToSave));
            dispatch(new ShowInfoEvent("Flow '" + flowToSave.getDefaultedLabel() + "' saved."));
        }
    }

    protected class DeleteFlowProcedure implements Callback {

        final Flow flowToDelete;

        public DeleteFlowProcedure(Flow flowToDelete) {
            this.flowToDelete = flowToDelete;
        }

        @Override
        public void call() {

            StringBuilder confirmationText = new StringBuilder();
            confirmationText.append("Are you sure you want to delete flow '")
                    .append(flowToDelete.getDefaultedLabel()).append("'?");

            // Remove all the consumers and producers to test what dependencies will be broken
            flowToDelete.removeProducerConsumerNodes();

            new CheckDependenciesProcedure(flowToDelete, true, affectedFlows -> {

                if (affectedFlows != null)
                    confirmationText.append(" ").append(affectedFlows);

                dispatch(new ConfirmationEvent("Remove Flow", confirmationText.toString(), new Callback() {
                    @Override
                    public void call() {

                        StatusResponseCallback responseCallback = new StatusResponseCallback("Delete flow", 204) {
                            @Override
                            protected void onResponse(Response response) {
                                deleteSuccess();
                            }

                            @Override
                            public void onFailure(RequestFailure requestFailure) {
                                super.onFailure(requestFailure);
                                deleteFailure(requestFailure);
                            }
                        };

                        sendRequest(false, false, resource("flow", flowToDelete.getId()).delete(),
                                responseCallback);
                    }
                }));
            }).call();
        }

        protected void deleteSuccess() {
            dispatch(new ShowInfoEvent("Flow '" + flowToDelete.getDefaultedLabel() + "' deleted."));
            dispatch(new FlowDeletedEvent(flowToDelete));
        }

        protected void deleteFailure(RequestFailure requestFailure) {
            if (requestFailure.statusCode == 409) {
                dispatch(new ShowFailureEvent(
                        "Flow '" + flowToDelete.getLabel()
                                + "' can't be deleted, stopping it failed or it's still in use by other flows.",
                        5000));
            } else {
                dispatch(new RequestFailureEvent(requestFailure));
            }
        }
    }

    protected class UpdateDependenciesProcedure implements Callback {

        final Flow flowToUpdate;
        final boolean hydrateSubs;
        final Callback success;

        public UpdateDependenciesProcedure(Flow flowToUpdate, boolean hydrateSubs, Callback success) {
            this.flowToUpdate = flowToUpdate;
            this.hydrateSubs = hydrateSubs;
            this.success = success;
        }

        @Override
        public void call() {
            // Don't send dependencies to the server
            flowToUpdate.clearDependencies();

            ObjectResponseCallback<Flow> responseCallback = new ObjectResponseCallback<Flow>(
                    "Get flow dependencies", FLOW_CODEC) {
                @Override
                protected void onResponse(Flow resolvedFlow) {
                    flowToUpdate.setSubDependencies(resolvedFlow.getSubDependencies());
                    flowToUpdate.setSuperDependencies(resolvedFlow.getSuperDependencies());
                    success.call();
                }

                @Override
                public void onFailure(RequestFailure requestFailure) {
                    super.onFailure(requestFailure);
                    dependencyFailure(requestFailure);
                }
            };

            LOG.debug("Resolving and updating dependencies: " + flowToUpdate);
            sendRequest(false, false,
                    resource("flow", "resolve").addQueryParam("hydrateSubs", Boolean.toString(hydrateSubs)).post()
                            .json(FLOW_CODEC.encode(flowToUpdate)),
                    responseCallback);
        }

        protected void dependencyFailure(RequestFailure requestFailure) {
            if (requestFailure.statusCode == 409) {
                dispatch(new ShowFailureEvent("Can't create an endless loop between flows.", 5000));
            } else {
                dispatch(new RequestFailureEvent(requestFailure));
            }
        }

    }

    protected class CheckDependenciesProcedure implements Callback {

        final Flow flowToCheck;
        final boolean isDelete;
        final Consumer<String> consumer;

        public CheckDependenciesProcedure(Flow flowToCheck, boolean isDelete, Consumer<String> consumer) {
            this.flowToCheck = flowToCheck;
            this.isDelete = isDelete;
            this.consumer = consumer;
        }

        @Override
        public void call() {
            // Find out what's broken and build a text that can be shown to the user
            new UpdateDependenciesProcedure(flowToCheck, false, () -> {
                FlowDependency[] superDependencies = flowToCheck.getDirectSuperDependencies();

                StringBuilder affectedFlows = new StringBuilder();
                for (FlowDependency superDependency : superDependencies) {
                    if (isDelete || superDependency.isPeersInvalid()) {
                        affectedFlows.append(" ");
                        affectedFlows.append(superDependency.getDefaultedLabel().toUpperCase(Locale.ROOT));
                        affectedFlows.append(",");
                    }
                }
                if (affectedFlows.length() > 0) {
                    affectedFlows.deleteCharAt(affectedFlows.length() - 1);
                    if (superDependencies.length > 0) {
                        consumer.accept(
                                "The following flows will be stopped and automatically changed:" + affectedFlows);
                    }
                } else {
                    consumer.accept(null);
                }
            }).call();
        }
    }

}