package ch.unifr.pai.twice.comm.serverPush.client;
/*
* Copyright 2013 Oliver Schmid
* 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.
*/
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import ch.unifr.pai.twice.authentication.client.Authentication;
import ch.unifr.pai.twice.authentication.client.security.MessagingException;
import ch.unifr.pai.twice.authentication.client.security.TWICESecurityManager;
import ch.unifr.pai.twice.comm.clientServerTime.client.ClientServerTimeOffset;
import ch.unifr.pai.twice.utils.device.client.UUID;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.web.bindery.event.shared.Event;
/**
* The fundamental functionality of the remote eventing mechanism
*
* @author Oliver Schmid
*
*/
public abstract class RemoteEventing {
/**
* Delay the of the event delivery in milliseconds. The bigger the value the less probable is a conflict between events, but the bigger is the latency
* between firing and executing the event.
*/
private int eventDeliveryDelay;
/**
* Non-blocking events are executed locally (the execution doesn't have to wait for the event to come back from the server. Since this evicts the network
* latency, those events will be executed very fast. If they are in conflict with remote events, they will regularly be rolled back. To prevent this, a
* delay can be introduced to reduce that effect. If this value is smaller than the eventDeliveryDelay, the latter will be applied.
*/
private int localEventDeliveryDelay;
/**
* The estimated server time offset - used to normalize the event time stamp to establish a common time base for all leaf nodes
*/
private Long serverTimeOffset;
private final Stack<UndoableRemoteEvent<?>> eventHistory = new Stack<UndoableRemoteEvent<?>>();
private final Map<String, Long> discardedEventsHistory = new HashMap<String, Long>();
/**
* Returns the estimated server time for the given timestamp of the local system clock
*
* @param timestamp
* - the local system clock timestamp or null for "now"
* @return the corresponding, estimated timestamp of the server side clock
*/
public long getEstimatedServerTime(Long timestamp) {
if (timestamp == null)
timestamp = new Date().getTime();
if (serverTimeOffset != null) {
return timestamp + serverTimeOffset;
}
return timestamp;
}
public RemoteEventing() {
updateServerTimeOffset();
}
/**
* Calculate the server time offset
*/
private void updateServerTimeOffset() {
ClientServerTimeOffset.getServerTimeOffset(new AsyncCallback<Long>() {
@Override
public void onSuccess(Long result) {
serverTimeOffset = result;
}
@Override
public void onFailure(Throwable caught) {
Window.alert("Was not able to synchronize the clock with the server");
}
});
}
/**
* Fires the event to the distributed eventing mechanism if the event is an instance of {@link RemoteEvent} This includes the serialization and the
* encryption of the message as well as the enrichment with data such as origin client identifier and user name
*
* @param event
* @param source
*/
public void fireEventFromSource(Event<?> event, Object source) {
if (source instanceof RemoteWidget) {
source = ((RemoteWidget) source).getEventSource();
}
if (event instanceof RemoteEvent) {
RemoteEvent<?> remoteEvent = ((RemoteEvent<?>) event);
remoteEvent.setTimestamp(getEstimatedServerTime(((RemoteEvent<?>) event).getTimestamp()));
remoteEvent.setOriginatingDevice(UUID.get());
remoteEvent.setUserName(Authentication.getUserName());
if (source != null)
remoteEvent.setSourceObject(source.toString());
GWT.log("Send message");
try {
sendMessage(remoteEvent.serialize(getSecurityManager()));
}
catch (MessagingException e) {
e.printStackTrace();
}
// atmosphereClient.broadcast(remoteEvent.serialize());
if (!remoteEvent.isBlocking())
fireEventInOrder(remoteEvent, source, true);
}
else {
fireEventInOrder(event, source, true);
}
}
/**
* @return the {@link TWICESecurityManager} to be used for the decryption and encryption of messages
*/
protected abstract TWICESecurityManager getSecurityManager();
/**
* Send the message to the distributed eventing mechanism
*
* @param message
*/
protected abstract void sendMessage(String message);
/**
* fire the event within the local event bus
*
* @param e
*/
protected abstract void fireEventLocally(Event<?> e);
/**
* fire the event within the local event bus with the given source
*
* @param e
* @param source
*/
protected abstract void fireEventFromSourceLocally(Event<?> e, Object source);
/**
* Fire the event in their correct order of appearance. If necessary also handle conflicts
*
* @param event
* @param source
* @param localEvent
*/
public void fireEventInOrder(Event<?> event, Object source, boolean localEvent) {
if (event instanceof RemoteEvent) {
RemoteEvent<?> remoteEvent = (RemoteEvent<?>) event;
if (eventHistory != null) {
Long timestamp = remoteEvent.getTimestamp();
Stack<UndoableRemoteEvent<?>> undidEvents = new Stack<UndoableRemoteEvent<?>>();
UndoableRemoteEvent<?> e;
if (timestamp != null) {
// Undo all events that should have been executed after this
// event
while (eventHistory.size() > 0) {
e = eventHistory.pop();
if (e.getTimestamp() == null || e.getTimestamp().longValue() <= timestamp.longValue()) {
eventHistory.push(e);
break;
}
else {
e.setUndo(true);
// Fire events directly - there is no need for an
// additional delay
fireEventLocally(e);
e.setUndo(false);
undidEvents.push(e);
}
}
// Fire this event
if (event instanceof DiscardingRemoteEvent) {
// If the event is a discarding event, it has only to be
// fired if no later event with the same identifier has
// been fired already
DiscardingRemoteEvent<?> discardingRemoteEvent = ((DiscardingRemoteEvent<?>) event);
if (discardingRemoteEvent.getInstanceId() != null) {
Long lastTimeStamp = discardedEventsHistory.get(discardingRemoteEvent.getInstanceId());
Long eventTimeStamp = discardingRemoteEvent.getTimestamp();
if (lastTimeStamp == null || (eventTimeStamp != null && lastTimeStamp.longValue() < eventTimeStamp.longValue())) {
discardedEventsHistory.put(discardingRemoteEvent.getInstanceId(), discardingRemoteEvent.getTimestamp());
fireEventToTheLocalBusWithDelay(event, source, localEvent);
}
}
else {
// If the discarding event has no identifier, it
// will be fired.
fireEventToTheLocalBusWithDelay(event, source, localEvent);
}
}
else {
fireEventToTheLocalBusWithDelay(event, source, localEvent);
// If the event is blocking, it is ensured that there
// will not be another event with a timestamp before the
// one of the remote event anymore (everything else
// would be an error). Therefore, the undoable events
// until that point in time can be skipped.
if (remoteEvent.isBlocking())
eventHistory.clear();
}
// Re-fire all events that have previously been undone
if (event instanceof UndoableRemoteEvent)
eventHistory.push((UndoableRemoteEvent<?>) event);
for (UndoableRemoteEvent<?> undidEvent : undidEvents) {
fireEventToTheLocalBusWithDelay(undidEvent, source, localEvent);
eventHistory.push(undidEvent);
}
}
}
}
}
/**
* Fires the event to the local event bus after a specific delay
*
* @param event
* @param source
* @param localEvent
*/
private void fireEventToTheLocalBusWithDelay(final Event<?> event, final Object source, boolean localEvent) {
if (event instanceof RemoteEvent) {
if (((RemoteEvent<?>) event).isFireLocally()) {
int delay = localEvent ? Math.max(localEventDeliveryDelay, eventDeliveryDelay) : eventDeliveryDelay;
if (delay == 0) {
if (source == null)
fireEventLocally(event);
else
fireEventFromSourceLocally(event, source);
}
else {
Timer t = new Timer() {
@Override
public void run() {
if (source == null)
fireEventLocally(event);
else
fireEventFromSourceLocally(event, source);
}
};
t.schedule(delay);
}
}
}
}
}