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

org.bidib.wizard.server.controllers.MessageController 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.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();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy