com.kk_electronic.kkportal.core.rpc.RpcDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for com.kk_electronic.kkportal.core.rpc.RpcDispatcher.java

Source

/*
 * Copyright 2010 kk-electronic a/s. 
 * 
 * This file is part of KKPortal.
 *
 * KKPortal is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KKPortal is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with KKPortal.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kk_electronic.kkportal.core.rpc;

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

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.kk_electronic.kkportal.core.event.FrameReceivedEvent;
import com.kk_electronic.kkportal.core.event.FrameReceivedHandler;
import com.kk_electronic.kkportal.core.event.FrameSentEvent;
import com.kk_electronic.kkportal.core.event.FrameSentHandler;
import com.kk_electronic.kkportal.core.event.INotificationEvent;
import com.kk_electronic.kkportal.core.inject.FlexInjector;
import com.kk_electronic.kkportal.core.reflection.FeatureMap;
import com.kk_electronic.kkportal.core.reflection.SecurityMap;
import com.kk_electronic.kkportal.core.security.SecurityMethod;

/**
 * RpcDispatcher is a flexible {@link Dispatcher} for use with direct server side communication
 * using bundling.
 * 
 * It assumes that the server can handle the connection type by the class implementing {@link WebSocket}
 * and the it uses the correct {@link FrameEncoder} class to transmit the rpc calls, and that the rpc
 * calls and {@link RemoteServer} is supported by the backend without any security wrapper.
 *
 * After connecting is uses a call the {@link RemoteServer#getSecurityMap(AsyncCallback)} to get a list
 * of the available method and the security requirements.
 * 
 * It then transmits the calls as fast as possible using bundling techniques for incoming and outgoing
 * frames. The actual bundling and data format is defined by the {@link FrameEncoder}. 
 * 
 * TODO: split feature and security handling into separate classes.
 * @author Jes Andersen
 *
 */
