![JAR search and dependency download from the Maven repository](/logo.png)
com.goodow.realtime.Document Maven / Gradle / Ivy
/*
* Copyright 2012 Goodow.com
*
* 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.goodow.realtime;
import com.goodow.realtime.channel.constant.Constants.Params;
import com.goodow.realtime.channel.util.ChannelNative;
import com.goodow.realtime.model.util.ModelFactory;
import com.goodow.realtime.operation.util.Pair;
import org.timepedia.exporter.client.Export;
import org.timepedia.exporter.client.ExportPackage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import elemental.json.JsonObject;
/**
* A Realtime document. A document consists of a Realtime model and a set of collaborators. Listen
* on the document for the following events:
*
* -
*
* {@link com.goodow.realtime.EventType#COLLABORATOR_LEFT}
*
-
*
* {@link com.goodow.realtime.EventType#COLLABORATOR_JOINED}
*
-
*
* {@link com.goodow.realtime.EventType#DOCUMENT_SAVE_STATE_CHANGED}
*
*
* This class should not be instantiated directly. The document object is generated during the
* document load process.
*/
@ExportPackage(ModelFactory.PACKAGE_PREFIX_REALTIME)
@Export(all = true)
public class Document implements EventTarget {
static final String EVENT_HANDLER_KEY = "document";
private static final Logger log = Logger.getLogger(Document.class.getName());
private final List collaborators = new ArrayList();
private Model model;
private Map, List>> handlers;
private boolean isEventsScheduled = false;
private List, Disposable>> events;
private Map> eventsById;
private final Runnable eventsTask = new Runnable() {
private List, Disposable>> evts;
private Map> evtsById;
@Override
public void run() {
evts = events;
evtsById = eventsById;
events = null;
eventsById = null;
isEventsScheduled = false;
for (int i = 0, len = evts.size(); i < len; i++) {
Pair, Disposable> evt = evts.get(i);
produceObjectChangedEvent(evt.first.first, evt.second);
}
for (Pair, Disposable> evt : evts) {
fireEvent(evt.first, evt.second);
}
assert evtsById.isEmpty();
evts = null;
evtsById = null;
}
private void bubblingToAncestors(String id, ObjectChangedEvent objectChangedEvent,
Set seen) {
if (seen.contains(id)) {
return;
}
seen.add(id);
evts.add(Pair.of(Pair.of(id, objectChangedEvent.type), (Disposable) objectChangedEvent));
String[] parents = model.getParents(id);
if (parents != null) {
for (String parent : parents) {
bubblingToAncestors(parent, objectChangedEvent, seen);
}
}
}
@SuppressWarnings({"unchecked", "cast"})
private void fireEvent(Pair key, Disposable event) {
List> handlers = getEventHandlers(key, false);
if (handlers == null) {
return;
}
for (int i = 0, len = handlers.size(); i < len; i++) {
EventHandler> handler = handlers.get(i);
if (handler instanceof EventHandler) {
((EventHandler) handler).handleEvent(event);
} else {
__ocniFireEvent__(handler, event);
}
}
}
private void produceObjectChangedEvent(String id, Disposable event) {
if (!evtsById.containsKey(id)) {
return;
}
BaseModelEvent evt = (BaseModelEvent) event;
assert !evt.bubbles;
List eventsPerId = evtsById.get(id);
evtsById.remove(id);
ObjectChangedEvent objectChangedEvent =
new ObjectChangedEvent(evt.target, evt.sessionId, evt.userId, eventsPerId
.toArray(new BaseModelEvent[0]));
Set seen = new HashSet();
bubblingToAncestors(id, objectChangedEvent, seen);
}
};
private DocumentBridge bridge;
/**
* @param bridge The driver for the GWT collaborative libraries.
* @param commService The communication service to dispose when this document is disposed.
* @param errorHandlerFn The third-party error handling function.
*/
Document(DocumentBridge bridge, Disposable commService, ErrorHandler errorHandlerFn) {
this.bridge = bridge;
model = new Model(bridge, this);
}
public void addCollaboratorJoinedListener(EventHandler handler) {
addEventListener(EventType.COLLABORATOR_JOINED, handler, false);
}
public void addCollaboratorLeftListener(EventHandler handler) {
addEventListener(EventType.COLLABORATOR_LEFT, handler, false);
}
public void addDocumentSaveStateListener(EventHandler handler) {
addEventListener(EventType.DOCUMENT_SAVE_STATE_CHANGED, handler, false);
}
@Override
public void addEventListener(EventType type, EventHandler> handler, boolean opt_capture) {
addEventListener(EVENT_HANDLER_KEY, type, handler, opt_capture);
}
/**
* Closes the document and disconnects from the server. After this function is called, event
* listeners will no longer fire and attempts to access the document, model, or model objects will
* throw a {@link com.goodow.realtime.DocumentClosedError}. Calling this function after the
* document has been closed will have no effect.
*
* @throws DocumentClosedError
*/
public void close() {
bridge.outputSink.close();
bridge = null;
model = null;
}
/**
* Gets an array of collaborators active in this session. Each collaborator is a jsMap with these
* fields: sessionId, userId, displayName, color, isMe, isAnonymous.
*
* @return An array of collaborators.
*/
public Collaborator[] getCollaborators() {
return collaborators.toArray(new Collaborator[0]);
}
/**
* Gets the collaborative model associated with this document.
*
* @return The collaborative model for this document.
*/
public Model getModel() {
return model;
}
public void removeCollaboratorJoinedListener(EventHandler handler) {
removeEventListener(EventType.COLLABORATOR_JOINED, handler, false);
}
public void removeCollaboratorLeftListener(EventHandler handler) {
removeEventListener(EventType.COLLABORATOR_LEFT, handler, false);
}
@Override
public void removeEventListener(EventType type, EventHandler> handler, boolean opt_capture) {
removeEventListener(EVENT_HANDLER_KEY, type, handler, opt_capture);
}
void addEventListener(String id, EventType type, EventHandler> handler, boolean opt_capture) {
if (id == null || type == null || handler == null) {
throw new NullPointerException((id == null ? "id" : type == null ? "type" : "handler")
+ " was null.");
}
List> handlersPerType = getEventHandlers(Pair.of(id, type), true);
if (handlersPerType.contains(handler)) {
log.warning("The same handler can only be added once per the type.");
} else {
handlersPerType.add(handler);
}
}
void checkStatus() throws DocumentClosedError {
if (bridge == null) {
throw new DocumentClosedError();
}
}
void onCollaboratorChanged(boolean isJoined, JsonObject json) {
Collaborator collaborator =
new Collaborator(json.getString(Params.USER_ID), json.getString(Params.SESSION_ID), json
.getString(Params.DISPLAY_NAME), json.getString(Params.COLOR), json
.getBoolean(Params.IS_ME), json.getBoolean(Params.IS_ANONYMOUS), json
.getString(Params.PHOTO_URL));
if (isJoined) {
collaborators.add(collaborator);
CollaboratorJoinedEvent event = new CollaboratorJoinedEvent(this, collaborator);
scheduleEvent(Document.EVENT_HANDLER_KEY, EventType.COLLABORATOR_JOINED, event);
} else {
collaborators.remove(collaborator);
CollaboratorLeftEvent event = new CollaboratorLeftEvent(this, collaborator);
scheduleEvent(Document.EVENT_HANDLER_KEY, EventType.COLLABORATOR_LEFT, event);
}
}
void removeEventListener(String id, EventType type, EventHandler> handler, boolean opt_capture) {
if (handlers == null || handler == null) {
return;
}
List> handlersPerType = handlers.get(Pair.of(id, type));
if (handlersPerType == null) {
return;
}
handlersPerType.remove(handler);
if (handlersPerType.isEmpty()) {
handlers.remove(handlersPerType);
if (handlers.isEmpty()) {
handlers = null;
}
}
}
void scheduleEvent(String id, EventType type, Disposable event) {
if (events == null) {
initializeEvents();
}
events.add(Pair.of(Pair.of(id, type), event));
if (event instanceof BaseModelEvent) {
BaseModelEvent evt = (BaseModelEvent) event;
assert !evt.bubbles;
List eventsPerId = eventsById.get(id);
if (eventsPerId == null) {
eventsPerId = new ArrayList();
eventsById.put(id, eventsPerId);
}
eventsPerId.add(evt);
}
if (!isEventsScheduled) {
isEventsScheduled = true;
ChannelNative.get().scheduleDeferred(eventsTask);
}
}
// @formatter:off
private native void __ocniFireEvent__(Object handler, Object event) /*-[
GDREventBlock block = (GDREventBlock)handler;
block(event);
]-*/ /*-{
}-*/;
// @formatter:on
private List> getEventHandlers(Pair key,
boolean createIfNotExist) {
if (handlers == null) {
if (!createIfNotExist) {
return null;
}
handlers = new HashMap, List>>();
}
List> handlersPerType = handlers.get(key);
if (handlersPerType == null) {
if (!createIfNotExist) {
return null;
}
handlersPerType = new ArrayList>();
handlers.put(key, handlersPerType);
}
return handlersPerType;
}
private void initializeEvents() {
events = new ArrayList, Disposable>>();
eventsById = new HashMap>();
}
}