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

com.openfin.desktop.DesktopConnection Maven / Gradle / Ivy

There is a newer version: 11.0.2
Show newest version
package com.openfin.desktop;

import com.openfin.desktop.net.WebSocketConnection;
import com.openfin.desktop.net.WebSocketEventHandler;
import com.openfin.desktop.net.WebSocketException;
import com.openfin.desktop.net.WebSocketMessage;
import com.openfin.desktop.win32.DesktopPortHandler;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.lang.System;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A  object for launching, connecting to, and controlling Runtime.
 */
public class DesktopConnection {
    private final static Logger logger = LoggerFactory.getLogger(DesktopConnection.class.getName());

    private Map callbacks;

    private Map notificationListenerMap = new ConcurrentHashMap();
    // >>
    private Map>>> applicationEventCallbackMap = new ConcurrentHashMap<>();
    // >
    private Map>> systemEventCallbackMap = new ConcurrentHashMap<>();
    // >>
    private Map>>>> windowEventCallbackMap = new ConcurrentHashMap<>();

    private List externalMessageHandlers = new CopyOnWriteArrayList();

    private WebSocketConnection websocket;
    private JSONObject jsonMsg;
    private boolean connected = false;
    private String uuid, host;
    private Integer port;  // if initial value is null, get it from Runtime based on Runtime version required
    private int timeout;   // current timeout value in seconds
    private long waitStartTime;  // start time of wait-to-connect thread
    private boolean opened = false;
    private boolean authRequested = false;
    private String authorizationAction;
    private String authorizationType;
    private long messageId = 0;
    private Integer notificationCount = 1;
    private JSONObject notificationPayload;
    private String runtimeSecurityRealm;  // if speicified, adding "--security-realm=xxx" to Runtime argument
    private String additionalRuntimeArguments; // additional Runtime arguments
    private String rdmUrl;                     // RDM Url for license tracking
    private int devtoolsPort;                   // port for chromium devtools
    private String runtimeAssetsUrl;           // URL for RVM and Runtime assets
    private String additionalRvmArguments;  // arguments to launch RVM, in addition to --config
    private DesktopStateListener listener;
    private PortDiscoveryHandler portDiscoveryHandler;
    private static volatile String processId;   // only send PID for the first connection per process since Runtime core
                                                // uses PID as key to manage connections

    private java.util.Timer timer;
    private boolean disconnecting = false;
    private InterApplicationBus busInstance;
    private long startTime;
    private long eventTime;
    private long authReqMsgId;
    private EventListener portDiscoveryEventListener;
    private RuntimeConfiguration activeConfiguration; // config used by current connection

    /**
     * Creates a new connection to Runtime
     *
     * @param uuid unique ID for Runtime to refer to this DesktopConnection
     * @throws DesktopException if this method fails to create a connection
     */
    public DesktopConnection(String uuid) throws DesktopException {
        this(uuid, null, null);
    }

    /**
     * Creates a new connection to Runtime
     * @param uuid unique ID for Runtime to refer to this DesktopConnection
     * @param host The host that Runtime is running on.  Only "localhost" is accepted
     * @param port The port that Runtime is listening on for connections.  -null if unknown
     * @throws DesktopException if this method fails to create a connection
     * @deprecated use {@link #DesktopConnection(String)} instead.
     */
    public DesktopConnection(String uuid, String host, Integer port) throws DesktopException {
        if (uuid == null) {
            throw new DesktopException("Invalid uuid: ");
        }
        this.uuid = uuid;
        this.host = "localhost";
        this.port = port;
        callbacks = new ConcurrentHashMap();
        jsonMsg = new JSONObject();
        busInstance = new InterApplicationBus(this);
    }

    private void timingEvent(String event) {
        long now = System.currentTimeMillis();
        double time = (now - eventTime)/1000d;
        logger.debug("Event:" + event + " secs:" + time);
        eventTime = now;
    }

    private void startEvent(String event) {
        long now = System.currentTimeMillis();
        double time = (now - startTime)/1000d;
        logger.debug("Total Time: " + event + " secs:" + time);
    }

    private void sendRequestAuthorization() throws DesktopException, DesktopIOException {
        if (!authRequested) {
            JSONObject json = new JSONObject();
            try {
                json.put("action", authorizationAction); //"request-authorization");
                JsonUtils.updateValue(json, "action", authorizationAction);
                JSONObject client = new JSONObject();
                client.put("type", "java");
                client.put("version", OpenFinRuntime.getAdapterVersion() ==  null ? "N/A" : OpenFinRuntime.getAdapterVersion());
                client.put("javaVersion", System.getProperty("java.version"));
                JSONObject payload = new JSONObject();
                payload.put("uuid", uuid);
                payload.put("type", authorizationType); //"command-line-token");
                payload.put("licenseKey", (this.activeConfiguration.getLicenseKey() == null || this.activeConfiguration.getLicenseKey().trim().isEmpty()) ? "N/A" : this.activeConfiguration.getLicenseKey());
                payload.put("client", client);
                payload.put("configUrl", this.activeConfiguration.getManifestLocation() != null ?
                        this.activeConfiguration.getManifestLocation() : this.activeConfiguration.getGeneratedManifestLocation());
                
                synchronized(DesktopConnection.class) {
                    if (processId == null) {
                        payload.put("pid", processId = getProcessId("N/A"));
                    } 
                    else {
                        logger.debug("skipping passing pid");
                    }
                }
                json.put("payload", payload);
                this.authReqMsgId = this.getNextMessageId();
                json.put("messageId", this.authReqMsgId);
                String msg = json.toString();
                logger.debug("sending message: " + msg);
                if (listener != null) listener.onOutgoingMessage(msg);
                websocket.send(msg);
            } catch (JSONException je) {
                throw new DesktopException(je);
            } catch (WebSocketException wex) {
                throw new DesktopIOException(wex);
            }
        }
    }

    /**
     * Launches Runtime and notifies the listener when connected.  This method launches OpenFinRVM.exe embedded in the jar.
     * @param commandLineArguments Command line arguments to start the Runtime with
     * @param listener Receives updates on startup and connection state
     * @param timeout For connecting to Desktop after launch. If the connection to Runtime is not established by the timeout the listener will get an onError() call
     * @throws URISyntaxException if URL has invalid syntax
     * @throws DesktopIOException if this method fails to connect to Runtime
     * @deprecated use {@link #connect(RuntimeConfiguration, DesktopStateListener, int)} instead.
     */
    public void launchAndConnect(String commandLineArguments, final DesktopStateListener listener, int timeout) throws URISyntaxException, DesktopIOException {
        this.launchAndConnect(null, commandLineArguments, listener, timeout);
    }

    /**
     * Launches Runtime and notifies the listener when connected.
     * @param desktopPath Absolute path to the Runtime executable
     * @param commandLineArguments Command line arguments to start the Runtime with
     * @param listener Receives updates on startup and connection state
     * @param timeout For connecting to Desktop after launch. If the connection to Runtime is not established by the timeout the listener will get an onError() call
     * @throws DesktopIOException if this method fails to connect to Runtime
     * @deprecated use {@link #connect(RuntimeConfiguration, DesktopStateListener, int)} instead.
     */
    public void launchAndConnect(String desktopPath, String commandLineArguments, final DesktopStateListener listener, int timeout) throws DesktopIOException {
        this.startTime = java.lang.System.currentTimeMillis();
        this.eventTime = startTime;
        this.listener = listener;
        //make sure commandLineArguments isnt null...
        commandLineArguments = commandLineArguments == null ? "" : commandLineArguments;
        if (desktopPath != null) {
            runDesktop(desktopPath, commandLineArguments);
        } else {
            RuntimeLauncher.launchConfig(commandLineArguments);
        }
        connect("file-token", listener);
    }

    /**
     * Connect to specified version of Desktop.  If the specified version is not running, this method will try to start it.
     *
     * @param runtimeVersion version of Runtime required
     * @param listener Receives updates on startup and connection state
     * @param timeout number of seconds to wait for connection. If connection to Runtime is not established by the timeout the listener will get an onError() call
     * @throws DesktopIOException if this method fails to connect to Runtime
     * @throws IOException if IO exception is thrown during launching Runtime
     * @deprecated use {@link #connect(RuntimeConfiguration, DesktopStateListener, int)} instead.
     *
     */
    public void connectToVersion(final String runtimeVersion, final DesktopStateListener listener, final int timeout) throws DesktopIOException, IOException {
        RuntimeConfiguration config = new RuntimeConfiguration();
        config.setRuntimeVersion(runtimeVersion);
        config.setSecurityRealm(this.runtimeSecurityRealm);
        config.setAdditionalRuntimeArguments(this.additionalRuntimeArguments);
        config.setRdmURL(this.rdmUrl);
        config.setRuntimeAssetURL(this.runtimeAssetsUrl);
        config.setAdditionalRvmArguments(this.additionalRvmArguments);
        if (this.devtoolsPort > 0) {
            config.setDevToolsPort(this.devtoolsPort);
        }
        connect(config, listener, timeout);
    }

    /**
     * Connect to Runtime with the specified configuration.  If the specified version is not running, this method will try to start it.
     *
     * @param configuration an instance of RuntimeConfiguration
     * @param listener Receives updates on startup and connection state
     * @param timeout number of seconds to wait for connection. If connection to Runtime is not established by the timeout the listener will get an onError() call
     * @throws DesktopIOException if this method fails to connect to Runtime
     * @throws IOException if IO exception is thrown during launching Runtime
     * @see RuntimeConfiguration
     */
    public void connect(final RuntimeConfiguration configuration, final DesktopStateListener listener, final int timeout) throws DesktopIOException, IOException {
        if (configuration.getRuntimePort() > 0) {
            logger.warn("connecting with hard-coded port has been deprecated.  Please check API docs.");
            this.port = configuration.getRuntimePort();
            this.activeConfiguration = configuration;
            this.connect(listener);
        }
        else if (this.portDiscoveryEventListener == null) {
            this.startTime = java.lang.System.currentTimeMillis();
            this.eventTime = startTime;
            this.listener = listener;
            this.port = null;
            this.timeout = timeout;
            this.activeConfiguration = configuration;
            this.portDiscoveryEventListener = initPortDiscoveryEventListener(configuration, listener,
                    timeout);
            this.portDiscoveryHandler = initDesktopPortHandler(this.portDiscoveryEventListener, configuration);
            RuntimeLauncher.launchVersion(configuration, this.uuid);
        } else {
            logger.error("Already waiting to connect to a version");
        }
    }

    private boolean matchRuntimeInstance(String requestedVersion, String securityRealm, RuntimeConfiguration configuration) {
        if (configuration.getRuntimeVersion() != null && configuration.getSecurityRealm() != null){
            if (configuration.getRuntimeVersion().equals(requestedVersion) && configuration.getSecurityRealm().equals(securityRealm)) {
                logger.debug("matched Runtime version and security realm");
                return true;
            }
        }
        if (configuration.getRuntimeFallbackVersion() != null && configuration.getSecurityRealm() != null){
            if (configuration.getRuntimeFallbackVersion().equals(requestedVersion) && configuration.getSecurityRealm().equals(securityRealm)) {
                logger.debug("matched Runtime fullback version and security realm");
                return true;
            }
        }
        if (configuration.getRuntimeVersion() != null && configuration.getSecurityRealm() == null) {
            if (configuration.getRuntimeVersion().equals(requestedVersion) && securityRealm == null) {
                logger.debug("matched Runtime version and null security realm");
                return true;
            }
        }
        if (configuration.getRuntimeFallbackVersion() != null && configuration.getSecurityRealm() == null) {
            if (configuration.getRuntimeFallbackVersion().equals(requestedVersion) && securityRealm == null) {
                logger.debug("matched Runtime fallback version and null security realm");
                return true;
            }
        }
        return false;
    }

