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

org.bidib.wizard.server.controllers.NodesController Maven / Gradle / Ivy

There is a newer version: 2.0.29
Show newest version
package org.bidib.wizard.server.controllers;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.bidib.api.json.types.GetNodesRequest;
import org.bidib.api.json.types.NodeAddress;
import org.bidib.api.json.types.NodeInfo;
import org.bidib.api.json.types.NodeInfo.NodeAction;
import org.bidib.api.json.types.NodeInfoResponse;
import org.bidib.api.json.types.SystemError;
import org.bidib.api.json.types.booster.BoosterState.BoosterStateType;
import org.bidib.api.json.types.booster.BoosterState.CommandStationStateType;
import org.bidib.api.json.types.booster.BoosterStateAction;
import org.bidib.api.json.types.booster.BoosterStateQuery;
import org.bidib.api.json.types.commandstation.CommandStationStateAction;
import org.bidib.api.json.types.commandstation.CommandStationStateQuery;
import org.bidib.jbidibc.messages.utils.NodeUtils;
import org.bidib.wizard.api.model.NodeInterface;
import org.bidib.wizard.api.model.NodeProvider;
import org.bidib.wizard.api.notification.NodeUpdate;
import org.bidib.wizard.api.service.node.BoosterService;
import org.bidib.wizard.api.service.node.CommandStationService;
import org.bidib.wizard.api.utils.JsonNodeUtils;
import org.bidib.wizard.core.model.connection.ConnectionRegistry;
import org.bidib.wizard.core.service.ConnectionUtils;
import org.bidib.wizard.model.status.BoosterStatus;
import org.bidib.wizard.model.status.CommandStationStatus;
import org.bidib.wizard.server.aspect.LogExecutionTime;
import org.bidib.wizard.server.config.StompDestinations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.subjects.PublishSubject;
import io.reactivex.rxjava3.subjects.Subject;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/nodes")
public class NodesController {

    private static final Logger LOGGER = LoggerFactory.getLogger(NodesController.class);

    private static final Logger LOGGER_EVENT = LoggerFactory.getLogger("EVENT");

    @Autowired
    private ConnectionRegistry connectionRegistry;

    @Autowired
    private CommandStationService commandStationService;

    @Autowired
    private BoosterService boosterService;

    // The SimpMessagingTemplate is used to send Stomp over WebSocket messages.
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    private Map mapConnectionDisposable = new HashMap<>();

    private final CompositeDisposable connectionRegistryDisposable = new CompositeDisposable();

    @PostConstruct
    public void initialize() {

        final NodesController nodesController = this;

        // subscribe to changes of connections
        Disposable connectionRegistryDisposable = connectionRegistry.getConnectionRegistrySubject().subscribe(event -> {
            LOGGER.info("The connection regitry subject has signalled an event: {}", event);

            String connectionId = event.getConnectionId();
            switch (event.getConnectionRegistryState()) {
                case ADD:
                    final NodeProvider nodeProvider =
                        ConnectionUtils.findConnection(connectionRegistry, connectionId).getNodeProvider();
                    nodesController.subscribeToNodeProvider(connectionId, nodeProvider);
                    break;
                default:
                    // TODO unsubscribe
                    CompositeDisposable disposable = mapConnectionDisposable.remove(connectionId);
                    if (disposable != null) {
                        LOGGER.info("Dispose the disposable: {}", disposable);
                        LOGGER_EVENT.info("Dispose the disposable: {}", disposable);
                        disposable.dispose();
                        disposable.clear();
                    }
                    else {
                        LOGGER.warn("No disposable to remove for connectionId: {}", connectionId);
                    }
                    break;
            }

        }, ex -> {
            LOGGER.warn("The connection regitry subject has signalled an error: {}", ex);
        }, () -> {
            LOGGER.info("The connection regitry subject has completed.");
        });

        this.connectionRegistryDisposable.add(connectionRegistryDisposable);
    }

    private static class NotificationEvent {
        final NodeUpdate nodeUpdate;

        final String connectionId;

        public NotificationEvent(String connectionId, final NodeUpdate nodeUpdate) {
            this.connectionId = connectionId;
            this.nodeUpdate = nodeUpdate;
        }

        public NodeUpdate getNodeUpdate() {
            return nodeUpdate;
        }

        public String getConnectionId() {
            return connectionId;
        }

