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

com.vaadin.client.JavaScriptConnectorHelper Maven / Gradle / Ivy

Go to download

Vaadin is a web application framework for Rich Internet Applications (RIA). Vaadin enables easy development and maintenance of fast and secure rich web applications with a stunning look and feel and a wide browser support. It features a server-side architecture with the majority of the logic running on the server. Ajax technology is used at the browser-side to ensure a rich and interactive user experience.

There is a newer version: 8.27.1
Show newest version
/*
 * Copyright 2000-2016 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.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.communication.StateChangeEvent;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
import com.vaadin.client.ui.layout.ElementResizeEvent;
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 rpcObjects = new HashMap<>();
    private final Map> rpcMethods = new HashMap<>();
    private final Map> resizeListeners = new HashMap<>();

    private JavaScriptObject connectorWrapper;
    private int tag;

    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(new StateChangeHandler() {
            @Override
            public void onStateChanged(StateChangeEvent stateChangeEvent) {
                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> entry : state.getRpcInterfaces()
                .entrySet()) {
            String rpcName = entry.getKey();
            String jsName = getJsInterfaceName(rpcName);
            if (!rpcObjects.containsKey(jsName)) {
                Set methods = entry.getValue();
                rpcObjects.put(jsName, createRpcObject(rpcName, methods));

                // Init all methods for wildcard rpc
                for (String method : methods) {
                    JavaScriptObject wildcardRpcObject = rpcObjects.get("");
                    Set 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 methods) {
        JavaScriptObject object = JavaScriptObject.createObject();

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

        return object;
    }

    protected boolean initJavaScript() {
        ArrayList 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(ArrayList 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 rpcObjects)
    /*-{
        return {
            'getConnectorId': function() {
                return connectorId;
            },
            'getParentId': $entry(function(connectorId) {
                return [email protected]::getParentId(Ljava/lang/String;)(connectorId);
            }),
            'getState': function() {
                return nativeState;
            },
            'getRpcProxy': $entry(function(iface) {
                if (!iface) {
                    iface = '';
                }
                return [email protected]::get(Ljava/lang/Object;)(iface);
            }),
            'getElement': $entry(function(connectorId) {
                return [email protected]::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 [email protected]::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([email protected]::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([email protected]::removeResizeListener(*)).call(h, element, resizeListener);
            }
        };
    }-*/;

    // Called from JSNI to add a listener
    private void addResizeListener(Element element,
            final JavaScriptObject callbackFunction) {
        Map 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 = new ElementResizeListener() {
                @Override
                public void onElementResize(ElementResizeEvent e) {
                    invokeElementResizeCallback(e.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 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() {
            [email protected]::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.length() == 0) {
            return connector;
        }

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

    private void fireRpc(String iface, String method,
            JsArray 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 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.length() != 0) {
                    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 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 state) {
            if (state.hasOwnProperty(key)) {
                delete state[key];
            }
        }
    
        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 setTag(int tag) {
        this.tag = tag;
    }

    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);
            [email protected]::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> 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 ArrayList getPotentialInitFunctionNames() {
        ApplicationConfiguration conf = connector.getConnection()
                .getConfiguration();
        ArrayList initFunctionNames = new ArrayList();
        Integer tag = Integer.valueOf(this.tag);
        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());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy