/*
* 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.core.client.Scheduler;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Command;
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.FrameSentEvent;
import com.kk_electronic.kkportal.core.event.ServerConnectEvent;
import com.kk_electronic.kkportal.core.event.ServerDisconnectEvent;
import com.kk_electronic.kkportal.core.event.ServerEvent;
import com.kk_electronic.kkportal.core.inject.FlexInjector;
import com.kk_electronic.kkportal.core.reflection.EventFromJsonCreator;
import com.kk_electronic.kkportal.core.reflection.FeatureMap;
import com.kk_electronic.kkportal.core.reflection.SecurityMap;
import com.kk_electronic.kkportal.core.reflection.ServerEventMap;
import com.kk_electronic.kkportal.core.rpc.jsonformat.UnableToDeserialize;
import com.kk_electronic.kkportal.core.rpc.jsonformat.UnableToSerialize;
import com.kk_electronic.kkportal.core.security.SecurityMethod;
import com.kk_electronic.kkportal.core.util.Stats;
/**
* 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 FrameSentEvent.Handler, Dispatcher,
ServerConnectEvent.Handler, ServerDisconnectEvent.Handler,
FrameReceivedEvent.Handler {
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<JSONValue> frameEncoder;
private final FeatureMap clientFeatureMap;
private final SecurityMap clientSecurityMap;
private final FlexInjector injector;
private final ServerEventMap serverEventMap;
private final EventFromJsonCreator creator;
private final Stats stats;
/**
* 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 class PendingCall<T> implements
AsyncCallback<RpcResponse<JSONValue>> {
private final AsyncCallback<T> callback;
private RpcRequest request;
private PendingCallStatus status;
private final FrameEncoder<JSONValue> encoder;
private final Class<?>[] returnValueType;
public PendingCall(AsyncCallback<T> callback, RpcRequest request,
FrameEncoder<JSONValue> 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;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(request.getMethod());
sb.append("[");
sb.append(request.getId());
sb.append("]");
return sb.toString();
}
public void setRequest(RpcRequest request) {
this.request = request;
}
@Override
public void onFailure(Throwable caught) {
stats.sendStats(RpcDispatcher.class, this, "end");
if (callback != null) {
callback.onFailure(caught);
}
}
@Override
public void onSuccess(RpcResponse<JSONValue> response) {
stats.sendStats(RpcDispatcher.class, this, "callback");
T result = null;
try {
result = encoder.validate(response.getResult(), result,
returnValueType);
} catch (UnableToDeserialize e) {
callback.onFailure(e);
return;
}
if (callback != null) {
callback.onSuccess(result);
}
stats.sendStats(RpcDispatcher.class, this, "end");
}
}
@Inject
public RpcDispatcher(IdCreator<Integer> idCreator, WebSocket socket,
FrameEncoder<JSONValue> encoder, SecurityMap clientSecurityMap,
FeatureMap clientFeatureMap, FlexInjector injector,
ServerEventMap serverEventMap, EventFromJsonCreator creator,
Stats stats) {
this.idCreator = idCreator;
this.socket = socket;
this.frameEncoder = encoder;
this.clientSecurityMap = clientSecurityMap;
this.clientFeatureMap = clientFeatureMap;
this.injector = injector;
this.serverEventMap = serverEventMap;
this.creator = creator;
this.stats = stats;
authenticationMethods.put(RemoteServer.class, null);
socket.addServerConnectHandler(this);
socket.addServerDisconnectHandler(this);
socket.addFrameReceivedHandler(this);
socket.addFrameSentHandler(this);
}
public <T> void execute(Request<T> orequest) {
String featureName = clientFeatureMap.getKeyFromClass(orequest
.getServerinterface());
if (serverFeatures != null && !serverFeatures.contains(featureName)) {
orequest
.onFailure(new Exception("Feature not supported on server"));
return;
}
if (featureName == null) {
GWT.log("RPC-could not map class to feature: "
+ orequest.getServerinterface().getName());
}
RpcRequest request = new RpcRequest(featureName, orequest.getMethod(),
orequest.getParams());
// TODO: Delay Creation of id
request.setId(idCreator.getNextId());
final PendingCall<T> pendingCall = new PendingCall<T>(orequest
.getCallback(), request, frameEncoder, orequest
.getReturnValueType());
pending.put(request.getId(), pendingCall);
stats.sendStats(this.getClass(), pendingCall, "begin");
if (authenticationMethods.containsKey(orequest.getServerinterface())) {
SecurityMethod securityMethod = authenticationMethods.get(orequest
.getServerinterface());
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<RpcRequest>() {
@Override
public void onFailure(Throwable caught) {
GWT
.log(
"RPC-Security could not sign request - aborting call",
caught);
}
@Override
public void onSuccess(RpcRequest result) {
pendingCall.setRequest(result);
transmit(pendingCall);
}
});
}
protected void transmit(PendingCall<?> pendingCall) {
pendingCall.setStatus(PendingCallStatus.WAITING_FOR_TRANSMIT);
stats.sendStats(RpcDispatcher.class, pendingCall, "transmit");
txQueue.add(pendingCall.request);
sendQueue();
}
protected void sendQueue() {
if (txQueue.isEmpty())
return;
if (socket.isConnected()) {
if (!socket.isTxBusy()) {
try {
StringBuilder request = new StringBuilder();
frameEncoder.encode(txQueue, request);
socket.send(request.toString());
} catch (UnableToSerialize e) {
e.printStackTrace();
return;
}
for (RpcRequest request : txQueue) {
pending.get(request.getId()).setStatus(
PendingCallStatus.WAITING_FOR_RESPONSE);
}
txQueue.clear();
}
} else {
socket.connect(GWT.getHostPageBaseURL() + "websocket", "kk-entity");
}
}
List<RpcRequest> txQueue = new ArrayList<RpcRequest>();
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);
sendCallsWithFeature(feature);
} 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 sendCallsWithFeature(Class<? extends RemoteService> feature) {
String featureName = clientFeatureMap.getKeyFromClass(feature);
for (PendingCall<?> call : pending.values()) {
if (call.getStatus() == PendingCallStatus.NEW
&& call.request.getFeatureName().equals(featureName)) {
transmit(call);
}
}
}
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 : new ArrayList<PendingCall<?>>(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());
}
for (Entry<String, String> entry : result.entrySet()) {
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(new Request<Map<String, String>>(x, new Class<?>[] { Map.class,
String.class, String.class },
com.kk_electronic.kkportal.core.rpc.RemoteServer.class, "getSecurityMap")
);
}
@Override
public void onServerConnect(ServerConnectEvent event) {
sendQueue();
updateServerSecurityMap();
}
@Override
public void onServerDisconnect(ServerDisconnectEvent event) {
}
@SuppressWarnings("unchecked")
@Override
public void onFrameReceived(FrameReceivedEvent event) {
List<RpcEnvelope> responses = null;
try {
JSONValue object = frameEncoder.decode(event.getData());
responses = frameEncoder.validate(object, responses,
new Class<?>[] { List.class, RpcResponse.class });
} catch (UnableToDeserialize e) {
GWT.log("RpcDispatcher could not get responses", e);
return;
}
for (RpcEnvelope envelope : responses) {
if (envelope instanceof RpcResponse<?>) {
final RpcResponse response = (RpcResponse) envelope;
final PendingCall<?> pendingCall = pending.remove(response
.getId());
Scheduler.get().scheduleDeferred(
new Scheduler.ScheduledCommand() {
@Override
public void execute() {
pendingCall.onSuccess(response);
}
});
continue;
}
if (envelope instanceof RpcError) {
final RpcError response = (RpcError) envelope;
if (response.getCode() != -31301) {
final PendingCall<?> pendingCall = pending.remove(response
.getId());
Scheduler.get().scheduleDeferred(
new Scheduler.ScheduledCommand() {
@Override
public void execute() {
pendingCall.onFailure(response);
}
});
} else {
PendingCall<?> pendingCall = pending.get(response.getId());
Class<? extends RemoteService> clazz = clientFeatureMap
.getClassFromKey(pendingCall.request
.getFeatureName());
SecurityMethod securityMethod = authenticationMethods
.get(clazz);
securityMethod.invalid();
signAndTransmit(pendingCall, securityMethod);
}
continue;
}
if (envelope instanceof RpcRequest) {
RpcRequest request = (RpcRequest) envelope;
fireServerEvent(request);
continue;
}
}
}
private void fireServerEvent(RpcRequest request) {
Class<? extends ServerEvent> clazz = serverEventMap
.getClassFromKey(request.getMethod());
if (clazz == null) {
GWT.log("SERVEREVENT-Unknown event " + request.getMethod());
}
creator.create(clazz, request.getParams(),
new AsyncCallback<ServerEvent>() {
@Override
public void onFailure(Throwable caught) {
GWT
.log(
"SERVEREVENT-Failure during creation of serverevent",
caught);
}
@Override
public void onSuccess(ServerEvent result) {
GWT.log("SERVEREVENT-Created");
}
});
}
@Override
public void onFrameSent(FrameSentEvent frameSentEvent) {
Scheduler.get().scheduleDeferred(new Command() {
@Override
public void execute() {
sendQueue();
}
});
}
public WebSocket getSocket() {
return socket;
}
}