    private EventListener initPortDiscoveryEventListener(final RuntimeConfiguration configuration, final DesktopStateListener listener, final int timeout) {
        return new EventListener() {
            @Override
            public void eventReceived(com.openfin.desktop.ActionEvent actionEvent) {
                if (actionEvent.getType().equals("TIMEOUT")) {
                    logger.error("timed out on connectionToVersion");
                    portDiscoveryHandler.removeEventListener(portDiscoveryEventListener);
                    portDiscoveryEventListener = null;
                    listener.onError("Connection timed out");
                } else {
                    JSONObject runtimeInfo = actionEvent.getEventObject();
                    // "port":7840,"requestedVersion":"stable","securityRealm":"noCacheRealm","sslPort":7840,"version":"5.44.7.17"}
                    Integer port = JsonUtils.getIntegerValue(runtimeInfo, "port", null);
                    String requestedVersion = JsonUtils.getStringValue(runtimeInfo, "requestedVersion", null);
                    String requestedSecurityRealm = JsonUtils.getStringValue(runtimeInfo, "securityRealm", null);
                    if (matchRuntimeInstance(requestedVersion, requestedSecurityRealm, configuration)) {
                        String runVersion = JsonUtils.getStringValue(runtimeInfo, "version", null);
                        if (port == null) {
                            listener.onError("Port for version " + configuration.getRuntimeVersion() + " not found in COPYDATA");
                        } else if (runVersion == null || runVersion.length() == 0) {
                            listener.onError("Version for version " + configuration.getRuntimeVersion() + " not found in COPYDATA");
                        } else if (DesktopConnection.this.port == null) {
                            DesktopConnection.this.port = port;
                            logger.debug("Runtime version " + runVersion + " at port " + port);
                            try {
                                DesktopConnection.this.connect(listener);
                            } catch (Exception e) {
                                logger.error("Error connecing to desktop", e);
                                listener.onError(e.getMessage());
                            }
                        } else {
                            logger.debug("Port already set at " + DesktopConnection.this.port + ", ignoring " + port);
                        }
                        portDiscoveryHandler.removeEventListener(portDiscoveryEventListener);
                        portDiscoveryEventListener = null;
                    } else {
                        logger.debug("RequestedVersion " + requestedVersion + " mismatches " + configuration.getRuntimeVersion() + ", ignoring...");
                    }
                }
            }
        };
    }

    private PortDiscoveryHandler initDesktopPortHandler(EventListener portDiscoveryEventListener, RuntimeConfiguration runtimeConfiguration) {
        PortDiscoveryHandler handler;
        if (runtimeConfiguration.isUseNamedPipePortDiscovery() || !DesktopUtils.isWindows()) {
            long randomNum = (long) Math.floor(Math.random() * 100000);
            String pipeName = "OpenfinJavaAdapter." + System.currentTimeMillis() + "." + randomNum;
            handler = DesktopUtils.isWindows() ? new com.openfin.desktop.win32.NamedPipePortHandler(pipeName) :
                    new com.openfin.desktop.nix.NamedPipePortHandler(pipeName);
            runtimeConfiguration.setAdditionalRuntimeArguments("--v=1 --runtime-information-channel-v6=" +
                                    handler.getEffectivePipeName());
            handler.registerEventListener(portDiscoveryEventListener, this.timeout);
        } else {
            handler = DesktopPortHandler.getInstance();
            handler.registerEventListener(portDiscoveryEventListener, this.timeout);
        }
        return handler;
    }
    /**
     * Disconnects from Runtime
     *
     * @throws DesktopException if this method fails to disconnect from Runtime
     */
    public void disconnect() throws DesktopException {
        this.disconnect(null);
    }

    protected void disconnect(String reason) throws DesktopException {
        disconnecting = true;
        try {
            if (timer != null) {
                timer.cancel();
            }
            websocket.close(reason);
        } catch (WebSocketException e) {
            logger.error("Error disconnecting Runtime", e);
            throw new DesktopException(e);
        }
    }

    /**
     * InterApplicationBus
     * @return true if connected
     */
    public boolean isConnected() {
        return connected;
    }

    /**
     * Notify Runtime to exit
     *
     * @throws DesktopException if this method fails to exit
     */
    public void exit() throws DesktopException{
        try {
            sendAction("exit-desktop", new JSONObject());
        } catch (Exception e) {
            logger.error("Error existing Runtime", e);
            throw new DesktopException(e);
        }
    }

    /**
     * Gets the Inter-Application message dispatcher associated with this DesktopConnection
     * @return InterApplicationBus
     */
    public InterApplicationBus getInterApplicationBus() {
        return busInstance;
    }

    /**
     * Gets port number that Runtime is running on
     *
     * @return port number
     */
    public Integer getPort() {
        return this.port;
    }

    /**
     * Sends a message to Runtime
     *
     * @param action The action of the message
     * @param payload The message object to send
     * @throws DesktopException if this method fails to send the message
     */
    public void sendAction(String action, JSONObject payload) throws DesktopException {
        sendAction(action, payload, null);
    }

    /**
     * Sends a message to Runtime
     *
     * @param action The action of the message
     * @param payload The message object to send
     * @param newMessageId message Id for the message object
     * @throws DesktopException if this method fails to send the message
     */
    private void sendAction(String action, JSONObject payload, Long newMessageId) throws DesktopException {
        if (isConnected()) {
            try {
                jsonMsg.put("action", action);
                if (newMessageId == null) {
                    newMessageId = this.getNextMessageId();
                }
                jsonMsg.put("messageId", newMessageId);
                if (payload != null) {
                    jsonMsg.put("payload", payload);
                } else {
                    jsonMsg.remove("payload");
                }
                String msg = jsonMsg.toString();
                if (logger.isDebugEnabled()) {
                    logger.debug("Sending: " + msg);
                }
                if (listener != null) listener.onOutgoingMessage(msg);
                websocket.send(msg);
            } catch (Exception e) {
                logger.error("Error sending action", e);
                throw new DesktopException(e);
            }
        } else {
            logger.warn("Not connected to sendAction " + jsonMsg);
        }
    }

    /**
     * Gets nesxt message Id
     * @return next message Id
     */
    private synchronized long getNextMessageId() {
        return this.messageId++;
    }

    /**
     * Sends a message to Runtime
     * @param action The action of the message
     * @param payload The message object to send
     * @param listener AckListener for the message
     * @param source Message source
     * @see AckListener
     */
    public void sendAction(String action, JSONObject payload, AckListener listener, Object source) {
        if (isConnected()) {
            ListenerSourcePair lsp = new ListenerSourcePair(listener, source);
            Long msgId = null;
            if (listener != null) {
                msgId = this.getNextMessageId();
                callbacks.put(msgId, lsp);
            }
            try {
                sendAction(action, payload, msgId);
            } catch (Exception ex) {
                logger.error("Error sending action", ex);
                DesktopUtils.errorAckOnException(listener, source, ex);
            }
        } else {
            logger.warn("Not connected to sendAction " + action);
        }
    }

    private void runDesktop(final String desktopPath, final String commandLine) throws DesktopIOException {
        logger.debug("run desktop from " + desktopPath + " " + commandLine);
        Thread thread = new Thread() {
            public void run() {
                try {
                    String line;
                    OutputStream stdin = null;
                    InputStream stderr = null;
                    InputStream stdout = null;

                    // launch EXE and grab stdin/stdout and stderr
                    Process process = Runtime.getRuntime().exec("cmd");
                    stdin = process.getOutputStream();
                    stderr = process.getErrorStream();
                    stdout = process.getInputStream();

                    // "write" the parms into stdin
                    line = "start " + desktopPath + " " + commandLine + "\n";
                    logger.debug(line);

                    stdin.write(line.getBytes());
                    stdin.flush();
                    stdin.close();

                    // clean up if any output in stdout
                    BufferedReader brCleanUp =
                            new BufferedReader(new InputStreamReader(stdout));
                    while ((line = brCleanUp.readLine()) != null) {
                        logger.debug("[Stdout] " + line);
                    }
                    brCleanUp.close();

                    // clean up if any output in stderr
                    brCleanUp =
                            new BufferedReader(new InputStreamReader(stderr));
                    while ((line = brCleanUp.readLine()) != null) {
                        logger.debug("[Stderr] " + line);
                    }
                    brCleanUp.close();
                } catch (Exception e1) {
                    logger.error("Error starting Runtime", e1);
                }
            }
        };
        thread.setName(this.getClass().getName() + ".runDesktop");
        thread.start();
    }

    /**
     * Connects to an Runtime process
     *
     * @param listener Receives updates on startup and connection state
     * @see DesktopStateListener
     *
     * @deprecated use {@link #connect(RuntimeConfiguration, DesktopStateListener, int)} instead.
     */
    public void connect(final DesktopStateListener listener) {
        if (this.activeConfiguration == null) {
            this.activeConfiguration = new RuntimeConfiguration();  // fake to avoid NullPointerException
        }
        this.connect("file-token", listener);
    }

    /**
     * Connects to an Runtime process
     *
     * @param type Describes the type of connection to establish
     * @param listener Receives updates on startup and connection state
     * @see DesktopStateListener
     */
    private void connect(final String type, final DesktopStateListener listener) {
        if (this.connected) {
            listener.onError("Desktop websocket already connected");
            return;
        }
        this.authorizationAction = "request-external-authorization";
        this.authorizationType = type;
        this.authRequested = false;

        try {
            if (host == null) throw new WebSocketException("");
            URI uri = new URI("ws://" + host + ":" + port + "/");
            logger.debug("opening websocket " + uri.toString());
            websocket = new WebSocketConnection(uri); // Register Event Handlers
        } catch (Exception ex) {
            logger.error("Error connecting to Runtime", ex);
            listener.onError(ex.getMessage());
        }

        websocket.setEventHandler(new WebSocketEventHandler() {
            public void onOpen() {
                try {
                	if (activeConfiguration.getMaxMessageSize() > 0) {
                	    logger.debug(String.format("setting max message size %d", activeConfiguration.getMaxMessageSize()));
                    	websocket.setMaxMessageSize(activeConfiguration.getMaxMessageSize());
                	}
                    timingEvent("onOpen");
                    disconnecting = false;
                    opened = true;
                    cancelConnectWaitTimer();
                    sendRequestAuthorization();
                } catch (Exception authEx) {
                    logger.error("Error onOpen", authEx);
                    listener.onError(authEx.getMessage());
                }
            }

            public void onMessage(WebSocketMessage message) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Received message: " + message.getText());
                }
                listener.onMessage(message.getText());
                try {
                    JSONObject json = new JSONObject(message.getText());
                    String action = json.getString("action");
                    JSONObject payload = json.getJSONObject("payload");
                    timingEvent("action:" + action);
                    if (action.equals("ack")) {
                        logger.debug("Receiving Ack: " + message.getText());
                        if (json.has("correlationId")) {
                            long correlationId = json.getLong("correlationId");
                            if (correlationId == DesktopConnection.this.authReqMsgId) {
                                boolean authDone = payload.getBoolean("success");
                                if (!authDone) {
                                    logger.debug("retry sendRequestAuthorization");
                                    try {
                                        sendRequestAuthorization();
                                    } catch (Exception authEx) {
                                        logger.error("Error sending auth request", authEx);
                                        listener.onError(authEx.getMessage());
                                    }
                                }
                            } else {
                                fireMessageCallback(correlationId, payload);
                            }
                        }
                    }
                    else if (action.equals("external-authorization-response"))
                    {
                        try {
                            processExternalAuthorizationResponse(payload);
                        } catch (Exception authEx) {
                            logger.error("Error processing auth request", authEx);
                            listener.onError(authEx.getMessage());
                        }
                    }
                    else if (action.equals("authorization-response")) {
                        authRequested = true;
                        boolean success = payload.getBoolean("success");
                        if (success) {
                            connected = true;
                            listener.onReady();
                            startEvent("Connected");
                        } else {
                            if (!disconnecting) {
                                String reason = payload.getString("reason");
                                try {
                                    listener.onError(reason);
                                    disconnect("Authorization request to Runtime is denied");
                                } catch (Exception ex) {
                                    logger.error("Error processing auth response");
                                }
                            }
                        }
                    }
                    else if (action.equals("process-message")) {
                        String sourceUuid = payload.getString("sourceUuid");
                        String topic = payload.getString("topic");
                        busInstance.dispatchMessageToCallbacks(sourceUuid, topic, payload.get("message"));
                        if ("utils-ready".equals(topic)) {
                            startEvent("AdminReady");
                        }
                    }
                    else if (action.equals("process-notification-event"))
                    {
                        processNotificationEvent(payload);
                    }
                    else if (action.equals("process-desktop-event"))
                    {
                        dispatchDesktopEvent(payload);
                    }
                    else if (action.equals("subscriber-added")) {
                        busInstance.dispatchToSubscribeListeners(payload.getString("uuid"), payload.getString("topic"));
                    }
                    else if (action.equals("subscriber-removed")) {
                        busInstance.dispatchToUnsubscribeListeners(payload.getString("uuid"), payload.getString("topic"));
                    }
                    else if (action.equals("process-external-app-action")) {
//                        processExternalAppAction(payload);
                    }
                    else if (action.equals("ping")) {
                        long pingId = payload.getLong("pingId");
                        respondToPing(pingId);
                    }
                    else if (action.equals("process-external-message")) {
                        processExternalMessage(payload);
                    }
                    else if (action.equals("app-connected")) {
                        processWindowConnected(payload);
                    }
                    else if (action.equals("app-loaded")) {
                        processWindowLoaded(payload);
                    }
                } catch (JSONException e) {
                    logger.error("Error processing message from Runtime", e);
                    listener.onError(e.getMessage());
                }
            }

            public void onClose(int statusCode, String reason) {
                logger.debug("onClose");
                connected = false;
                opened = false;
                cancelConnectWaitTimer();
                listener.onClose(reason);
                busInstance.reset();
                disconnecting = false;
            }
        });

        startConnectWaitTimer(new TimerTask() {
            @Override
            public void run() {
                if (websocket.isConnected()) {
                    logger.debug("connectToDesktop-timer-stopped connected");
                    timer.cancel();
                }
                else if (authRequested) {
                    logger.debug("connectToDesktop-timer-stopped auth received");
                    timer.cancel();
                }
                else if (opened) {
                    // should be called in WebSocketEventHandler
                    //  sendRequestAuthorization();
                } else {
                    try {
                        logger.debug("Trying to connect to Runtime");
                        websocket.connect();
                    } catch (WebSocketException e) {
                        logger.error("Failed to connect to Runtime", e);
                    }
                }
                if (checkConnectTimeout()) {
                    cancelConnectWaitTimer();
                    listener.onError("Connection timed out");
                }            }
        });
    }

    private void startConnectWaitTimer(TimerTask timerTask) {
        cancelConnectWaitTimer();
        this.waitStartTime = System.currentTimeMillis();
        timer = new java.util.Timer(String.format("ConnectWaitTimer-%s", this.uuid));
        logger.debug("connectToDesktop-timer-start " + uuid);
        timer.schedule(timerTask, 0, 100);
    }

    private void cancelConnectWaitTimer() {
        if (timer != null) {
            logger.debug("connectToDesktop-timer-stopped " + this.uuid);
            timer.cancel();
        }
    }

    private boolean checkConnectTimeout() {
        int secs = (int) ((System.currentTimeMillis() - this.waitStartTime) / 1000);
        if (secs > this.timeout) {
            logger.debug("checkConnectTimeout true " + this.uuid);
            return true;
        } else {
            return false;
        }
    }

    private void fireMessageCallback(long correlationId, JSONObject payload) {
        ListenerSourcePair lsp = callbacks.remove(correlationId);
        if (lsp != null) {
            Object source = lsp.getSource();
            AckListener ackListener = lsp.getListener();
            Ack ack = new Ack(payload, source);
            if (ack.isSuccessful()) {
                DesktopUtils.successAck(ackListener, ack);
            } else {
                DesktopUtils.errorAck(ackListener, ack);
            }
        }
    }

    private void processExternalAuthorizationResponse(JSONObject payload) throws JSONException, IOException, WebSocketException {
        // file-token authorization requested. Must write token into file.
        if (authorizationType.equals("file-token"))
        {
            String token = payload.getString("token");
            String file = payload.getString("file");
            if (token != null &&
                file != null &&
                file.length() > 0 &&
                token.length() > 0)
            {
                FileWriter writer = new FileWriter(file);
                writer.write(token);
                writer.close();
            }
        }
        sendFinalAuthorizationRequest(authorizationType);
    }

    private void sendFinalAuthorizationRequest(String type) throws JSONException, WebSocketException {
        disconnecting = false;
        JSONObject json = new JSONObject();
        json.put("action", "request-authorization");
        JSONObject payload = new JSONObject();
        payload.put("uuid", uuid);
        payload.put("type", type);
        if (this.activeConfiguration.isNonPersistent()) {
            payload.put("nonPersistent", true);
        }
        json.put("payload", payload);

        String msg = json.toString();
        if (listener != null)
        {
            listener.onOutgoingMessage(msg);
        }
        websocket.send(msg);
    }

    private void processNotificationEvent(JSONObject payload)
    {
        if (payload != null)
        {

            Integer notificationId = getNestedJSONInteger(payload, "payload.notificationId");
            if (notificationId != null) {
                try {
                    ListenerSourcePair lsp = notificationListenerMap.get(notificationId);
                    if (lsp != null)
                    {
                        String type = payload.getString("type");
                        if (lsp.getNotificationListener() != null) {
                            invokeNotificationCallback(lsp.getNotificationListener(), new Ack(payload, lsp.getSource()), type);
                        }
                        // Remove if the notification is no longer being used by Runtime.
                        if (type == "close" ||
                            type == "error" ||
                            type == "dismiss") {
                            notificationListenerMap.remove(notificationId);
                        }
                    }
                } catch (Exception e) {
                    logger.error("Error processing notification event", e);
                }
            }
        }
    }

    private void invokeNotificationCallback(NotificationListener listener, Ack ack, String type) {
        // convert click to onClick
        String methodName = "on" + type.substring(0, 1).toUpperCase() + type.substring(1);
        try {
            Method method = listener.getClass().getMethod(methodName, ack.getClass());
            if (method != null) {
                method.setAccessible(true);
                method.invoke(listener, ack);
            }
        } catch (Exception e) {
            logger.error("Error invoking notification callback", e);
        }
    }

    /**
     * Registers listener for a new notification
     * @param listener NotificationListener for the notification
     * @param source Source of the request
     * @return Notification Id
     * @see NotificationListener
     */
    public Integer registerNotificationListener(NotificationListener listener, Object source) {
        Integer returnId = 0;
        if(listener != null) {
            notificationListenerMap.put(notificationCount, new ListenerSourcePair(listener, source));
            returnId = notificationCount++;
        }
        return returnId;
    }

    public void sendActionToNotificationsCenter(String action, JSONObject payload, AckListener callback, Object source)
    {
        try {
            if(notificationPayload == null) {
                notificationPayload = new JSONObject();
            }
            notificationPayload.put("action", action);
            notificationPayload.put("payload", payload);
            sendAction("send-action-to-notifications-center", notificationPayload, callback, this);
        } catch (Exception e) {
            logger.error("Error sending message to notification center", e);
            DesktopUtils.errorAckOnException(callback, payload, e);
        }
    }

    private Integer getNestedJSONInteger(JSONObject json, String expression) {
        Integer result = null;
        StringTokenizer tokenizer = new StringTokenizer(expression, ".");
        List list = new ArrayList();
        while (tokenizer.hasMoreTokens()) {
            list.add(tokenizer.nextToken());
        }
        JSONObject nested = json;
        try {
            for (int i = 0; i < list.size() - 1; i++) {
                if (nested.has(list.get(i))) {
                    nested = nested.getJSONObject(list.get(i));
                } else {
                    nested = null;
                    break;
                }
            }
            if (nested != null) {
                result = nested.getInt(list.get(list.size() - 1));
            }
        } catch (Exception e) {
            logger.error("Error reading integer from JSON", e);
        }
        return result;
    }


    private List getEventCallbackList(JSONObject subscriptionObject) {
        List list = null;
        try {
            String topic = subscriptionObject.getString("topic");
            String type = getFullEventType(topic, subscriptionObject.getString("type"));
            String uuid = JsonUtils.getStringValue(subscriptionObject, "uuid", null);
            String name = null;
            if (subscriptionObject.has("name")) {
                name = subscriptionObject.getString("name");
            }
            list = getEventCallbackList(topic, type, uuid, name);
        } catch (Exception e) {
            logger.error("Error getting event callback list", e);
        }

        return list;
    }

    private List getEventCallbackList(String topic, String type, String uuid, String name) {
        List callbacks = null;
        if(topic != null && type != null) {
            if (topic.equals("application")) {
                if (uuid != null)
                {
                    Map>> matchedTopic = applicationEventCallbackMap.get(topic);
                    if (matchedTopic == null) {
                        matchedTopic = new ConcurrentHashMap<>();
                        applicationEventCallbackMap.put(topic, matchedTopic);
                    }
                    Map> matchedType = matchedTopic.get(type);
                    if (matchedType == null) {
                        matchedType = new ConcurrentHashMap<>();
                        matchedTopic.put(type, matchedType);
                    }
                    callbacks = matchedType.get(uuid);
                    if (callbacks == null) {
                        callbacks = new CopyOnWriteArrayList<>();
                        matchedType.put(uuid, callbacks);
                    }
                }
            }
            else if (topic.equals("system")) {
                Map> matchedTopic = systemEventCallbackMap.get(topic);
                if(matchedTopic == null) {
                    matchedTopic = new ConcurrentHashMap<>();
                    systemEventCallbackMap.put(topic, matchedTopic);
                }
                callbacks = matchedTopic.get(type);
                if (callbacks == null) {
                    callbacks = new CopyOnWriteArrayList<>();
                    matchedTopic.put(type, callbacks);
                }
            }
            else if (topic.equals("window")) {
                if (uuid != null && name != null) {
                    Map>>> matchedTopic = windowEventCallbackMap.get(topic);
                    if(matchedTopic == null) {
                        matchedTopic = new ConcurrentHashMap<>();
                        windowEventCallbackMap.put(topic, matchedTopic);
                    }
                    Map>> matchedType = matchedTopic.get(type);
                    if(matchedType == null) {
                        matchedType = new ConcurrentHashMap>>();
                        matchedTopic.put(type,  matchedType);
                    }

                    Map> matchedUuid = matchedType.get(uuid);
                    if(matchedUuid == null) {
                        matchedUuid = new ConcurrentHashMap>();
                        matchedType.put(uuid, matchedUuid);
                    }

                    callbacks = matchedUuid.get(name);
                    if (callbacks == null) {
                        callbacks = new CopyOnWriteArrayList<>();
                        matchedUuid.put(name, callbacks);
                    }
                }
            } else {
                callbacks = null;
            }
        }
        return callbacks;
    }

    private String getFullEventType(String topic, String type) {
        String fullType;
        if (topic.equals("application")) {
            fullType = (type.indexOf("application-") == -1) ? ("application-" + type) : type;
        }
        else if (topic.equals("system")) {
            fullType = type;
        }
        else if (topic.equals("window")) {
            fullType = (type.indexOf("window-") == -1) ? ("window-" + type) : type;
        } else {
            fullType = type;
        }
        return fullType;
    }

    /**
     * Registers an event listener on the specified event
     * @param subscriptionObject JSON object containing subscription information such as the topic and type
     * @param listener EventListener for the event
     * @param callback AckListener for this request
     * @param source Source of this request
     * @see EventListener
     * @see AckListener
     */
    public void addEventCallback(JSONObject subscriptionObject,
                                 EventListener listener,
                                 AckListener callback,
                                 Object source) {

        List callbacks = getEventCallbackList(subscriptionObject);
        if (callbacks != null) {
            // Notify core of first subscription
            if (callbacks.size() == 0) {
                String topic = subscriptionObject.getString("topic");
                String type = subscriptionObject.getString("type");
                if (topic.equals("window") && type.equals("app-connected")) {
                    // app-connected is generated differently from other window events
                    JSONObject notifyConnected = new JSONObject();
                    notifyConnected.put("targetUuid", subscriptionObject.getString("uuid"));
                    notifyConnected.put("name", subscriptionObject.getString("name"));
                    sendAction("notify-on-app-connected", notifyConnected, callback, source);
                }
                else if (topic.equals("window") && type.equals("app-loaded")) {
                    // app-loaded is generated differently from other window events
                    JSONObject notifyConnected = new JSONObject();
                    notifyConnected.put("targetUuid", subscriptionObject.getString("uuid"));
                    notifyConnected.put("name", subscriptionObject.getString("name"));
                    sendAction("notify-on-content-loaded", notifyConnected, callback, source);
                }
                else {
                    sendAction("subscribe-to-desktop-event", subscriptionObject, callback, source);
                }
            }
            try {
                callbacks.add(new ListenerSourcePair(listener, subscriptionObject.getString("type"), source));
                if (callbacks.size() > 1) {
                    // did not call sendAction so need to ack right away
                    if (callback != null) {
                        JSONObject payload = new JSONObject();
                        payload.put("success", true);
                        DesktopUtils.successAck(callback, new Ack(payload, source));
                    }
                }
            } catch (Exception e) {
                logger.error("Error adding event callback", e);
            }
        }
    }

    /**
     * Removes a previously registered event listener from the specified event
     * @param subscriptionObject JSON object containing subscription information such as the topic and type
     * @param listener EventListener that was registered before
     * @param callback AckListener for this request
     * @param source source of this request
     */
    public void removeEventCallback(JSONObject subscriptionObject,
                                    EventListener listener,
                                    AckListener callback,
                                    Object source)  {

        List callbacks = getEventCallbackList(subscriptionObject);
        if (callbacks != null) {
            removeFromListenerSourcePairList(callbacks, listener, source);
            // Notify core if no subscriptions remain
            if (callbacks.size() == 0) {
                String topic = subscriptionObject.getString("topic");
                String type = subscriptionObject.getString("type");
                if (topic.equals("window") && type.equals("app-connected")) {
                    // core does not support remove app-connected listeners
                }
                else if (topic.equals("window") && type.equals("app-loaded")) {
                    // core does not support remove app-loaded listeners
                }
                else {
                    sendAction("unsubscribe-to-desktop-event", subscriptionObject, callback, source);
                }
            }
        }
    }

    private void removeFromListenerSourcePairList(List list, EventListener listener, Object source) {
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ListenerSourcePair lsp = list.get(i);
                if (lsp.getEventListener() == listener && lsp.getSource() == source) {
                    list.remove(i);
                    break;
                }
            }
        }
    }

    private void dispatchDesktopEvent(JSONObject payload) {
        List callbacks = getEventCallbackList(payload);
        if (callbacks != null) {
            for (ListenerSourcePair lsp : callbacks) {
                lsp.getEventListener().eventReceived(new com.openfin.desktop.ActionEvent(lsp.getType(), payload, lsp.getSource()));
            }
        }
    }

    protected void respondToPing(long pingId) {
        JSONObject payload = new JSONObject();
        try {
            payload.put("correlationId", pingId);
            sendAction("pong", payload);
        } catch (Exception e) {
            logger.error("Error responding to ping", e);
        }
    }

    /**
     * Registers a listener to handle messages for this connection's UUID originating via HTTPS/HTTP
     * @param listener process a received HTTPS/HTTP message for this connection
     * @param source The object that originally registered the listener
     */
    public void addExternalMessageHandler(ExternalMessageListener listener, Object source) {
        externalMessageHandlers.add(new ExternalMessageListenerSourcePair(listener, source));
    }

    /**
     *
     * Dispatches the a payload from "process-external-message" to all registered ExternalMessageHandlerDelegates
     * and sends the result after all result handlers have set a success/fail result.
     *
     * @param payload The data to process
     */
    private void processExternalMessage(JSONObject payload) {
        ExternalMessageResultHandlerFactory messageHandlerFactory = new ExternalMessageResultHandlerFactory(uuid, this, payload);

        for (ExternalMessageListenerSourcePair pair : this.externalMessageHandlers) {
            pair.handler.process(messageHandlerFactory.makeResultHandler(), payload);
        }

        messageHandlerFactory.allDispatched();
    }

    /**
     * Dispatches event to app-connected listeners
     *
     * @param payload
     */
    private void processWindowConnected(JSONObject payload) {
        // app-connected is not generated as an event of window type
        // make it a window event
        String uuid = payload.getString("uuid");  // this is not the appUUID
        String appUuid = payload.getString("appUuid");
        String name = payload.getString("name");  // window name
        String topic = "window";
        String type = getFullEventType(topic, "app-connected");

        List list = this.getEventCallbackList(topic, type, appUuid, name);
        if (list != null) {
            for (ListenerSourcePair pair: list) {
                pair.getEventListener().eventReceived(new com.openfin.desktop.ActionEvent(pair.getType(), payload, pair.getSource()));
            }
        }
    }

    /**
     * Dispatches event to app-loadedlisteners
     *
     * @param payload
     */
    private void processWindowLoaded(JSONObject payload) {
        // app-loaded is not generated as an event of window type
        // make it a window event
        String uuid = payload.getString("uuid");  // this is not the appUUID
        String appUuid = payload.getString("appUuid");
        String name = payload.getString("name");  // window name
        String topic = "window";
        String type = getFullEventType(topic, "app-loaded");

        List list = this.getEventCallbackList(topic, type, appUuid, name);
        if (list != null) {
            for (ListenerSourcePair pair: list) {
                pair.getEventListener().eventReceived(new com.openfin.desktop.ActionEvent(pair.getType(), payload, pair.getSource()));
            }
        }
    }

    /**
     *
     * Set additional runtime arguments passed to the Runtime
     *
     * @param additionalRuntimeArguments additional arguments, such as "--v=1" to enable more logging
     * @deprecated use {@link RuntimeConfiguration} instead
     *
     */
    public void setAdditionalRuntimeArguments(String additionalRuntimeArguments) {
        this.additionalRuntimeArguments = additionalRuntimeArguments;
    }

    /**
     *
     * Set additional runtime arguments passed to the RVM
     *
     * @param additionalRvmArguments additional arguments
     * @deprecated use {@link RuntimeConfiguration} instead
     *
     */
    public void setAdditionalRvmArguments(String additionalRvmArguments) {
        this.additionalRvmArguments = additionalRvmArguments;
    }

    /**
     *
     * Set security realm of the Runtime
     * @param runtimeSecurityRealm name of the realm
     * @deprecated use {@link RuntimeConfiguration} instead
     *
     */
    public void setRuntimeSecurityRealm(String runtimeSecurityRealm) {
        this.runtimeSecurityRealm = runtimeSecurityRealm;
    }

    /**
     * Set URL of RDM service
     *
     * @param url URL of RDM service
     * @deprecated use {@link #connect(RuntimeConfiguration, DesktopStateListener, int)} instead.
     * @deprecated use {@link RuntimeConfiguration} instead
     */
    public void setRdmUrl(String url) {
        this.rdmUrl = url;
    }

    /**
     * Set port for accessing devtools on localhost
     * @param port port number
     * @deprecated use {@link RuntimeConfiguration} instead
     */
    public void setDevToolsPort(int port) {
        this.devtoolsPort = port;
    }

    /**
     * Set root URL/URI for the RVM and Runtime assets. This can be a CDN or other server location.
     *
     * RVM and Runtime assets should be served from the following paths:
     *
     * 
     *  url/rvm
     *  url/runtime
     * 
* * @param url URL of Runtime assets * @deprecated use {@link RuntimeConfiguration} instead */ public void setRuntimeAssetsUrl(String url) { this.runtimeAssetsUrl = url; } /** * Add a config for app asset * * @param config config string which must be a valid JSON string * @deprecated use {@link RuntimeConfiguration} instead */ public void addAppAsset(String config) { } /** * Set log level to be FINE * * @deprecated * @param enabled true for enabling DEBUG logging * * @deprecated use logging configuration file instead */ public void setLogLevel(boolean enabled) { } private static class ListenerSourcePair { private AckListener listener; private EventListener eventListener; private NotificationListener notificationListener; private Object source; private String type; ListenerSourcePair(AckListener listener, Object source) { this.listener = listener; this.source = source; } ListenerSourcePair(NotificationListener listener, Object source) { this.notificationListener = listener; this.source = source; } ListenerSourcePair(EventListener listener, String eventType, Object source) { this.eventListener = listener; this.type = eventType; this.source = source; } Object getSource() { return source; } AckListener getListener() { return listener; } NotificationListener getNotificationListener() { return this.notificationListener; } EventListener getEventListener() { return this.eventListener; } String getType() { return this.type; } } private static class ExternalMessageListenerSourcePair { private ExternalMessageListener handler; private Object source; public ExternalMessageListenerSourcePair(ExternalMessageListener handler, Object source) { this.handler = handler; this.source = source; } } private static String getProcessId(final String fallback) { // Note: may fail in some JVM implementations // therefore fallback has to be provided // something like '@', at least in SUN / Oracle JVMs final String jvmName = ManagementFactory.getRuntimeMXBean().getName(); final int index = jvmName.indexOf('@'); if (index < 1) { // part before '@' empty (index = 0) / '@' not found (index = -1) return fallback; } try { return Long.toString(Long.parseLong(jvmName.substring(0, index))); } catch (NumberFormatException e) { // ignore } return fallback; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy