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

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

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

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.bidib.api.json.types.ConnectionPhase;
import org.bidib.api.json.types.NodeStatusInformation;
import org.bidib.api.json.types.NodeStatusInformation.StatusLevel;
import org.bidib.api.json.types.SerialPortInfo;
import org.bidib.api.json.types.StatusInfo;
import org.bidib.api.json.types.SystemError;
import org.bidib.api.json.types.system.SystemAvailablePortsResponse;
import org.bidib.api.json.types.system.SystemInfoResponse;
import org.bidib.jbidibc.messages.helpers.Context;
import org.bidib.jbidibc.messages.helpers.DefaultContext;
import org.bidib.wizard.api.LookupService;
import org.bidib.wizard.api.model.common.CommPort;
import org.bidib.wizard.api.model.connection.BidibConnection;
import org.bidib.wizard.api.model.connection.ConnectionIdentifier;
import org.bidib.wizard.api.model.connection.ConnectionState;
import org.bidib.wizard.api.notification.ConnectionInfo;
import org.bidib.wizard.api.version.ApiVersion;
import org.bidib.wizard.common.exception.ConnectionException;
import org.bidib.wizard.core.model.connection.ConnectionRegistry;
import org.bidib.wizard.core.service.ConnectionService;
import org.bidib.wizard.core.service.LogFileService;
import org.bidib.wizard.core.service.SystemInfoService;
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.core.io.FileSystemResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.Observer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/system")
@Tag(name = "system", description = "the system API")
public class SystemController {
    private static final Logger LOGGER = LoggerFactory.getLogger(SystemController.class);

    @Autowired
    private ConnectionService connectionService;

    @Autowired
    private LogFileService logFileService;

    @Autowired
    private SystemInfoService systemInfoService;

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

    @Autowired
    private LookupService lookupService;

    public SystemController() {
    }

    @PostConstruct
    public void initialize() {
        String versionInfo = getClass().getPackage().getImplementationVersion();
        LOGGER.info("Initialize the SystemController, versionInfo: {}", versionInfo);

        final CompositeDisposable compDispNodeStatus = new CompositeDisposable();

        connectionService.subscribeNodeStatusChanges(new Observer() {

            @Override
            public void onSubscribe(Disposable d) {
                LOGGER.info("Subscribed to connection registry changes, disposed: {}", d.isDisposed());
                compDispNodeStatus.add(d);
            }

            @Override
            public void onNext(NodeStatusInformation evt) {
                LOGGER.info("New status received from connection registry: {}", evt);
                publishStatusInformation(evt);
            }

            @Override
            public void onError(Throwable e) {
                LOGGER
                    .info("Subscription to status information from connection registry changes has thrown an error: {}",
                        e);
                compDispNodeStatus.dispose();
            }

            @Override
            public void onComplete() {
                LOGGER
                    .info("The subscription to status information from for connection registry changes has finished.");
                compDispNodeStatus.dispose();
            }
        });

        final CompositeDisposable compDispConnectionStatus = new CompositeDisposable();

        connectionService.subscribeConnectionStatusChanges(new Observer() {

            @Override
            public void onSubscribe(Disposable d) {
                LOGGER.info("Subscribed to connection status changes, disposed: {}", d.isDisposed());
                compDispConnectionStatus.add(d);
            }

            @Override
            public void onNext(ConnectionInfo connectionInfo) {
                LOGGER.info("Publish the connection info: {}", connectionInfo);

                try {
                    boolean notifyStatus = false;

                    // Publish only connected and disconnected to web client
                    switch (connectionInfo.getConnectionState().getActualPhase()) {
                        case CONNECTED:
                        case DISCONNECTED:
                            notifyStatus = true;
                            break;
                        default:
                            break;
                    }

                    if (notifyStatus) {

                        // create the JSON connection info
                        org.bidib.api.json.types.ConnectionInfo jsonConnectionInfo =
                            toJsonConnectionInfo(connectionInfo.getConnectionId(),
                                connectionInfo.getConnectionState().getActualPhase());

                        messagingTemplate
                            .convertAndSend(StompDestinations.PUBLISH_SYSTEM_CONNECTIONSTATE_DESTINATION,
                                jsonConnectionInfo);
                    }
                }
                catch (Exception ex) {
                    LOGGER.warn("Send the connection state failed: {}", connectionInfo, ex);
                }
            }

            @Override
            public void onError(Throwable e) {
                LOGGER.info("Subscription to connection status information changes has thrown an error: {}", e);
                compDispConnectionStatus.dispose();
            }

            @Override
            public void onComplete() {
                LOGGER.info("The subscription to connection status information changes has finished.");
                compDispConnectionStatus.dispose();
            }
        });

        systemInfoService.subscribeUsbPortEvents(upe -> {
            LOGGER.info("Publish the USB port event: {}", upe);

            messagingTemplate.convertAndSend(StompDestinations.SYSTEM_USBPORTCHANGE_DESTINATION, upe);
        }, error -> {
            LOGGER.warn("The USB port event signalled a failure: {}", error);
        });
    }

