com.vaadin.server.communication.ServerRpcHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.server.communication.ServerRpcHandler.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.server.communication;

import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.vaadin.server.ClientConnector;
import com.vaadin.server.Constants;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.LegacyCommunicationManager;
import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
import com.vaadin.server.ServerRpcManager;
import com.vaadin.server.ServerRpcManager.RpcInvocationException;
import com.vaadin.server.ServerRpcMethodInvocation;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VariableOwner;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector;
import com.vaadin.shared.Version;
import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
import com.vaadin.shared.communication.MethodInvocation;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.communication.UidlValue;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.ui.Component;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.ui.UI;

import elemental.json.JsonArray;
import elemental.json.JsonException;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import elemental.json.impl.JsonUtil;

/**
 * Handles a client-to-server message containing serialized {@link ServerRpc
 * server RPC} invocations.
 *
 * @author Vaadin Ltd
 * @since 7.1
 */
public class ServerRpcHandler implements Serializable {

    /**
     * A data transfer object representing an RPC request sent by the client
     * side.
     *
     * @since 7.2
     * @author Vaadin Ltd
     */
    public static class RpcRequest implements Serializable {

        private final String csrfToken;
        private final JsonArray invocations;
        private final int syncId;
        private final JsonObject json;
        private final boolean resynchronize;
        private final int clientToServerMessageId;
        private String widgetsetVersion = null;

        public RpcRequest(String jsonString, VaadinRequest request) {
            json = JsonUtil.parse(jsonString);

            JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
            if (token == null) {
                csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
            } else {
                String csrfToken = token.asString();
                if (csrfToken.isEmpty()) {
                    csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
                }
                this.csrfToken = csrfToken;
            }

            if (request.getService().getDeploymentConfiguration().isSyncIdCheckEnabled()) {
                syncId = (int) json.getNumber(ApplicationConstants.SERVER_SYNC_ID);
            } else {
                syncId = -1;
            }

            if (json.hasKey(ApplicationConstants.RESYNCHRONIZE_ID)) {
                resynchronize = json.getBoolean(ApplicationConstants.RESYNCHRONIZE_ID);
            } else {
                resynchronize = false;
            }
            if (json.hasKey(ApplicationConstants.WIDGETSET_VERSION_ID)) {
                widgetsetVersion = json.getString(ApplicationConstants.WIDGETSET_VERSION_ID);
            }

            if (json.hasKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) {
                clientToServerMessageId = (int) json.getNumber(ApplicationConstants.CLIENT_TO_SERVER_ID);
            } else {
                getLogger().warning("Server message without client id received");
                clientToServerMessageId = -1;
            }
            invocations = json.getArray(ApplicationConstants.RPC_INVOCATIONS);
        }

        /**
         * Gets the CSRF security token (double submit cookie) for this request.
         *
         * @return the CSRF security token for this current change request
         */
        public String getCsrfToken() {
            return csrfToken;
        }

        /**
         * Gets the data to recreate the RPC as requested by the client side.
         *
         * @return the data describing which RPC should be made, and all their
         *         data
         */
        public JsonArray getRpcInvocationsData() {
            return invocations;
        }

        /**
         * Gets the sync id last seen by the client.
         *
         * @return the last sync id given by the server, according to the
         *         client's request
         */
        public int getSyncId() {
            return syncId;
        }

        /**
         * Checks if this is a request to resynchronize the client side.
         *
         * @return true if this is a resynchronization request, false otherwise
         */
        public boolean isResynchronize() {
            return resynchronize;
        }

        /**
         * Gets the id of the client to server message.
         *
         * @since 7.6
         * @return the server message id
         */
        public int getClientToServerId() {
            return clientToServerMessageId;
        }

        /**
         * Gets the entire request in JSON format, as it was received from the
         * client.
         * <p>
         * <em>Note:</em> This is a shared reference - any modifications made
         * will be shared.
         *
         * @return the raw JSON object that was received from the client
         *
         */
        public JsonObject getRawJson() {
            return json;
        }

        /**
         * Gets the widget set version reported by the client.
         *
         * @since 7.6
         * @return The widget set version reported by the client or null if the
         *         message did not contain a widget set version
         */
        public String getWidgetsetVersion() {
            return widgetsetVersion;
        }
    }

    private static final int MAX_BUFFER_SIZE = 64 * 1024;

    /**
     * Reads JSON containing zero or more serialized RPC calls (including legacy
     * variable changes) and executes the calls.
     *
     * @param ui
     *            The {@link UI} receiving the calls. Cannot be null.
     * @param reader
     *            The {@link Reader} used to read the JSON.
     * @param request
     * @throws IOException
     *             If reading the message fails.
     * @throws InvalidUIDLSecurityKeyException
     *             If the received security key does not match the one stored in
     *             the session.
     */
    public void handleRpc(UI ui, Reader reader, VaadinRequest request)
            throws IOException, InvalidUIDLSecurityKeyException {
        ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());

        String changeMessage = getMessage(reader);

        if (changeMessage == null || changeMessage.isEmpty()) {
            // The client sometimes sends empty messages, this is probably a bug
            return;
        }

        RpcRequest rpcRequest = new RpcRequest(changeMessage, request);

        // Security: double cookie submission pattern unless disabled by
        // property
        if (!VaadinService.isCsrfTokenValid(ui.getSession(), rpcRequest.getCsrfToken())) {
            throw new InvalidUIDLSecurityKeyException("");
        }

        checkWidgetsetVersion(rpcRequest.getWidgetsetVersion());

        int expectedId = ui.getLastProcessedClientToServerId() + 1;
        if (rpcRequest.getClientToServerId() != -1 && rpcRequest.getClientToServerId() != expectedId) {
            // Invalid message id, skip RPC processing but force a full
            // re-synchronization of the client as it might have not received
            // the previous response (e.g. due to a bad connection)

            // Must resync also for duplicate messages because the server might
            // have generated a response for the first message but the response
            // did not reach the client. When the client re-sends the message,
            // it would only get an empty response (because the dirty flags have
            // been cleared on the server) and would be out of sync
            ui.getSession().getCommunicationManager().repaintAll(ui);

            if (rpcRequest.getClientToServerId() < expectedId) {
                // Just a duplicate message due to a bad connection or similar
                // It has already been handled by the server so it is safe to
                // ignore
                getLogger().fine("Ignoring old message from the client. Expected: " + expectedId + ", got: "
                        + rpcRequest.getClientToServerId());
            } else {
                getLogger().warning("Unexpected message id from the client. Expected: " + expectedId + ", got: "
                        + rpcRequest.getClientToServerId());
            }
        } else {
            // Message id ok, process RPCs
            ui.setLastProcessedClientToServerId(expectedId);
            handleInvocations(ui, rpcRequest.getSyncId(), rpcRequest.getRpcInvocationsData());
        }

        if (rpcRequest.isResynchronize()) {
            ui.getSession().getCommunicationManager().repaintAll(ui);
        }

    }

    /**
     * Checks that the version reported by the client (widgetset) matches that
     * of the server.
     *
     * @param widgetsetVersion
     *            the widget set version reported by the client or null
     */
    private void checkWidgetsetVersion(String widgetsetVersion) {
        if (widgetsetVersion == null) {
            // Only check when the widgetset version is reported. It is reported
            // in the first UIDL request (not the initial request as it is a
            // plain GET /)
            return;
        }

        if (!Version.getFullVersion().equals(widgetsetVersion)) {
            getLogger().warning(
                    String.format(Constants.WIDGETSET_MISMATCH_INFO, Version.getFullVersion(), widgetsetVersion));
        }
    }

    /**
     * Processes invocations data received from the client.
     * <p>
     * The invocations data can contain any number of RPC calls, including
     * legacy variable change calls that are processed separately.
     * <p>
     * Consecutive changes to the value of the same variable are combined and
     * changeVariables() is only called once for them. This preserves the Vaadin
     * 6 semantics for components and add-ons that do not use Vaadin 7 RPC
     * directly.
     *
     * @param ui
     *            the UI receiving the invocations data
     * @param lastSyncIdSeenByClient
     *            the most recent sync id the client has seen at the time the
     *            request was sent
     * @param invocationsData
     *            JSON containing all information needed to execute all
     *            requested RPC calls.
     * @since 7.7
     */
    protected void handleInvocations(UI ui, int lastSyncIdSeenByClient, JsonArray invocationsData) {
        // TODO PUSH Refactor so that this is not needed
        LegacyCommunicationManager manager = ui.getSession().getCommunicationManager();

        try {
            ConnectorTracker connectorTracker = ui.getConnectorTracker();

            Set<Connector> enabledConnectors = new HashSet<>();

            List<MethodInvocation> invocations = parseInvocations(ui.getConnectorTracker(), invocationsData,
                    lastSyncIdSeenByClient);
            for (MethodInvocation invocation : invocations) {
                final ClientConnector connector = connectorTracker.getConnector(invocation.getConnectorId());

                if (connector != null && connector.isConnectorEnabled()) {
                    enabledConnectors.add(connector);
                }
            }

            for (MethodInvocation invocation : invocations) {
                final ClientConnector connector = connectorTracker.getConnector(invocation.getConnectorId());
                if (connector == null) {
                    logUnknownConnector(invocation.getConnectorId(), invocation.getInterfaceName(),
                            invocation.getMethodName());
                    continue;
                }

                if (!enabledConnectors.contains(connector)) {

                    if (invocation instanceof LegacyChangeVariablesInvocation) {
                        LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
                        // TODO convert window close to a separate RPC call and
                        // handle above - not a variable change

                        // Handle special case where window-close is called
                        // after the window has been removed from the
                        // application or the application has closed
                        Map<String, Object> changes = legacyInvocation.getVariableChanges();
                        if (changes.size() == 1 && changes.containsKey("close")
                                && Boolean.TRUE.equals(changes.get("close"))) {
                            // Silently ignore this
                            continue;
                        }
                    } else if (invocation instanceof ServerRpcMethodInvocation) {
                        ServerRpcMethodInvocation rpc = (ServerRpcMethodInvocation) invocation;
                        // special case for data communicator requesting more
                        // data
                        if (DataRequestRpc.class.getName().equals(rpc.getInterfaceClass().getName())) {
                            handleInvocation(ui, connector, rpc);
                        }
                        continue;
                    }

                    // Connector is disabled, log a warning and move to the next
                    getLogger().warning(getIgnoredDisabledError("RPC call", connector));
                    continue;
                }
                // DragAndDropService has null UI
                if (connector.getUI() != null && connector.getUI().isClosing()) {
                    String msg = "Ignoring RPC call for connector " + connector.getClass().getName();
                    if (connector instanceof Component) {
                        String caption = ((Component) connector).getCaption();
                        if (caption != null) {
                            msg += ", caption=" + caption;
                        }
                    }
                    msg += " in closed UI";
                    getLogger().warning(msg);
                    continue;

                }

                if (invocation instanceof ServerRpcMethodInvocation) {
                    handleInvocation(ui, connector, (ServerRpcMethodInvocation) invocation);
                } else {
                    LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
                    handleInvocation(ui, connector, legacyInvocation);
                }
            }
        } catch (JsonException e) {
            getLogger().warning("Unable to parse RPC call from the client: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    private void logUnknownConnector(String connectorId, String interfaceName, String methodName) {
        getLogger().log(Level.FINE, "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})",
                new Object[] { connectorId, interfaceName, methodName });
    }

    /**
     * Handles the given RPC method invocation for the given connector.
     *
     * @since 7.7
     * @param ui
     *            the UI containing the connector
     * @param connector
     *            the connector the RPC is targeted to
     * @param invocation
     *            information about the rpc to invoke
     */
    protected void handleInvocation(UI ui, ClientConnector connector, ServerRpcMethodInvocation invocation) {
        try {
            ServerRpcManager.applyInvocation(connector, invocation);
        } catch (RpcInvocationException e) {
            ui.getSession().getCommunicationManager().handleConnectorRelatedException(connector, e);
        }

    }

    /**
     * Handles the given Legacy variable change RPC method invocation for the
     * given connector.
     *
     * @since 7.7
     * @param ui
     *            the UI containing the connector
     * @param connector
     *            the connector the RPC is targeted to
     * @param legacyInvocation
     *            information about the rpc to invoke
     */
    protected void handleInvocation(UI ui, ClientConnector connector,
            LegacyChangeVariablesInvocation legacyInvocation) {
        Map<String, Object> changes = legacyInvocation.getVariableChanges();
        try {
            if (connector instanceof VariableOwner) {
                // The source parameter is never used anywhere
                changeVariables(null, (VariableOwner) connector, changes);
            } else {
                throw new IllegalStateException("Received a legacy variable change for "
                        + connector.getClass().getName() + " (" + connector.getConnectorId()
                        + ") which is not a VariableOwner. The client-side connector sent these legacy variables: "
                        + changes.keySet());
            }
        } catch (Exception e) {
            ui.getSession().getCommunicationManager().handleConnectorRelatedException(connector, e);
        }

    }

    /**
     * Parse JSON from the client into a list of MethodInvocation instances.
     *
     * @param connectorTracker
     *            The ConnectorTracker used to lookup connectors
     * @param invocationsJson
     *            JSON containing all information needed to execute all
     *            requested RPC calls.
     * @param lastSyncIdSeenByClient
     *            the most recent sync id the client has seen at the time the
     *            request was sent
     * @return list of MethodInvocation to perform
     */
    private List<MethodInvocation> parseInvocations(ConnectorTracker connectorTracker, JsonArray invocationsJson,
            int lastSyncIdSeenByClient) {
        int invocationCount = invocationsJson.length();
        List<MethodInvocation> invocations = new ArrayList<>(invocationCount);

        MethodInvocation previousInvocation = null;
        // parse JSON to MethodInvocations
        for (int i = 0; i < invocationCount; ++i) {

            JsonArray invocationJson = invocationsJson.getArray(i);

            MethodInvocation invocation = parseInvocation(invocationJson, previousInvocation, connectorTracker,
                    lastSyncIdSeenByClient);
            if (invocation != null) {
                // Can be null if the invocation was a legacy invocation and it
                // was merged with the previous one or if the invocation was
                // rejected because of an error.
                invocations.add(invocation);
                previousInvocation = invocation;
            }
        }
        return invocations;
    }

    private MethodInvocation parseInvocation(JsonArray invocationJson, MethodInvocation previousInvocation,
            ConnectorTracker connectorTracker, long lastSyncIdSeenByClient) {
        String connectorId = invocationJson.getString(0);
        String interfaceName = invocationJson.getString(1);
        String methodName = invocationJson.getString(2);

        JsonArray parametersJson = invocationJson.getArray(3);

        if (LegacyChangeVariablesInvocation.isLegacyVariableChange(interfaceName, methodName)) {
            if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) {
                previousInvocation = null;
            }

            return parseLegacyChangeVariablesInvocation(connectorId, interfaceName, methodName,
                    (LegacyChangeVariablesInvocation) previousInvocation, parametersJson, connectorTracker);
        } else {
            return parseServerRpcInvocation(connectorId, interfaceName, methodName, parametersJson,
                    connectorTracker);
        }

    }

    private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(String connectorId,
            String interfaceName, String methodName, LegacyChangeVariablesInvocation previousInvocation,
            JsonArray parametersJson, ConnectorTracker connectorTracker) {
        if (parametersJson.length() != 2) {
            throw new JsonException("Invalid parameters in legacy change variables call. Expected 2, was "
                    + parametersJson.length());
        }
        String variableName = parametersJson.getString(0);
        UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType(UidlValue.class, true, parametersJson.get(1),
                connectorTracker);

        Object value = uidlValue.getValue();

        if (previousInvocation != null && previousInvocation.getConnectorId().equals(connectorId)) {
            previousInvocation.setVariableChange(variableName, value);
            return null;
        } else {
            return new LegacyChangeVariablesInvocation(connectorId, variableName, value);
        }
    }

    private ServerRpcMethodInvocation parseServerRpcInvocation(String connectorId, String interfaceName,
            String methodName, JsonArray parametersJson, ConnectorTracker connectorTracker) throws JsonException {
        ClientConnector connector = connectorTracker.getConnector(connectorId);
        if (connector == null) {
            logUnknownConnector(connectorId, interfaceName, methodName);
            return null;
        }
        ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName);
        if (rpcManager == null) {
            /*
             * Security: Don't even decode the json parameters if no RpcManager
             * corresponding to the received method invocation has been
             * registered.
             */
            String message = "Ignoring RPC call to " + interfaceName + "." + methodName + " in connector "
                    + connector.getClass().getName() + "(" + connectorId
                    + ") as no RPC implementation is registered";
            assert rpcManager != null : message;
            getLogger().warning(message);
            return null;
        }

        // Use interface from RpcManager instead of loading the class based on
        // the string name to avoid problems with OSGi
        Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface();

        ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(connectorId, rpcInterface, methodName,
                parametersJson.length());

        Object[] parameters = new Object[parametersJson.length()];
        Type[] declaredRpcMethodParameterTypes = invocation.getMethod().getGenericParameterTypes();

        for (int j = 0; j < parametersJson.length(); ++j) {
            JsonValue parameterValue = parametersJson.get(j);
            Type parameterType = declaredRpcMethodParameterTypes[j];
            parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, parameterValue, connectorTracker);
        }
        invocation.setParameters(parameters);
        return invocation;
    }

    protected void changeVariables(Object source, VariableOwner owner, Map<String, Object> m) {
        owner.changeVariables(source, m);
    }

    protected String getMessage(Reader reader) throws IOException {

        StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
        char[] buffer = new char[MAX_BUFFER_SIZE];

        while (true) {
            int read = reader.read(buffer);
            if (read == -1) {
                break;
            }
            sb.append(buffer, 0, read);
        }

        return sb.toString();
    }

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

    /**
     * Generates an error message when the client is trying to to something
     * ('what') with a connector which is disabled or invisible.
     *
     * @since 7.1.8
     * @param connector
     *            the connector which is disabled (or invisible)
     * @return an error message
     */
    public static String getIgnoredDisabledError(String what, ClientConnector connector) {
        String msg = "Ignoring " + what + " for disabled connector " + connector.getClass().getName();
        if (connector instanceof Component) {
            String caption = ((Component) connector).getCaption();
            if (caption != null) {
                msg += ", caption=" + caption;
            }
        }
        return msg;
    }
}