@Singleton
public class RpcDispatcher implements OpenHandler<WebSocket>, CloseHandler<WebSocket>, FrameReceivedHandler,
        FrameSentHandler, Dispatcher {

    private final IdCreator<Integer> idCreator;

    private Map<Class<? extends RemoteService>, SecurityMethod> authenticationMethods = new HashMap<Class<? extends RemoteService>, SecurityMethod>();

    private Set<String> serverFeatures;

    private final WebSocket socket;

    private final FrameEncoder frameEncoder;

    private final FeatureMap clientFeatureMap;

    private final SecurityMap clientSecurityMap;

    private final FlexInjector injector;

    private final HandlerManager eventbus;

    /**
     * A holder to keep metainformation about the calls made.
     * should generally have no use outside this class.
     * @author Jes Andersen
     *
     * @param <T> used to provide type safety for the callback
     */
    public final static class PendingCall<T> implements AsyncCallback<Response> {
        private final AsyncCallback<T> callback;
        private Request request;
        private PendingCallStatus status;
        private final FrameEncoder encoder;
        private final Class<?>[] returnValueType;

        public PendingCall(AsyncCallback<T> callback, Request request, FrameEncoder encoder,
                Class<?>[] returnValueType) {
            this.callback = callback;
            this.request = request;
            this.encoder = encoder;
            this.returnValueType = returnValueType;
            status = PendingCallStatus.NEW;
        }

        public void setStatus(PendingCallStatus status) {
            this.status = status;
        }

        public PendingCallStatus getStatus() {
            return status;
        }

        public void setRequest(Request request) {
            this.request = request;
        }

        @Override
        public void onFailure(Throwable caught) {
            if (callback != null) {
                callback.onFailure(caught);
            }
        }

        @Override
        public void onSuccess(Response response) {
            T result = encoder.<T>decodeResult(returnValueType, response.getResult());
            if (callback != null) {
                callback.onSuccess(result);
            }
        }
    }

    @Inject
    public RpcDispatcher(IdCreator<Integer> idCreator, WebSocket socket, FrameEncoder encoder,
            SecurityMap clientSecurityMap, FeatureMap clientFeatureMap, FlexInjector injector,
            HandlerManager eventbus) {
        this.idCreator = idCreator;
        this.socket = socket;
        this.frameEncoder = encoder;
        this.clientSecurityMap = clientSecurityMap;
        this.clientFeatureMap = clientFeatureMap;
        this.injector = injector;
        this.eventbus = eventbus;
        authenticationMethods.put(RemoteServer.class, null);
        socket.addOpenHandler(this);
        socket.addCloseHandler(this);
        socket.addFrameReceivedHandler(this);
        socket.addFrameSentHandler(this);
    }

    public <T> void execute(AsyncCallback<T> callback, Class<?>[] returnValueType,
            Class<? extends RemoteService> serverinterface, String method, Object... params) {
        if (serverFeatures != null && serverFeatures.contains(serverinterface)) {
            callback.onFailure(new Exception("Feature not supported on server"));
        }
        String featureName = clientFeatureMap.getKeyFromClass(serverinterface);
        if (featureName == null) {
            GWT.log("RPC-could not map class to feature: " + serverinterface.getName());
        }
        Request request = new Request(featureName, method, params);
        //TODO: Delay Creation of id
        request.setId(idCreator.getNextId());
        final PendingCall<T> pendingCall = new PendingCall<T>(callback, request, frameEncoder, returnValueType);
        pending.put(request.getId(), pendingCall);

        if (authenticationMethods.containsKey(serverinterface)) {
            SecurityMethod securityMethod = authenticationMethods.get(serverinterface);
            if (securityMethod == null) {
                transmit(pendingCall);
            } else {
                signAndTransmit(pendingCall, securityMethod);
            }
        } else {
            updateServerSecurityMap();
            GWT.log("RPC-Delaying call due to unresolved security: " + request.getFeatureName() + "."
                    + request.getMethod());
        }
    }

    protected void signAndTransmit(final PendingCall<?> pendingCall, SecurityMethod securityMethod) {
        pendingCall.status = PendingCallStatus.WAITING_FOR_SECURITY;
        securityMethod.sign(pendingCall.request, new AsyncCallback<Request>() {

            @Override
            public void onFailure(Throwable caught) {
                GWT.log("RPC-Security could not sign request - aborting call", caught);
            }

            @Override
            public void onSuccess(Request result) {
                pendingCall.setRequest(result);
                transmit(pendingCall);
            }
        });
    }

    protected void transmit(PendingCall<?> pendingCall) {
        pendingCall.setStatus(PendingCallStatus.WAITING_FOR_TRANSMIT);
        txQueue.add(pendingCall.request);
        sendQueue();
    }

    protected void sendQueue() {
        if (txQueue.isEmpty())
            return;

        if (socket.isConnected()) {
            if (!socket.isTxBusy()) {
                socket.send(frameEncoder.encode(txQueue));
                for (Request request : txQueue) {
                    pending.get(request.getId()).setStatus(PendingCallStatus.WAITING_FOR_RESPONSE);
                }
                txQueue.clear();
            }
        } else {
            socket.connect(GWT.getHostPageBaseURL() + "websocket", "kk-entity");
        }
    }

    List<Request> txQueue = new ArrayList<Request>();

    Map<Integer, PendingCall<?>> pending = new HashMap<Integer, PendingCall<?>>();

    private void cancelCallsWithUnsupportedFeatures() {
        for (PendingCall<?> call : pending.values()) {
            if (!serverFeatures.contains(call.request.getFeatureName())) {
                call.onFailure(new Exception("Feature not supported on server"));
            }
        }
    }

    private void addFeature(final String featureName, final String securityName) {
        final Class<? extends RemoteService> feature = clientFeatureMap.getClassFromKey(featureName);
        if (feature == null) {
            GWT.log("RPC-PortalServer has unknown feature " + featureName);
            return;
        }
        if (securityName == null) {
            GWT.log("RPC-PortalServer Feature " + featureName + " with no security enabled");
            authenticationMethods.put(feature, null);
        } else {
            Class<? extends SecurityMethod> security = clientSecurityMap.getClassFromKey(securityName);
            if (security == null) {
                GWT.log("RPC-PortalServer Feature " + featureName + " has unknown security " + securityName);
                return;
            }
            loadAuthenticationMethod(feature, security);
        }
    }

    private void loadAuthenticationMethod(final Class<? extends RemoteService> feature,
            final Class<? extends SecurityMethod> security) {
        injector.create(security, new AsyncCallback<SecurityMethod>() {
            @Override
            public void onFailure(Throwable caught) {
                GWT.log("RPC-PortalServer Feature " + clientFeatureMap.getKeyFromClass(feature) + " with security "
                        + clientSecurityMap.getKeyFromClass(security) + " disabled since code cannot be loaded",
                        caught);
                cancelCallsWithFeature(feature);
            }

            @Override
            public void onSuccess(SecurityMethod result) {
                GWT.log("RPC-PortalServer Feature " + clientFeatureMap.getKeyFromClass(feature) + " with security "
                        + clientSecurityMap.getKeyFromClass(security) + " enabled");
                addAuthenticationMethod(feature, result);
            }
        });
    }

    protected void cancelCallsWithFeature(Class<? extends RemoteService> feature) {
        Iterator<PendingCall<?>> i = pending.values().iterator();
        String featureName = clientFeatureMap.getKeyFromClass(feature);
        for (PendingCall<?> call = i.next(); i.hasNext(); call = i.next()) {
            if (call.getStatus() == PendingCallStatus.NEW && call.request.getFeatureName().equals(featureName)) {
                call.onFailure(new Exception("RPC-Could not load security method"));
                i.remove();
            }
        }
    }

    protected void addAuthenticationMethod(Class<? extends RemoteService> feature, SecurityMethod securityMethod) {
        authenticationMethods.put(feature, securityMethod);
        String featureName = clientFeatureMap.getKeyFromClass(feature);
        for (PendingCall<?> call : pending.values()) {
            if (call.getStatus() == PendingCallStatus.NEW && call.request.getFeatureName().equals(featureName)) {
                signAndTransmit(call, securityMethod);
            }
        }
    }

    private boolean isUpdating = false;

    /**
     * this updates the the feature and security map of the server.
     */
    public void updateServerSecurityMap() {
        if (isUpdating)
            return;
        isUpdating = true;
        AsyncCallback<Map<String, String>> x = new AsyncCallback<Map<String, String>>() {

            @Override
            public void onSuccess(Map<String, String> result) {
                serverFeatures = new HashSet<String>();
                for (Entry<String, String> entry : result.entrySet()) {
                    serverFeatures.add(entry.getKey());
                    addFeature(entry.getKey(), entry.getValue());
                }
                cancelCallsWithUnsupportedFeatures();
            }

            @Override
            public void onFailure(Throwable caught) {
                GWT.log("RPC-The server does not fully support the kkProtocol", caught);
                socket.close();
            }
        };
        /*
         * To remove a circular dependency we make the call directly here
         */
        execute(x, new Class<?>[] { Map.class, String.class, String.class },
                com.kk_electronic.kkportal.core.rpc.RemoteServer.class, "getSecurityMap");
    }

    @Override
    public void onOpen(OpenEvent<WebSocket> event) {
        sendQueue();
        updateServerSecurityMap();
    }

    @Override
    public void onClose(CloseEvent<WebSocket> event) {

    }

    @Override
    public void onFrameReceived(FrameReceivedEvent event) {
        List<Response> responses = frameEncoder.decode(event.getData());
        for (Response response : responses) {
            if (response == null)
                continue;
            if (response.isNotification()) {
                GWT.log("NOTIFY-" + response.getMethod());
                List<Object> params = frameEncoder.decodeResult(new Class<?>[] { List.class, Object.class },
                        response.getParams());
                String s1 = frameEncoder.decodeResult(new Class<?>[] { String.class }, params.get(0));
                String s2 = frameEncoder.decodeResult(new Class<?>[] { String.class }, params.get(1));
                INotificationEvent.fire(eventbus, s1, s2);
                continue;
            }
            PendingCall<?> pendingCall = pending.remove(response.getId());
            if (response.hasError()) {
                pendingCall.setStatus(PendingCallStatus.DONE);
                pending.remove(response.getId());
                pendingCall.onFailure(new Exception(response.getError().getMessage()));
            } else {
                pendingCall.onSuccess(response);
            }
        }
    }

    @Override
    public void onFrameSent(FrameSentEvent frameSentEvent) {
        DeferredCommand.addCommand(new Command() {

            @Override
            public void execute() {
                sendQueue();
            }
        });
    }
}