        @Override
        public boolean equals(Object obj) {

            if (!(obj instanceof NotificationEvent)) {
                return false;
            }
            NotificationEvent other = (NotificationEvent) obj;
            if (!other.connectionId.equals(connectionId)) {
                return false;
            }
            if (other.nodeUpdate.getNode().getUniqueId() != nodeUpdate.getNode().getUniqueId()) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            return connectionId.hashCode() + nodeUpdate.hashCode();
        }

        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }
    }

    private final Subject notificationSubject = PublishSubject. create();

    /**
     * Subscribe to changes of the node list.
     * 
     * @param connectionId
     *            the connection id
     * @param nodeProvider
     *            the node provider
     */
    public void subscribeToNodeProvider(String connectionId, final NodeProvider nodeProvider) {

        LOGGER.info("Subscribe to node list changes of nodeProvider: {}", nodeProvider);
        LOGGER_EVENT.info("Subscribe to node list changes of nodeProvider: {}", nodeProvider);

        CompositeDisposable disp = mapConnectionDisposable.get(connectionId);
        if (disp == null) {
            disp = new CompositeDisposable();
            CompositeDisposable prev = mapConnectionDisposable.put(connectionId, disp);
            LOGGER.info("Added new composite disposabe, prev: {}", prev);

            if (prev != null) {
                LOGGER.warn("Replaced previous registered composite disposabe, prev: {}, disp: {}", prev, disp);
            }
        }

        CompositeDisposable disposable = disp;

        LOGGER.info("Create new buffer for the notificationSubject: {}", notificationSubject);

        // create an aggregator
        Observable> buffer = notificationSubject.buffer(1000, TimeUnit.MILLISECONDS);

        Disposable dispBufferMap = buffer.map(list -> {
            // LOGGER_EVENT.info(">>> before distinct: {}", list);
            List after = list.stream().distinct().collect(Collectors.toList());
            // LOGGER_EVENT.info(">>> after distinct: {}", after);

            return after;
        }).subscribe(list -> {

            for (NotificationEvent ne : list) {
                try {
                    final NodeInfo ni = JsonNodeUtils.toNodeInfo(ne.nodeUpdate.getNode());
                    ni.setNodeAction(ne.nodeUpdate.getNodeAction());

                    NodesController.this.notify(ne.getConnectionId(), ni);
                }
                catch (Exception ex) {
                    LOGGER_EVENT.warn("Publish nodeInfo failed, event: {}", ne, ex);
                }
            }
        });

        disposable.add(dispBufferMap);

        LOGGER_EVENT.info("Subscribe to nodeList changes on nodeProvider: {}", nodeProvider);
        nodeProvider.subscribeNodeListChanges(new Observer() {

            @Override
            public void onSubscribe(Disposable nodeListDisposable) {
                // this is called with a PublishDisposable that can be used to unsubscribe
                LOGGER
                    .info("Subscribed to node list changes, disposable.isDisposed: {}",
                        nodeListDisposable.isDisposed());

                if (!nodeListDisposable.isDisposed()) {
                    LOGGER.info("Add disposable of nodeListChanges: {}", nodeListDisposable);
                    disposable.add(nodeListDisposable);

                }
                else {
                    LOGGER.warn("The subscriber is disposed already!");
                }
            }

            @Override
            public void onNext(NodeUpdate nodeUpdate) {
                LOGGER.info("New item received from node list: {}", nodeUpdate);

                notificationSubject.onNext(new NotificationEvent(nodeUpdate.getConnectionId(), nodeUpdate));
            }

            @Override
            public void onError(Throwable e) {
                LOGGER.info("Subscription to node list changes signalled an error: {}", e);
                notificationSubject.onError(e);
            }

            @Override
            public void onComplete() {
                LOGGER.info("The subscription to node list changes has finished.");
                notificationSubject.onComplete();
            }
        });
    }

    @PreDestroy
    public void shutdown() {
        LOGGER.info("Shutdown the NodesController.");

        for (Disposable disposable : mapConnectionDisposable.values()) {
            if (disposable != null) {
                disposable.dispose();
            }
        }
        mapConnectionDisposable.clear();

        if (connectionRegistryDisposable != null) {
            connectionRegistryDisposable.dispose();
        }
    }

    @MessageMapping(StompDestinations.APP_NODES_NODES_DESTINATION)
    @SendToUser(destinations = StompDestinations.REPLY_NODES_NODES_DESTINATION, broadcast = false)
    public NodeInfoResponse msgRequestAllNodes(GetNodesRequest getNodesRequest) {
        LOGGER.info(">>> Request all nodes, getNodesRequest: {}", getNodesRequest);

        String connectionId = getNodesRequest.getConnectionId();

        final NodeProvider nodeProvider =
            ConnectionUtils.findConnection(connectionRegistry, connectionId).getNodeProvider();

        List nodeInfos = fetchAllNodes(connectionId, nodeProvider);
        for (NodeInfo nodeInfo : nodeInfos) {
            nodeInfo.setNodeAction(NodeAction.UPDATE);
        }

        NodeInfoResponse nodeInfoResponse = new NodeInfoResponse(connectionId, nodeInfos);

        LOGGER.info(">>> Send all nodes: {}", nodeInfoResponse);
        return nodeInfoResponse;
    }

    @GetMapping(path = "/all")
    public ResponseEntity getAllNodes(String connectionId) {
        LOGGER.info("Get all nodes.");

        try {
            final NodeProvider nodeProvider =
                ConnectionUtils.findConnection(connectionRegistry, connectionId).getNodeProvider();
            List nodeInfos = fetchAllNodes(connectionId, nodeProvider);
            for (NodeInfo nodeInfo : nodeInfos) {
                nodeInfo.setNodeAction(NodeAction.UPDATE);
            }

            NodeInfoResponse nodeInfoResponse = new NodeInfoResponse(connectionId, nodeInfos);
            return ResponseEntity.ok().body(nodeInfoResponse);
        }
        catch (NoSuchElementException ex) {
            return ResponseEntity.noContent().build();
        }
    }

    private List fetchAllNodes(final String connectionId, final NodeProvider nodeProvider) {
        List nodeInfoList = Observable.fromCallable(new Callable>() {

            @Override
            public Collection call() throws Exception {
                return nodeProvider.getNodes();
            }
        }).map(nodes -> {
            LOGGER.info("Recevied nodes from nodeProvider: {}", nodes);
            List nodeInfos = new LinkedList<>();
            for (NodeInterface node : nodes) {
                NodeInfo ni = JsonNodeUtils.toNodeInfo(node);

                nodeInfos.add(ni);
            }
            return nodeInfos;
        }).blockingSingle();
        return nodeInfoList;
    }

    @GetMapping(path = "/booster")
    public ResponseEntity getBoosterNodes(String connectionId) {
        LOGGER.info("Get booster nodes.");

        List nodeInfos = Observable.fromCallable(new Callable>() {

            @Override
            public Collection call() throws Exception {
                final NodeProvider nodeProvider =
                    ConnectionUtils.findConnection(connectionRegistry, connectionId).getNodeProvider();
                return nodeProvider.getNodes();
            }
        }).map(nodes -> {
            LOGGER.info("Recevied nodes from nodeProvider: {}", nodes);
            List nodeInfoList = new LinkedList<>();
            for (NodeInterface node : nodes) {

                // only return boosters
                if (NodeUtils.hasBoosterFunctions(node.getUniqueId())) {

                    NodeInfo ni = JsonNodeUtils.toNodeInfo(node);

                    LOGGER.info("The current node has booster functions: {}", ni);
                    nodeInfoList.add(ni);
                }
            }
            return nodeInfoList;
        }).onErrorReturnItem(Collections.emptyList()).blockingSingle();

        if (CollectionUtils.isNotEmpty(nodeInfos)) {
            NodeInfoResponse nodeInfoResponse = new NodeInfoResponse(connectionId, nodeInfos);
            return ResponseEntity.ok().body(nodeInfoResponse);
        }
        return ResponseEntity.noContent().build();
    }

    @PutMapping(path = "/booster/state")
    public void setBoosterState(BoosterStateAction boosterStateAction) {
        LOGGER.info("Set the booster state: {}", boosterStateAction);

        String connectionId = boosterStateAction.getConnectionId();
        NodeInfo nodeInfo = boosterStateAction.getNode();
        final BoosterStateType boosterState = boosterStateAction.getState();
        final BoosterStatus boosterStatus = BoosterStatus.valueOf(boosterState.name());

        boosterService.setBoosterState(connectionId, nodeInfo, boosterStatus);
    }

    @PostMapping(path = "/booster/state")
    public void queryBoosterState(BoosterStateQuery boosterStateQuery) {
        LOGGER.info("Query the booster state: {}", boosterStateQuery);

        String connectionId = boosterStateQuery.getConnectionId();
        NodeInfo nodeInfo = boosterStateQuery.getNode();

        boosterService.queryBoosterState(connectionId, nodeInfo);
    }

    @MessageMapping(StompDestinations.APP_NODES_BOOSTER_SET_STATE_DESTINATION)
    @LogExecutionTime
    public void msgSetBoosterState(BoosterStateAction boosterStateAction) {
        LOGGER.info("Set the booster state: {}", boosterStateAction);

        String connectionId = boosterStateAction.getConnectionId();
        NodeInfo nodeInfo = boosterStateAction.getNode();
        final BoosterStateType boosterState = boosterStateAction.getState();
        final BoosterStatus boosterStatus = BoosterStatus.valueOf(boosterState.name());

        boosterService.setBoosterState(connectionId, nodeInfo, boosterStatus);
    }

    @MessageMapping(StompDestinations.APP_NODES_BOOSTER_QUERY_STATE_DESTINATION)
    @LogExecutionTime
    public void msgQueryBoosterState(BoosterStateQuery boosterStateQuery) {
        LOGGER.info("Query the booster state: {}", boosterStateQuery);

        String connectionId = boosterStateQuery.getConnectionId();
        NodeInfo nodeInfo = boosterStateQuery.getNode();

        boosterService.queryBoosterState(connectionId, nodeInfo);
    }

    @PutMapping(path = "/commandstation/state")
    public void setCommandStationState(CommandStationStateAction commandStationStateAction) {
        LOGGER.info("Set the commandStation state: {}", commandStationStateAction);

        String connectionId = commandStationStateAction.getConnectionId();
        NodeAddress nodeInfo = commandStationStateAction.getNode();
        final CommandStationStateType commandStationStateType = commandStationStateAction.getState();
        final CommandStationStatus commandStationState = CommandStationStatus.valueOf(commandStationStateType.name());

        commandStationService.setCommandStationState(connectionId, nodeInfo, commandStationState);
    }

    @MessageMapping(StompDestinations.APP_NODES_COMMANDSTATION_SET_STATE_DESTINATION)
    @LogExecutionTime
    public void msgSetCommandStationState(CommandStationStateAction commandStationStateAction) {
        LOGGER.info("Set the commandStation state: {}", commandStationStateAction);

        String connectionId = commandStationStateAction.getConnectionId();
        NodeAddress nodeInfo = commandStationStateAction.getNode();
        final CommandStationStateType commandStationStateType = commandStationStateAction.getState();
        final CommandStationStatus commandStationState = CommandStationStatus.valueOf(commandStationStateType.name());

        commandStationService.setCommandStationState(connectionId, nodeInfo, commandStationState);
    }

    @PostMapping(path = "/commandstation/state")
    public void queryCommandStationState(CommandStationStateQuery commandStationStateQuery) {
        LOGGER.info("Query the commandStation state: {}", commandStationStateQuery);

        String connectionId = commandStationStateQuery.getConnectionId();
        NodeAddress nodeInfo = commandStationStateQuery.getNode();

        commandStationService.queryCommandStationState(connectionId, nodeInfo);
    }

    @MessageMapping(StompDestinations.APP_NODES_COMMANDSTATION_QUERY_STATE_DESTINATION)
    @LogExecutionTime
    public void msgQueryCommandStationState(CommandStationStateQuery commandStationStateQuery) {
        LOGGER.info("Query the commandStation state: {}", commandStationStateQuery);

        String connectionId = commandStationStateQuery.getConnectionId();
        NodeAddress nodeInfo = commandStationStateQuery.getNode();

        commandStationService.queryCommandStationState(connectionId, nodeInfo);
    }

    /**
     * Send notification to users subscribed on channel "/topic/nodes".
     *
     * The message will be sent only to the user with the given username.
     * 
     * @param connectionId
     *            The connectionId.
     * @param nodeInfo
     *            The nodeInfo.
     */
    private void notify(String connectionId, NodeInfo nodeInfo) {
        LOGGER.info("Notify changed node, connectionId: {}, node : {}", connectionId, nodeInfo);

        NodeInfoResponse nodeInfoResponse = new NodeInfoResponse(connectionId, Arrays.asList(nodeInfo));

        messagingTemplate.convertAndSend(StompDestinations.PUBLISH_NODES_NODES_DESTINATION, nodeInfoResponse);
    }

    @MessageExceptionHandler
    public String handleException(Throwable exception) {
        LOGGER.error("Error detected: ", exception);

        final SystemError se = new SystemError(exception.getMessage(), null, LocalDateTime.now(), null);
        messagingTemplate.convertAndSend(StompDestinations.SYSTEM_ERROR_DESTINATION, se);
        return exception.getMessage();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy