com.vaadin.client.JavaScriptConnectorHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.client.JavaScriptConnectorHelper.java

Source

/*
 * Copyright 2000-2018 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.vaadin.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.dom.client.Element;
import com.vaadin.client.communication.JavaScriptMethodInvocation;
import com.vaadin.client.communication.ServerRpcQueue;
import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.JavaScriptConnectorState;
import com.vaadin.shared.communication.MethodInvocation;

import elemental.json.JsonArray;

public class JavaScriptConnectorHelper {

    private final ServerConnector connector;
    private final JavaScriptObject nativeState = JavaScriptObject.createObject();
    private final JavaScriptObject rpcMap = JavaScriptObject.createObject();

    private final Map<String, JavaScriptObject> rpcObjects = new HashMap<>();
    private final Map<String, Set<String>> rpcMethods = new HashMap<>();
    private final Map<Element, Map<JavaScriptObject, ElementResizeListener>> resizeListeners = new HashMap<>();

    private JavaScriptObject connectorWrapper;

    private String initFunctionName;
    private String tagName;

    public JavaScriptConnectorHelper(ServerConnector connector) {
        this.connector = connector;

        // Wildcard rpc object
        rpcObjects.put("", JavaScriptObject.createObject());
    }

    /**
     * The id of the previous response for which state changes have been
     * processed. If this is the same as the
     * {@link ApplicationConnection#getLastSeenServerSyncId()}, it means that
     * the state change has already been handled and should not be done again.
     */
    private int processedResponseId = -1;

    public void init() {
        connector.addStateChangeHandler(event -> processStateChanges());
    }

    /**
     * Makes sure the javascript part of the connector has been initialized. The
     * javascript is usually initalized the first time a state change event is
     * received, but it might in some cases be necessary to make this happen
     * earlier.
     *
     * @since 7.4.0
     */
    public void ensureJavascriptInited() {
        if (initFunctionName == null) {
            processStateChanges();
        }
    }

    private void processStateChanges() {
        int lastResponseId = connector.getConnection().getLastSeenServerSyncId();
        if (processedResponseId == lastResponseId) {
            return;
        }
        processedResponseId = lastResponseId;

        JavaScriptObject wrapper = getConnectorWrapper();
        JavaScriptConnectorState state = getConnectorState();

        for (String callback : state.getCallbackNames()) {
            ensureCallback(JavaScriptConnectorHelper.this, wrapper, callback);
        }

        for (Entry<String, Set<String>> entry : state.getRpcInterfaces().entrySet()) {
            String rpcName = entry.getKey();
            String jsName = getJsInterfaceName(rpcName);
            if (!rpcObjects.containsKey(jsName)) {
                Set<String> methods = entry.getValue();
                rpcObjects.put(jsName, createRpcObject(rpcName, methods));

                // Init all methods for wildcard rpc
                for (String method : methods) {
                    JavaScriptObject wildcardRpcObject = rpcObjects.get("");
                    Set<String> interfaces = rpcMethods.get(method);
                    if (interfaces == null) {
                        interfaces = new HashSet<>();
                        rpcMethods.put(method, interfaces);
                        attachRpcMethod(wildcardRpcObject, null, method);
                    }
                    interfaces.add(rpcName);
                }
            }
        }

        // Init after setting up callbacks & rpc
        if (initFunctionName == null) {
            initJavaScript();
        }

        invokeIfPresent(wrapper, "onStateChange");
    }

    private static String getJsInterfaceName(String rpcName) {
        return rpcName.replace('$', '.');
    }

    protected JavaScriptObject createRpcObject(String iface, Set<String> methods) {
        JavaScriptObject object = JavaScriptObject.createObject();

        for (String method : methods) {
            attachRpcMethod(object, iface, method);
        }

        return object;
    }

    protected boolean initJavaScript() {
        List<String> initFunctionNames = getPotentialInitFunctionNames();
        for (String initFunctionName : initFunctionNames) {
            if (tryInitJs(initFunctionName, getConnectorWrapper())) {
                getLogger().info("JavaScript connector initialized using " + initFunctionName);
                this.initFunctionName = initFunctionName;
                return true;
            } else {
                getLogger().warning("No JavaScript function " + initFunctionName + " found");
            }
        }
        getLogger().info("No JavaScript init for connector found");
        showInitProblem(initFunctionNames);
        return false;
    }

    protected void showInitProblem(List<String> attemptedNames) {
        // Default does nothing
    }

    private static native boolean tryInitJs(String initFunctionName, JavaScriptObject connectorWrapper)
    /*-{
    if (typeof $wnd[initFunctionName] == 'function') {
        $wnd[initFunctionName].apply(connectorWrapper);
        return true;
    } else {
        return false;
    }
    }-*/;

    public JavaScriptObject getConnectorWrapper() {
        if (connectorWrapper == null) {
            connectorWrapper = createConnectorWrapper(this, connector.getConnection(), nativeState, rpcMap,
                    connector.getConnectorId(), rpcObjects);
        }

        return connectorWrapper;
    }

    private static native JavaScriptObject createConnectorWrapper(JavaScriptConnectorHelper h,
            ApplicationConnection c, JavaScriptObject nativeState, JavaScriptObject registeredRpc,
            String connectorId, Map<String, JavaScriptObject> rpcObjects)
    /*-{
    return {
        'getConnectorId': function() {
            return connectorId;
        },
        'getParentId': $entry(function(connectorId) {
            return h.@com.vaadin.client.JavaScriptConnectorHelper::getParentId(Ljava/lang/String;)(connectorId);
        }),
        'getState': function() {
            return nativeState;
        },
        'getRpcProxy': $entry(function(iface) {
            if (!iface) {
                iface = '';
            }
            return rpcObjects.@java.util.Map::get(Ljava/lang/Object;)(iface);
        }),
        'getElement': $entry(function(connectorId) {
            return h.@com.vaadin.client.JavaScriptConnectorHelper::getWidgetElement(Ljava/lang/String;)(connectorId);
        }),
        'registerRpc': function(iface, rpcHandler) {
            //registerRpc(handler) -> registerRpc('', handler);
            if (!rpcHandler) {
                rpcHandler = iface;
                iface = '';
            }
            if (!registeredRpc[iface]) {
                registeredRpc[iface] = [];
            }
            registeredRpc[iface].push(rpcHandler);
        },
        'translateVaadinUri': $entry(function(uri) {
            return c.@com.vaadin.client.ApplicationConnection::translateVaadinUri(Ljava/lang/String;)(uri);
        }),
        'addResizeListener': function(element, resizeListener) {
            if (!element || element.nodeType != 1) throw "element must be defined";
            if (typeof resizeListener != "function") throw "resizeListener must be defined";
            $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::addResizeListener(*)).call(h, element, resizeListener);
        },
        'removeResizeListener': function(element, resizeListener) {
            if (!element || element.nodeType != 1) throw "element must be defined";
            if (typeof resizeListener != "function") throw "resizeListener must be defined";
            $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::removeResizeListener(*)).call(h, element, resizeListener);
        }
    };
    }-*/;

    // Called from JSNI to add a listener
    private void addResizeListener(Element element, final JavaScriptObject callbackFunction) {
        Map<JavaScriptObject, ElementResizeListener> elementListeners = resizeListeners.get(element);
        if (elementListeners == null) {
            elementListeners = new HashMap<>();
            resizeListeners.put(element, elementListeners);
        }

        ElementResizeListener listener = elementListeners.get(callbackFunction);
        if (listener == null) {
            LayoutManager layoutManager = LayoutManager.get(connector.getConnection());
            listener = event -> invokeElementResizeCallback(event.getElement(), callbackFunction);
            layoutManager.addElementResizeListener(element, listener);
            elementListeners.put(callbackFunction, listener);
        }
    }

    private static native void invokeElementResizeCallback(Element element, JavaScriptObject callbackFunction)
    /*-{
    // Call with a simple event object and 'this' pointing to the global scope
    callbackFunction.call($wnd, {'element': element});
    }-*/;

    // Called from JSNI to remove a listener
    private void removeResizeListener(Element element, JavaScriptObject callbackFunction) {
        Map<JavaScriptObject, ElementResizeListener> listenerMap = resizeListeners.get(element);
        if (listenerMap == null) {
            return;
        }

        ElementResizeListener listener = listenerMap.remove(callbackFunction);
        if (listener != null) {
            LayoutManager.get(connector.getConnection()).removeElementResizeListener(element, listener);
            if (listenerMap.isEmpty()) {
                resizeListeners.remove(element);
            }
        }
    }

    private native void attachRpcMethod(JavaScriptObject rpc, String iface, String method)
    /*-{
    var self = this;
    rpc[method] = $entry(function() {
        self.@com.vaadin.client.JavaScriptConnectorHelper::fireRpc(Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(iface, method, arguments);
    });
    }-*/;

    private String getParentId(String connectorId) {
        ServerConnector target = getConnector(connectorId);
        if (target == null) {
            return null;
        }
        ServerConnector parent = target.getParent();
        if (parent == null) {
            return null;
        } else {
            return parent.getConnectorId();
        }
    }

    private Element getWidgetElement(String connectorId) {
        ServerConnector target = getConnector(connectorId);
        if (target instanceof ComponentConnector) {
            return ((ComponentConnector) target).getWidget().getElement();
        } else {
            return null;
        }
    }

    private ServerConnector getConnector(String connectorId) {
        if (connectorId == null || connectorId.isEmpty()) {
            return connector;
        }

        return ConnectorMap.get(connector.getConnection()).getConnector(connectorId);
    }

    private void fireRpc(String iface, String method, JsArray<JavaScriptObject> arguments) {
        if (iface == null) {
            iface = findWildcardInterface(method);
        }

        JsonArray argumentsArray = Util.jso2json(arguments);
        Object[] parameters = new Object[arguments.length()];
        for (int i = 0; i < parameters.length; i++) {
            parameters[i] = argumentsArray.get(i);
        }
        ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
        rpcQueue.add(new JavaScriptMethodInvocation(connector.getConnectorId(), iface, method, parameters), false);
        rpcQueue.flush();
    }

    private String findWildcardInterface(String method) {
        Set<String> interfaces = rpcMethods.get(method);
        if (interfaces.size() == 1) {
            return interfaces.iterator().next();
        } else {
            // TODO Resolve conflicts using argument count and types
            String interfaceList = "";
            for (String iface : interfaces) {
                if (!interfaceList.isEmpty()) {
                    interfaceList += ", ";
                }
                interfaceList += getJsInterfaceName(iface);
            }

            throw new IllegalStateException("Can not call method " + method
                    + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: "
                    + interfaceList
                    + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function.");
        }
    }

    private void fireCallback(String name, JsArray<JavaScriptObject> arguments) {
        MethodInvocation invocation = new JavaScriptMethodInvocation(connector.getConnectorId(),
                "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call", new Object[] { name, arguments });
        ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
        rpcQueue.add(invocation, false);
        rpcQueue.flush();
    }

    public void setNativeState(JavaScriptObject state) {
        updateNativeState(nativeState, state);
    }

    private static native void updateNativeState(JavaScriptObject state, JavaScriptObject input)
    /*-{
    // Copy all fields to existing state object
    for (var key in input) {
        if (input.hasOwnProperty(key)) {
            state[key] = input[key];
        }
    }
    }-*/;

    public Object[] decodeRpcParameters(JsonArray parametersJson) {
        return new Object[] { Util.json2jso(parametersJson) };
    }

    public void invokeJsRpc(MethodInvocation invocation, JsonArray parametersJson) {
        String iface = invocation.getInterfaceName();
        String method = invocation.getMethodName();
        if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface) && "call".equals(method)) {
            String callbackName = parametersJson.getString(0);
            JavaScriptObject arguments = Util.json2jso(parametersJson.get(1));
            invokeCallback(getConnectorWrapper(), callbackName, arguments);
        } else {
            JavaScriptObject arguments = Util.json2jso(parametersJson);
            invokeJsRpc(rpcMap, iface, method, arguments);
            // Also invoke wildcard interface
            invokeJsRpc(rpcMap, "", method, arguments);
        }
    }

    private static native void invokeCallback(JavaScriptObject connector, String name, JavaScriptObject arguments)
    /*-{
    connector[name].apply(connector, arguments);
    }-*/;

    private static native void invokeJsRpc(JavaScriptObject rpcMap, String interfaceName, String methodName,
            JavaScriptObject parameters)
    /*-{
    var targets = rpcMap[interfaceName];
    if (!targets) {
        return;
    }
    for (var i = 0; i < targets.length; i++) {
        var target = targets[i];
        target[methodName].apply(target, parameters);
    }
    }-*/;

    private static native void ensureCallback(JavaScriptConnectorHelper h, JavaScriptObject connector, String name)
    /*-{
    connector[name] = $entry(function() {
        var args = Array.prototype.slice.call(arguments, 0);
        h.@com.vaadin.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
    });
    }-*/;

    private JavaScriptConnectorState getConnectorState() {
        return (JavaScriptConnectorState) connector.getState();
    }

    public void onUnregister() {
        invokeIfPresent(connectorWrapper, "onUnregister");

        if (!resizeListeners.isEmpty()) {
            LayoutManager layoutManager = LayoutManager.get(connector.getConnection());
            for (Entry<Element, Map<JavaScriptObject, ElementResizeListener>> entry : resizeListeners.entrySet()) {
                Element element = entry.getKey();
                for (ElementResizeListener listener : entry.getValue().values()) {
                    layoutManager.removeElementResizeListener(element, listener);
                }
            }
            resizeListeners.clear();
        }
    }

    private static native void invokeIfPresent(JavaScriptObject connectorWrapper, String functionName)
    /*-{
    if (typeof connectorWrapper[functionName] == 'function') {
        connectorWrapper[functionName].apply(connectorWrapper, arguments);
    }
    }-*/;

    public String getInitFunctionName() {
        return initFunctionName;
    }

    private List<String> getPotentialInitFunctionNames() {
        ApplicationConfiguration conf = connector.getConnection().getConfiguration();
        List<String> initFunctionNames = new ArrayList<String>();
        Integer tag = Integer.valueOf(connector.getTag());
        while (tag != null) {
            String initFunctionName = conf.getServerSideClassNameForTag(tag);
            initFunctionName = initFunctionName.replaceAll("\\.", "_");
            initFunctionNames.add(initFunctionName);
            tag = conf.getParentTag(tag);
        }
        return initFunctionNames;
    }

    public String getTagName() {
        if (tagName != null) {
            return tagName;
        }
        for (String initFunctionName : getPotentialInitFunctionNames()) {
            tagName = getTagJs(initFunctionName);
            if (tagName != null) {
                return tagName;
            }
        }
        // No tagName found, use default
        tagName = "div";
        return tagName;
    }

    private static native String getTagJs(String initFunctionName)
    /*-{
    if ($wnd[initFunctionName] && typeof $wnd[initFunctionName].tag == 'string') {
        return $wnd[initFunctionName].tag;
    } else {
        return null;
    }
    }-*/;

    private static Logger getLogger() {
        return Logger.getLogger(JavaScriptConnectorHelper.class.getName());
    }
}