org.bidib.wizard.server.controllers.NodesController Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bidibwizard-server Show documentation
Show all versions of bidibwizard-server Show documentation
jBiDiB BiDiB Wizard Server POM
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();
}
}