org.bidib.wizard.server.controllers.MessageController 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import org.bidib.api.json.types.ConnectionRegistryEvent;
import org.bidib.api.json.types.LogInfo;
import org.bidib.api.json.types.RxTxMessage;
import org.bidib.api.json.types.RxTxMessage.RxTxDirection;
import org.bidib.api.json.types.SystemError;
import org.bidib.api.json.types.booster.BoostDiagnostic;
import org.bidib.api.json.types.booster.BoosterState;
import org.bidib.api.json.types.occupancy.OccupancyStatusResponse;
import org.bidib.api.json.types.switching.PortConfigResponse;
import org.bidib.api.json.types.switching.PortStatusResponse;
import org.bidib.wizard.api.model.NodeInterface;
import org.bidib.wizard.api.model.NodeProvider;
import org.bidib.wizard.api.model.connection.AbstractMessageEvent;
import org.bidib.wizard.api.model.connection.AbstractNodeQueueEvent;
import org.bidib.wizard.api.model.connection.AbstractQueueEvent;
import org.bidib.wizard.api.model.connection.BidibConnection;
import org.bidib.wizard.api.model.connection.event.AccessoryStateMessageEvent;
import org.bidib.wizard.api.model.connection.event.StringMessageEvent;
import org.bidib.wizard.api.model.event.NodeActionEvent;
import org.bidib.wizard.api.model.event.NodeActionEvent.ActionIdentifier;
import org.bidib.wizard.api.model.event.NodeStatusEvent;
import org.bidib.wizard.common.utils.TimeConversionUtils;
import org.bidib.wizard.core.logger.BidibLogsAppender;
import org.bidib.wizard.core.model.connection.ConnectionRegistry;
import org.bidib.wizard.core.model.connection.MessageEventHandler;
import org.bidib.wizard.core.model.connection.MessageQueue;
import org.bidib.wizard.core.service.ConnectionUtils;
import org.bidib.wizard.model.ports.event.FeedbackPortStatusEvent;
import org.bidib.wizard.model.ports.event.PortConfigEvent;
import org.bidib.wizard.model.ports.event.PortStatusEvent;
import org.bidib.wizard.server.config.StompDestinations;
import org.bidib.wizard.server.controllers.actions.BoosterDiagAction;
import org.bidib.wizard.server.controllers.actions.BoosterStatusAction;
import org.bidib.wizard.server.controllers.actions.CommandStationStatusAction;
import org.bidib.wizard.server.controllers.actions.FeedbackPortAction;
import org.bidib.wizard.server.controllers.actions.NodeStatusAction;
import org.bidib.wizard.server.controllers.actions.PortConfigAction;
import org.bidib.wizard.server.controllers.actions.PortStatusAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
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.BehaviorSubject;
import io.reactivex.rxjava3.subjects.PublishSubject;
import io.reactivex.rxjava3.subjects.Subject;
@CrossOrigin(origins = "*")
@Controller
public class MessageController {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageController.class);
private static final Logger LOGGER_EVENT = LoggerFactory.getLogger("PORT_CONFIG_EVENT");
@Autowired
private ConnectionRegistry connectionRegistry;
// The SimpMessagingTemplate is used to send Stomp over WebSocket messages.
@Autowired
private SimpMessagingTemplate messagingTemplate;
// the disposable of the message subscription
private Map messageSubscriptionMap = new HashMap<>();
// the disposable for the RXTX subscription
private Disposable loggingDisposable;
private MessageQueue messageQueue;
private final Subject portConfigEventSubject = PublishSubject. create();
private final CompositeDisposable disposable = new CompositeDisposable();
public MessageController() {
}
@EventListener
public void handleContextStart(ApplicationStartedEvent ase) {
LOGGER.info("The application is started. Initialize the MessageController.");
prepareMessageMap();
// create the message event handler
final MessageEventHandler messageEventHandler =
new MessageEventHandler() {
@Override
public void handleMessageEvent(AbstractQueueEvent event) {
LOGGER.debug("Handle the message event: {}", event);
// process the event
processMessageEvent(event);
}
};
// create the message queue
this.messageQueue = new MessageQueue<>(messageEventHandler);
this.messageQueue.startReceiveQueueWorker();
final BehaviorSubject connectionRegistrySubject =
connectionRegistry.getConnectionRegistrySubject();
connectionRegistrySubject.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
LOGGER.info("Subscribed to connection registry changes, disposed: {}", d.isDisposed());
}
@Override
public void onNext(ConnectionRegistryEvent evt) {
LOGGER.info("New item received from connection registry: {}", evt);
try {
String connectionId = evt.getConnectionId();
if (ConnectionRegistryEvent.ConnectionRegistryState.ADD.equals(evt.getConnectionRegistryState())) {
LOGGER
.info("The main connection was added. Fetch the '{}' connection from the registry.",
connectionId);
addMessageListener(connectionId);
}
else if (ConnectionRegistryEvent.ConnectionRegistryState.REMOVE
.equals(evt.getConnectionRegistryState())) {
LOGGER
.info(
"The main connection was removed. Remove the message listener from the '{}' connection and release the connection.",
connectionId);
removeMessageListener(connectionId);
}
}
catch (Exception ex) {
LOGGER.warn("Handle connection registry event failed: {}", evt, ex);
}
}
@Override
public void onError(Throwable e) {
LOGGER.info("Subscription to connection registry changes has thrown an error: {}", e);
}
@Override
public void onComplete() {
LOGGER.info("The subscription to connection registry changes has finished.");
}
});
// create an aggregator
Observable> buffer = portConfigEventSubject.buffer(1000, TimeUnit.MILLISECONDS);
Disposable dispBufferMap = buffer.map(list -> {
LOGGER_EVENT.debug(">>> before distinct: {}", list.size());
List after = list.stream().distinct().collect(Collectors.toList());
LOGGER_EVENT.debug(">>> after distinct: {}", after.size());
return after;
}).subscribe(list -> {
for (PortConfigEvent pce : list) {
try {
messageQueue
.queueMessageEvent(
new PortConfigAction(pce.getConnectionId(), pce.getUniqueId(), pce.getPort()));
}
catch (Exception ex) {
LOGGER_EVENT.warn("Publish nodeInfo failed, event: {}", pce, ex);
}
}
});
disposable.add(dispBufferMap);
addRxTxLogAppender();
}
@PreDestroy
public void shudown() {
LOGGER.info("Shutdown the MessageController.");
this.messageQueue.stopReceiveQueue();
LOGGER.info("The message queue was stopped.");
if (loggingDisposable != null) {
loggingDisposable.dispose();
}
disposable.dispose();
}
private void addRxTxLogAppender() {
LOGGER.info("Add the bidib logs appender to RX and TX logger.");
try {
// assume SLF4J is bound to logback in the current environment
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
// ch.qos.logback.classic.Logger log = context.getLogger("org.bidib");
ch.qos.logback.classic.Logger logTX = context.getLogger("TX");
ch.qos.logback.classic.Logger logRX = context.getLogger("RX");
Appender appender = logTX.getAppender(BidibLogsAppender.APPENDER_NAME);
if (appender == null) {
BidibLogsAppender bidibLogsAppender = new BidibLogsAppender();
bidibLogsAppender.setContext(context);
bidibLogsAppender.setName(BidibLogsAppender.APPENDER_NAME);
bidibLogsAppender.start();
logTX.setAdditive(false);
logTX.setLevel(Level.INFO);
logTX.addAppender(bidibLogsAppender);
logRX.setAdditive(false);
logRX.setLevel(Level.INFO);
logRX.addAppender(bidibLogsAppender);
loggingDisposable = bidibLogsAppender.getLoggingSubject().subscribe(le -> {
final RxTxMessage rxTxMessage = new RxTxMessage();
rxTxMessage.setDirection("TX".equals(le.getLoggerName()) ? RxTxDirection.Out : RxTxDirection.In);
rxTxMessage.setTimestamp(TimeConversionUtils.convertMillisToLocalDateTime(le.getTimeStamp()));
rxTxMessage.setData(le.getMessage());
publishRxtxMessage(rxTxMessage);
});
}
else {
LOGGER.info("BidibLogsAppender is already assigned.");
}
}
catch (Exception ex) {
LOGGER.info("Add BidibLogsAppender to RX and TX logger failed.");
}
}
private void addMessageListener(String connectionId) {
LOGGER.info("Add message listener for connectionId: {}", connectionId);
if (messageSubscriptionMap.containsKey(connectionId)) {
LOGGER.warn("The message listener is registered already, connectionId : {}", connectionId);
removeMessageListener(connectionId);
}
final BidibConnection bidibConnection = connectionRegistry.getConnection(connectionId);
if (bidibConnection != null) {
CompositeDisposable disposable = new CompositeDisposable();
messageSubscriptionMap.put(connectionId, disposable);
Disposable dispPortEventSubscription = bidibConnection.subscribePortEvents(event -> {
LOGGER.info(">> Received a port event: {}", event);
if (event instanceof FeedbackPortStatusEvent) {
FeedbackPortStatusEvent evt = (FeedbackPortStatusEvent) event;
messageQueue
.queueMessageEvent(
new FeedbackPortAction(evt.getConnectionId(), evt.getUniqueId(), evt.getPort()));
}
else if (event instanceof PortStatusEvent) {
PortStatusEvent evt = (PortStatusEvent) event;
messageQueue
.queueMessageEvent(
new PortStatusAction(evt.getConnectionId(), evt.getUniqueId(), evt.getPort()));
}
else if (event instanceof PortConfigEvent) {
PortConfigEvent evt = (PortConfigEvent) event;
LOGGER_EVENT.info("Add new event: {}", evt);
portConfigEventSubject.onNext(evt);
// messageQueue
// .queueMessageEvent(
// new PortConfigAction(evt.getConnectionId(), evt.getUniqueId(), evt.getPort()));
}
}, err -> {
LOGGER.warn("The portEvent subscription signalled an error: {}", err);
}, () -> {
LOGGER.warn("The portEvent subscription has completed.");
}, null);
LOGGER
.info("Subscribed to portEvent subject, dispPortEventSubscription: {}",
dispPortEventSubscription.isDisposed());
disposable.add(dispPortEventSubscription);
Disposable dispNodeEventSubscription = bidibConnection.subscribeNodeEvents(event -> {
LOGGER.info(">> Received a node event: {}", event);
if (event instanceof NodeActionEvent) {
NodeActionEvent nae = (NodeActionEvent) event;
final ActionIdentifier actionIdentifier = nae.getActionIdentifier();
switch (actionIdentifier) {
case BoosterDiag:
messageQueue
.queueMessageEvent(new BoosterDiagAction(nae.getConnectionId(), nae.getUniqueId()));
break;
case BoosterStatus:
messageQueue
.queueMessageEvent(new BoosterStatusAction(nae.getConnectionId(), nae.getUniqueId()));
break;
case CommandStationStatus:
messageQueue
.queueMessageEvent(
new CommandStationStatusAction(nae.getConnectionId(), nae.getUniqueId()));
break;
default:
LOGGER.warn("The node event is not handled: {}", event);
break;
}
}
else if (event instanceof NodeStatusEvent) {
NodeStatusEvent nse = (NodeStatusEvent) event;
messageQueue.queueMessageEvent(new NodeStatusAction(nse.getConnectionId(), nse.getUniqueId()));
}
else {
LOGGER.warn("The node event is not handled: {}", event);
}
}, err -> {
LOGGER.warn("The nodeEvent subscription signalled an error: {}", err);
}, () -> {
LOGGER.warn("The nodeEvent subscription has completed.");
});
LOGGER
.info("Subscribed to nodeEvent subject, dispNodeEventSubscription: {}",
dispNodeEventSubscription.isDisposed());
disposable.add(dispNodeEventSubscription);
}
else {
LOGGER
.warn("No '{}' connection found in registry. Cannot add message listener for booster messages.",
connectionId);
}
}
private void removeMessageListener(String connectionId) {
LOGGER.info("Remove message listener for connectionId: {}", connectionId);
CompositeDisposable disposable = messageSubscriptionMap.remove(connectionId);
if (disposable != null) {
LOGGER.info("Dispose dispMessageSubscription: {}", disposable);
}
else {
LOGGER.warn("No message subscription to dispose found in map, connectionId: {}", connectionId);
}
}
protected void processMessageEvent(AbstractQueueEvent event) {
LOGGER.info("Received message event, event: {}", event);
String connectionId = event.getConnectionId();
// TODO change the messageActionMap to process only events from node and ports, remove the processing of
// AbstractMessageEvent
try {
MessageActions actions = messageActionMap.get(event.getClass());
if (actions != null) {
// find the matching node provider
NodeProvider nodeProvider =
ConnectionUtils.findConnection(connectionRegistry, connectionId).getNodeProvider();
if (event instanceof AbstractMessageEvent) {
byte[] address = ((AbstractMessageEvent) event).getAddress();
NodeInterface node = nodeProvider.findNodeByAddress(address);
actions.accept(event, node, this);
}
else if (event instanceof AbstractNodeQueueEvent) {
AbstractNodeQueueEvent abstractNodeQueueEvent = (AbstractNodeQueueEvent) event;
NodeInterface node = nodeProvider.findNodeByUniqueId(abstractNodeQueueEvent.getUniqueId());
actions.accept(abstractNodeQueueEvent, node, this);
}
else {
LOGGER.info("No action defined to handle message event: {}", event);
}
}
else {
LOGGER.info("No action defined to handle message event: {}", event);
}
}
catch (Exception ex) {
LOGGER.warn("Execute the message action failed, event: {}", event, ex);
}
}
private final Map, MessageActions> messageActionMap = new HashMap<>();
/**
* The message map contains the message mapping to publish the events to the clients over websocket.
*/
private void prepareMessageMap() {
LOGGER.info("Prepare the message map.");
// booster and command station
messageActionMap.put(BoosterDiagAction.class, MessageActions.BoosterDiagAction);
messageActionMap.put(BoosterStatusAction.class, MessageActions.BoosterStateAction);
messageActionMap.put(CommandStationStatusAction.class, MessageActions.CommandStationStateAction);
// node action
messageActionMap.put(NodeStatusAction.class, MessageActions.NodeStatusAction);
messageActionMap.put(StringMessageEvent.class, MessageActions.StringAction);
// switching
// messageActionMap.put(LcConfigXMessageEvent.class, MessageActions.LcConfigXAction);
// messageActionMap.put(LcConfigMessageEvent.class, MessageActions.LcConfigAction);
// messageActionMap.put(LcStatMessageEvent.class, MessageActions.LcStatAction);
// messageActionMap.put(LcNaMessageEvent.class, MessageActions.LcNaAction);
messageActionMap.put(PortConfigAction.class, MessageActions.PortConfigAction);
messageActionMap.put(PortStatusAction.class, MessageActions.PortStatusAction);
// accessory
messageActionMap.put(AccessoryStateMessageEvent.class, MessageActions.AccessoryStateAction);
// feedback port
messageActionMap.put(FeedbackPortAction.class, MessageActions.FeedbackPortAction);
}
void publishBoostDiagnostic(final BoostDiagnostic boostDiagnostic) {
LOGGER.info("Publish boostDiagnostic: {}", boostDiagnostic);
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_MESSAGES_BOOSTER_DIAG_DESTINATION,
new BoostDiagnostic[] { boostDiagnostic });
}
void publishBoosterState(final BoosterState boosterState) {
LOGGER.info("Publish boosterState: {}", boosterState);
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_NODES_BOOSTER_STATE_DESTINATION,
new BoosterState[] { boosterState });
}
public void publishPortStatus(PortStatusResponse portStatusResponse) {
LOGGER.info("Publish port status: {}", portStatusResponse);
String uniqueId = portStatusResponse.getNode().getUniqueId();
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_NODE_PORT_STATE_DESTINATION + "/" + uniqueId, portStatusResponse);
}
public void publishPortConfig(PortConfigResponse portConfigResponse) {
LOGGER.info("Publish port config: {}", portConfigResponse);
String uniqueId = portConfigResponse.getNode().getUniqueId();
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_NODE_PORT_CONFIG_DESTINATION + "/" + uniqueId,
portConfigResponse);
}
public void publishOccupancyStatus(OccupancyStatusResponse occupancyStatusResponse) {
LOGGER.info("Publish occupancy status: {}", occupancyStatusResponse);
String uniqueId = occupancyStatusResponse.getNode().getUniqueId();
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_NODE_OCCUPANCY_STATE_DESTINATION + "/" + uniqueId,
occupancyStatusResponse);
}
private void publishRxtxMessage(final RxTxMessage rxTxMessage) {
LOGGER.info("Publish rxtxMessage: {}", rxTxMessage);
messagingTemplate
.convertAndSend(StompDestinations.PUBLISH_MESSAGES_RXTX_DESTINATION, new RxTxMessage[] { rxTxMessage });
}
/**
* Send notification to users subscribed on channel "/topic/messages/logInfo".
*
* The message will be sent only to the user with the given username.
*
* @param logInfo
* The log info.
*/
public void notify(LogInfo logInfo) {
LOGGER.info("Send logInfo: {}", logInfo);
Map headers = new HashMap<>();
headers.put("simpUser", "guest");
messagingTemplate.convertAndSend(StompDestinations.PUBLISH_MESSAGES_LOG_INFO_DESTINATION, logInfo, headers);
return;
}
@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();
}
}