    @GetMapping(path = "/info")
    public ResponseEntity getSystemInfo() {

        try {
            String apiVersion = ApiVersion.getVersion();
            String versionNumber = ApiVersion.getBuildNumber();
            LOGGER.info("Current version: {}, versionNumber: {}", apiVersion, versionNumber);

            return new ResponseEntity<>(new SystemInfoResponse(Integer.parseInt(versionNumber), apiVersion),
                HttpStatus.OK);
        }
        catch (RuntimeException ex) {
            LOGGER.warn("No system info available.", ex);
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @MessageMapping("/info")
    @SendToUser(destinations = StompDestinations.SYSTEM_INFO_DESTINATION, broadcast = false)
    @LogExecutionTime
    public SystemInfoResponse msgGetSystemInfo() {

        String apiVersion = ApiVersion.getVersion();
        String versionNumber = ApiVersion.getBuildNumber();
        LOGGER.info("Current version: {}, versionNumber: {}", apiVersion, versionNumber);

        return new SystemInfoResponse(Integer.parseInt(versionNumber), apiVersion);
    }

    @GetMapping(path = "/availablePorts")
    public ResponseEntity getAvailablePorts() {

        try {
            LOGGER.info("Get the currently available serial ports.");

            List serialPorts = new ArrayList<>();

            for (CommPort commPort : lookupService.getDetectedComPorts()) {
                SerialPortInfo pi =
                    new SerialPortInfo(commPort.getName(), commPort.getSerialNumber(), commPort.getVendorId(),
                        commPort.getProductId(), commPort.getDescription(), commPort.getManufacturerString());
                serialPorts.add(pi);
            }

            return new ResponseEntity<>(new SystemAvailablePortsResponse(serialPorts), HttpStatus.OK);
        }
        catch (RuntimeException ex) {
            LOGGER.warn("No system info available.", ex);
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @MessageMapping("/availablePorts")
    @SendToUser(destinations = StompDestinations.SYSTEM_AVAILABLEPORTS_DESTINATION, broadcast = false)
    @LogExecutionTime
    public SystemAvailablePortsResponse msgGetAvailablePorts() {

        LOGGER.info("Get the currently available serial ports.");

        List serialPorts = new ArrayList<>();

        for (CommPort commPort : lookupService.getDetectedComPorts()) {
            SerialPortInfo pi =
                new SerialPortInfo(commPort.getName(), commPort.getSerialNumber(), commPort.getVendorId(),
                    commPort.getProductId(), commPort.getDescription(), commPort.getManufacturerString());
            serialPorts.add(pi);
        }

        return new SystemAvailablePortsResponse(serialPorts);
    }

    @GetMapping(path = "/currentState")
    public ResponseEntity getCurrentState() {

        String name = ConnectionRegistry.CONNECTION_ID_MAIN;
        LOGGER.debug("Get connection status for name: {}", name);

        return getCurrentState(name);
    }

    @GetMapping(path = "/currentState/{connectionId}")
    public ResponseEntity getCurrentState(
        @PathVariable(value = "connectionId") String connectionId) {
        LOGGER.info("Get connection status for name: {}", connectionId);
        try {

            ConnectionState connectionState = connectionService.getCurrentState(connectionId);
            org.bidib.api.json.types.ConnectionInfo.ConnectionState jsonState = toJsonState(connectionState);

            return new ResponseEntity<>(jsonState, HttpStatus.OK);
        }
        catch (RuntimeException ex) {
            LOGGER.warn("No connection found with name: {}", connectionId, ex);
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    private org.bidib.api.json.types.ConnectionInfo.ConnectionState toJsonState(ConnectionState connectionState) {
        org.bidib.api.json.types.ConnectionInfo.ConnectionState jsonState = null;
        switch (connectionState.getActualPhase()) {
            case CONNECTED:
            case DISCONNECTING:
                jsonState = org.bidib.api.json.types.ConnectionInfo.ConnectionState.CONNECTED;
                break;
            case CONNECTING:
            case DISCONNECTED:
                jsonState = org.bidib.api.json.types.ConnectionInfo.ConnectionState.DISCONNECTED;
                break;
            case STALL:
                jsonState = org.bidib.api.json.types.ConnectionInfo.ConnectionState.STALL;
                break;
            default:
                jsonState = org.bidib.api.json.types.ConnectionInfo.ConnectionState.UNKNOWN;
                break;
        }
        return jsonState;
    }

    @MessageMapping(StompDestinations.SYSTEM_CONNECTIONSTATE_DESTINATION)
    @SendTo(StompDestinations.PUBLISH_SYSTEM_CONNECTIONSTATE_DESTINATION)
    @LogExecutionTime
    public org.bidib.api.json.types.ConnectionInfo msgGetCurrentState(ConnectionIdentifier connectionIdentifier) {
        LOGGER.info("Get connection status for name: {}", connectionIdentifier);
        String connectionId = connectionIdentifier.getConnectionId();
        if (StringUtils.isBlank(connectionId)) {
            connectionId = ConnectionRegistry.CONNECTION_ID_MAIN;
        }

        org.bidib.api.json.types.ConnectionInfo connectionInfo = null;
        try {
            ConnectionState connectionState = connectionService.getCurrentState(connectionId);
            org.bidib.api.json.types.ConnectionInfo.ConnectionState jsonState = toJsonState(connectionState);
            connectionInfo =
                new org.bidib.api.json.types.ConnectionInfo(connectionId, jsonState, connectionState.getError());
        }
        catch (ConnectionException ex) {
            LOGGER.warn("No connection found with name: {}", connectionId);
            connectionInfo =
                new org.bidib.api.json.types.ConnectionInfo(connectionId,
                    org.bidib.api.json.types.ConnectionInfo.ConnectionState.DISCONNECTED, null);
        }
        catch (RuntimeException ex) {
            LOGGER.warn("No connection found with name: {}", connectionId, ex);
            throw new RuntimeException("Find existing connection failed with name: " + connectionId);
        }
        return connectionInfo;
    }

    @PostMapping(path = "/connect")
    @LogExecutionTime
    public ResponseEntity connectInterface() {
        String name = ConnectionRegistry.CONNECTION_ID_MAIN;

        LOGGER.info("Connect to default interface with name: {}", name);

        return connectInterface(name);
    }

    @PostMapping(path = "/disconnect")
    @LogExecutionTime
    public ResponseEntity disconnectInterface() {
        String name = ConnectionRegistry.CONNECTION_ID_MAIN;

        LOGGER.info("Disonnect from interface with name: {}", name);

        return disconnectInterface(name);
    }

    @PutMapping(path = "/connect/{connectionId}")
    @LogExecutionTime
    public ResponseEntity connectInterface(@PathVariable(value = "connectionId") String connectionId) {
        LOGGER.info("Connect to interface with connectionId: {}", connectionId);

        // create the context
        final Context context = new DefaultContext();

        try {
            final BidibConnection connection =
                connectionService.connect(connectionId, conn -> conn, conn -> conn, context);

            return new ResponseEntity<>(connection.getConnectionState(), HttpStatus.OK);
        }
        catch (ConnectionException ex) {
            LOGGER.warn("Open connection failed.", ex);
            throw ex;
        }
        catch (RuntimeException ex) {
            LOGGER.warn("Find existing connection failed.", ex);
        }

        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @MessageMapping("/connect")
    @SendToUser(destinations = StompDestinations.PUBLISH_SYSTEM_CONNECTIONSTATE_DESTINATION, broadcast = true)
    @LogExecutionTime
    public org.bidib.api.json.types.ConnectionInfo msgConnectInterface(ConnectionIdentifier connectionIdentifier) {
        LOGGER.info("Connect to interface with name: {}", connectionIdentifier);

        String connectionId = connectionIdentifier.getConnectionId();
        if (StringUtils.isBlank(connectionId)) {
            connectionId = ConnectionRegistry.CONNECTION_ID_MAIN;
        }

        // create the context
        final Context context = new DefaultContext();

        try {
            LOGGER.info("Try to connect the connection with id: {}", connectionId);

            BidibConnection connection = connectionService.connect(connectionId, conn -> conn, conn -> conn, context);

            LOGGER.info("Created connection: {}", connection);

            final org.bidib.api.json.types.ConnectionInfo connectionInfo =
                toJsonConnectionInfo(connectionId, ConnectionPhase.CONNECTED);

            return connectionInfo;
        }
        catch (ConnectionException ex) {
            LOGGER.warn("Open connection failed, connectionId: {}, uri: {}", connectionId, ex.getUri(), ex);

            LOGGER.info("Prepare the connection state with the error.");

            handleException(ex);

            final org.bidib.api.json.types.ConnectionInfo connectionInfo =
                toJsonConnectionInfo(connectionId, ConnectionPhase.DISCONNECTED, "bidib-connect-failed");

            return connectionInfo;
        }
        catch (RuntimeException ex) {
            LOGGER.warn("Find existing connection failed.", ex);
        }

        throw new RuntimeException("Find existing connection failed.");
    }

    private org.bidib.api.json.types.ConnectionInfo toJsonConnectionInfo(
        String connectionId, ConnectionPhase connectionPhase) {
        return toJsonConnectionInfo(connectionId, connectionPhase, null);
    }

    private org.bidib.api.json.types.ConnectionInfo toJsonConnectionInfo(
        String connectionId, ConnectionPhase connectionPhase, String error) {
        org.bidib.api.json.types.ConnectionInfo.ConnectionState connectionState =
            org.bidib.api.json.types.ConnectionInfo.ConnectionState.valueOf(connectionPhase.name());

        final org.bidib.api.json.types.ConnectionInfo connectionInfo = new org.bidib.api.json.types.ConnectionInfo();
        connectionInfo.setId(connectionId);
        connectionInfo.setState(connectionState);
        if (StringUtils.isNotBlank(error)) {
            connectionInfo.setError(error);
        }

        return connectionInfo;
    }

    @Operation(summary = "Disconnect the interface with the provided name.")
    @PutMapping(path = "/disconnect/{connectionId}")
    @LogExecutionTime
    public ResponseEntity disconnectInterface(
        @PathVariable(value = "connectionId") String connectionId) {
        LOGGER.info("Disonnect from interface with connectionId: {}", connectionId);

        try {
            BidibConnection connection = connectionService.disconnect(connectionId);

            return new ResponseEntity<>(connection.getConnectionState(), HttpStatus.OK);
        }
        catch (RuntimeException ex) {
            LOGGER.warn("Find existing connection failed.", ex);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @Operation(summary = "Disconnect the interface with the provided connection identifier.")
    @PutMapping(path = "/disconnect/{connectionIdentifier}")
    @MessageMapping("/disconnect")
    @SendToUser(destinations = StompDestinations.PUBLISH_SYSTEM_CONNECTIONSTATE_DESTINATION, broadcast = true)
    @LogExecutionTime
    public org.bidib.api.json.types.ConnectionInfo msgDisconnectInterface(
        @PathVariable ConnectionIdentifier connectionIdentifier) {
        LOGGER.info("Disonnect from interface with connectionId: {}", connectionIdentifier);

        String connectionId = connectionIdentifier.getConnectionId();
        if (StringUtils.isBlank(connectionId)) {
            connectionId = ConnectionRegistry.CONNECTION_ID_MAIN;
        }

        try {
            BidibConnection connection = connectionService.disconnect(connectionId);

            final org.bidib.api.json.types.ConnectionInfo connectionInfo =
                toJsonConnectionInfo(connectionId, connection.getConnectionState().getActualPhase());

            return connectionInfo;
        }
        catch (RuntimeException ex) {
            LOGGER.warn("Find existing connection failed.", ex);
        }

        throw new RuntimeException("Find existing connection failed.");
    }

    @GetMapping("/download/log")
    public void downloadFile(HttpServletResponse response) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmm");
        String zipFileName = "bidib-server-logs-" + sdf.format(new Date()) + ".zip";

        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + zipFileName);
        response.setStatus(HttpServletResponse.SC_OK);

        List fileNames = logFileService.getFileNames();

        LOGGER.info("Total number of files: ", fileNames.size());

        try (ZipOutputStream zippedOut = new ZipOutputStream(response.getOutputStream())) {
            for (String file : fileNames) {
                FileSystemResource resource = new FileSystemResource(file);

                ZipEntry e = new ZipEntry(resource.getFilename());
                // Configure the zip entry, the properties of the file
                e.setSize(resource.contentLength());
                e.setTime(System.currentTimeMillis());
                // etc.
                zippedOut.putNextEntry(e);
                // And the content of the resource:
                StreamUtils.copy(resource.getInputStream(), zippedOut);
                zippedOut.closeEntry();
            }
            zippedOut.finish();
        }
        catch (Exception ex) {
            LOGGER.warn("The download of logfiles caused an error.", ex);
        }
    }

    /**
     * Send notification to users subscribed on channel "/topic/system/statusInfo".
     * 
     * @param statusInformation
     *            the statusInformation.
     */
    private void publishStatusInformation(NodeStatusInformation statusInformation) {
        LOGGER.info("Notify new statusInformation: {}", statusInformation);

        // provide the connection name
        String connectionName = statusInformation.getConnectionId();

        try {
            if (statusInformation.getStatusLevel() == null) {
                statusInformation.setStatusLevel(StatusLevel.Info);
            }
            switch (statusInformation.getStatusLevel()) {
                case Error:
                    final SystemError se = new SystemError();
                    se.setMsgKey(statusInformation.getMsgKey());
                    se.setMsgKeyArgs(statusInformation.getMsgKeyArgs());
                    se.setTimestamp(LocalDateTime.now());
                    se.setConnectionId(connectionName);
                    messagingTemplate.convertAndSend(StompDestinations.SYSTEM_ERROR_DESTINATION, se);
                    break;
                default:
                    final StatusInfo si =
                        new StatusInfo(connectionName, statusInformation.getStatusLevel(),
                            statusInformation.getMsgKey(), statusInformation.getMsgKeyArgs(), 20);
                    messagingTemplate.convertAndSend(StompDestinations.SYSTEM_STATEINFO_DESTINATION, si);
                    break;
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Send the status information failed: {}", statusInformation, ex);
        }
    }

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

        String msgKey = exception.getMsgKey();
        List args = Arrays.asList(exception.getUri());

        String connectionId = exception.getConnectionId();
        LOGGER.error("Error detected, msgKey: {}, args: {}, connectionId: {}", msgKey, args, connectionId);

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

    @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();
    }

}