package org.swellrt.model.js; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArrayMixed; import com.google.gwt.core.client.JsArrayString; import org.swellrt.model.generic.BooleanType; import org.swellrt.model.generic.ListType; import org.swellrt.model.generic.MapType; import org.swellrt.model.generic.Model; import org.swellrt.model.generic.NumberType; import org.swellrt.model.generic.StringType; import org.swellrt.model.generic.Type; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.Set; /** * * Adapt SwellRT objects from/to JavaScript proxy objects. * * @author pablojan@gmail.com (Pablo Ojanguren) * */ public class ProxyAdapter { /** * Converts a Java iterable of strings to a Javascript array. * * @param strings@org.swellrt.model.js.ProxyAdapter * @return */ private static JsArrayString iterableToArray(Iterable<String> strings) { JsArrayString array = (JsArrayString) JavaScriptObject.createArray(); for (String s : strings) array.push(s); return array; } /** * Converts a Java set of ParticipantId objects to a Javascript array of * strings. * * @param participants * @return */ private static JsArrayString participantsToArray(Set<ParticipantId> participants) { JsArrayString array = (JsArrayString) JavaScriptObject.createArray(); for (ParticipantId p : participants) array.push(p.getAddress()); return array; } private native static boolean isJsArray(JavaScriptObject jso) /*-{ return jso != null && (Array.isArray(jso)); }-*/; private native static boolean isJsObject(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "object")); }-*/; private native static boolean isJsNumber(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "number")); }-*/; private native static boolean isJsBoolean(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "boolean")); }-*/; private native static boolean isJsString(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "string")); }-*/; private native static boolean isJsFile(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "object") && (jso.constructor == File)); }-*/; private native static boolean isJsText(JavaScriptObject jso) /*-{ return jso != null && ((typeof jso == "object") && (jso.constructor == Text)); }-*/; private native static boolean isJsFunction(JavaScriptObject jso) /*-{ return jso != null && (typeof jso == "function"); }-*/; private native static String asString(JavaScriptObject jso) /*-{ return ""+jso; }-*/; private native static Integer asInteger(JavaScriptObject value) /*-{ var x; if (isNaN(value)) { return null; } x = parseFloat(value); if ((x | 0) === x) { return value; } return null; }-*/; private native static Double asDouble(JavaScriptObject value) /*-{ return value; }-*/; private native static boolean asBoolean(JavaScriptObject value) /*-{ return value; }-*/; private native static JsArrayMixed asArray(JavaScriptObject jso) /*-{ return jso; }-*/; private native static void log(String m, JavaScriptObject obj) /*-{ if (!$wnd._traces) { $wnd._traces = new Array(); } $wnd._traces.push({ trace: m, data: obj }); console.log(m); }-*/; private final Model model; public ProxyAdapter(Model model) { this.model = model; } /** * Generate a {@link Type} instance for a Javascript value. The new object is * NOT attached to a collaborative object. * * @param value * @return */ protected Type fromJs(JavaScriptObject value) { Type t = null; if (isJsNumber(value)) { // Using the string representation of the number to avoid // issues converting JS number to Java number with toString() methods t = model.createNumber(asString(value)); } else if (isJsString(value)) { t = model.createString(asString(value)); } else if (isJsBoolean(value)) { t = model.createBoolean(asString(value)); } else if (isJsArray(value)) { t = model.createList(); } else if (isJsText(value)) { t = model.createText(); } else if (isJsFile(value)) { t = model.createList(); } else if (isJsObject(value)) { t = model.createMap(); } return t; } /** * Populate the content of a native Javascript object into its counterpart of * the collaborative object. Hence, types of both 'tObject' and 'jsObject' * arguments must be similar. * * If they are primitive values, values are not populated. * * * @param tObject * @param jsObject * @return */ protected boolean populateValues(Type tObject, JavaScriptObject jsObject) { if (isJsNumber(jsObject)) { // Nothing to do return true; } else if (isJsString(jsObject)) { // Nothing to do return true; } else if (isJsBoolean(jsObject)) { // Nothing to do return true; } else if (isJsArray(jsObject)) { JsArrayMixed jsArray = asArray(jsObject); for (int i = 0; i < jsArray.length(); i++) { if (!add((ListType) tObject, jsArray.getObject(i))) { return false; } } } else if (isJsText(jsObject)) { // TODO add support for Text objects return true; } else if (isJsFile(jsObject)) { // TODO add support for File objects return true; } else if (isJsObject(jsObject)) { JsMap jsMap = JsMap.of(jsObject); JsArrayString keys = jsMap.keys(); for (int i = 0; i < keys.length(); i++) { String key = keys.get(i); if (!put((MapType) tObject, key, jsMap.get(key))) { return false; } } } return false; } /** * Put a Javascript object into a {@MapType} instance. This can * trigger a recursive process to attach a new subtree of Javascript objects * into the collaborative object. * * @param map * @param key * @param value */ protected boolean put(MapType map, String key, JavaScriptObject value) { Type tvalue = fromJs(value); if (tvalue == null) return false; tvalue = map.put(key, tvalue); return populateValues(tvalue, value); } /** * Add a Javascript object into a {@ListType} instance in the * specified index. This can trigger a recursive process to attach a new * subtree of Javascript objects into the collaborative object. * * Collaborative list semantics differs from javascript's, provided index must * be in the bounds of the list. * * @param list * @param value * @return */ protected boolean add(ListType list, int index, JavaScriptObject value) { Type tvalue = fromJs(value); if (tvalue == null) return false; tvalue = list.add(index, tvalue); return populateValues(tvalue, value); } /** * Add a Javascript object into a {@ListType} instance. This can * trigger a recursive process to attach a new subtree of Javascript objects * into the collaborative object. * * @param list * @param value * @return */ protected boolean add(ListType list, JavaScriptObject value) { Type tvalue = fromJs(value); if (tvalue == null) return false; tvalue = list.add(tvalue); return populateValues(tvalue, value); } /** * Sets an event handler in a node of the collaborative object. Handler must * be a function. * * @param handler * @param target * @return the listener */ protected ProxyListener setListener(Type target, JavaScriptObject handler) { if (!isJsFunction(handler)) return null; if (target instanceof MapType) { ProxyMapListener listener = ProxyMapListener.create(handler); listener.setAdapter(this); ((MapType) target).addListener(listener); return listener; } else if (target instanceof ListType) { ProxyListListener listener = ProxyListListener.create(handler); listener.setAdapter(this); ((ListType) target).addListener(listener); return listener; } else if (target instanceof StringType) { ProxyPrimitiveListener listener = ProxyPrimitiveListener.create(handler); listener.setAdapter(this); ((StringType) target).addListener(listener); return listener; } return null; } /** * Remove an event handler from a node of the collaborative object. * * TODO verify that this implementation works * * @param target * @param handler */ protected boolean removeListener(Type target, ProxyListener handler) { if (handler instanceof ProxyMapListener) { ((MapType) target).removeListener((ProxyMapListener) handler); } else if (handler instanceof ProxyListListener) { ((ListType) target).removeListener((ProxyListListener) handler); } else if (handler instanceof ProxyPrimitiveListener) { ((StringType) target).removeListener((ProxyPrimitiveListener) handler); } return true; } /** * Creates a Javascript object proxing a collaborative object. The create * object allows a native JS syntax to work with the collab. object. * * It also provide syntax sugar to inner properties by path: <br> * * "object[listprop.3.field]" is equivalent to "object.listprop[3].field" * * * <br> * but the first expression is more efficient. * * @param delegate * @param root * @return */ public native JavaScriptObject getJSObject(Model delegate, MapType root) /*-{ var _this = this; var target = this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(root); target._object = delegate; var proxy = new $wnd.Proxy(target, { get: function(target, propKey) { if (typeof propKey == "string" && propKey.indexOf(".") > 0) { var value = target._object.@org.swellrt.model.generic.Model::fromPath(Ljava/lang/String;)(propKey); if (value) { return _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(value); } } else if (propKey == "addCollaborator") { return function(c) { target._object.@org.swellrt.model.generic.Model::addParticipant(Ljava/lang/String;)(c); }; } else if (propKey == "removeCollaborator") { return function(c) { target._object.@org.swellrt.model.generic.Model::removeParticipant(Ljava/lang/String;)(c); }; } else if (propKey == "collaborators") { var collaboratorSet = target._object.@org.swellrt.model.generic.Model::getParticipants()(); return @org.swellrt.model.js.ProxyAdapter::participantsToArray(Ljava/util/Set;)(collaboratorSet); } else if (propKey == "_nodes") { // // For debug purposes: list of Wavelet documents storing collaborative object's nodes // var parts = target._object.@org.swellrt.model.generic.Model::getModelDocuments()(); return @org.swellrt.model.js.ProxyAdapter::iterableToArray(Ljava/lang/Iterable;)(parts); } else if (propKey == "_node") { // // For debug purposes: return a Wavelet document storing a collaborative object's node // return function(node) { return target._object.@org.swellrt.model.generic.Model::getModelDocument(Ljava/lang/String;)(node); }; } else if (propKey == "_oid") { // TODO make _id property read-only return target._object.@org.swellrt.model.generic.Model::getId()(); } else if (propKey == "_nodeDebug") { return function(node, debug) { return target._object.@org.swellrt.model.generic.Model::debugDocumentEvents(Ljava/lang/String;Z)(node, debug); } } else { return target[propKey]; } } }); return proxy; }-*/; public JavaScriptObject of(Type delegate) { if (delegate instanceof MapType) return ofMap((MapType) delegate); if (delegate instanceof ListType) return ofList((ListType) delegate); if (delegate instanceof StringType) return ofString((StringType) delegate); if (delegate instanceof NumberType) return ofNumber((NumberType) delegate); if (delegate instanceof BooleanType) return ofBoolean((BooleanType) delegate); return null; } /** * Generate a JavaScript proxy object for an underlying collaborative map * * @param delegate * @return */ public native JavaScriptObject ofMap(MapType delegate) /*-{ var _this = this; var proxy = new $wnd.Proxy( { _delegate: delegate }, { get: function(target, propKey, receiver) { if (propKey == 'addListener' || propKey == 'on' || propKey == 'onEvent') { return function(listener, property) { if (!listener) return false; var eventTarget = target._delegate; if (property != null && typeof property == 'string') { eventTarget = target._delegate.@org.swellrt.model.generic.MapType::get(Ljava/lang/String;)(propKey); if (!eventTarget) return false; } var proxyListener = _this.@org.swellrt.model.js.ProxyAdapter::setListener(Lorg/swellrt/model/generic/Type;Lcom/google/gwt/core/client/JavaScriptObject;)(eventTarget, listener); // Return an object which can remove the listener return { dispose: function() { _this.@org.swellrt.model.js.ProxyAdapter::removeListener(Lorg/swellrt/model/generic/Type;Lorg/swellrt/model/js/ProxyListener;)(eventTarget, proxyListener); } }; }; } else if (propKey == '_object') { // bypass the _object property return target[propKey]; } else { var value = target._delegate.@org.swellrt.model.generic.MapType::get(Ljava/lang/String;)(propKey); if (!value) return undefined; var proxy = _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(value); return proxy; } }, has: function(target, propKey) { return target._delegate.@org.swellrt.model.generic.MapType::hasKey(Ljava/lang/String;)(propKey); }, ownKeys: function(target) { var nativeKeys = target._delegate.@org.swellrt.model.generic.MapType::keySet()(); var keys = @org.swellrt.model.js.ProxyAdapter::iterableToArray(Ljava/lang/Iterable;)(nativeKeys); return keys; }, getOwnPropertyDescriptor: function(target, propKey) { var hasPropKey = target._delegate.@org.swellrt.model.generic.MapType::hasKey(Ljava/lang/String;)(propKey); if (hasPropKey) { var descriptor = { value: this.get(target, propKey), writable: true, enumerable: true, configurable: true }; return descriptor; } else { return Reflect.getOwnPropertyDescriptor(target, propKey); } }, set: function(target, propKey, value, receiver) { // bypass a special property _object if (propKey == '_object') { return target[propKey] = value; } return _this.@org.swellrt.model.js.ProxyAdapter::put(Lorg/swellrt/model/generic/MapType;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(target._delegate, propKey, value); }, defineProperty: function(target, propKey, propDesc) { var value = propDesc.value; if (!value) return false; return _this.@org.swellrt.model.js.ProxyAdapter::put(Lorg/swellrt/model/generic/MapType;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(target._delegate, propKey, value); }, deleteProperty: function(target, propKey) { target._delegate.@org.swellrt.model.generic.MapType::remove(Ljava/lang/String;)(propKey); var hasPropKey = target._delegate.@org.swellrt.model.generic.MapType::hasKey(Ljava/lang/String;)(propKey); return !hasPropKey; } }); return proxy; }-*/; public native JavaScriptObject ofList(ListType delegate) /*-{ var _this = this; var _array = new Array(); _array._delegate = delegate; var proxy = new $wnd.Proxy(_array,{ get: function(target, propKey, receiver) { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var index = Number(propKey); if (index >=0 && index < length) { // // get // var value = target._delegate.@org.swellrt.model.generic.ListType::get(I)(index); if (!value) { return undefined; } else { return _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(value); } } else if (propKey == "length") { // // length // return target._delegate.@org.swellrt.model.generic.ListType::size()(); } else if (propKey == "push") { // // push // return function() { for(var i in arguments) { _this.@org.swellrt.model.js.ProxyAdapter::add(Lorg/swellrt/model/generic/ListType;Lcom/google/gwt/core/client/JavaScriptObject;)(target._delegate, arguments[i]); } return target._delegate.@org.swellrt.model.generic.ListType::size()(); } } else if (propKey == "pop") { // // pop // return function() { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); if (length > 0) { var value = target._delegate.@org.swellrt.model.generic.ListType::get(I)(length-1); var proxy = null; if (!value) { return undefined; } else { proxy = _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(value); } target._delegate.@org.swellrt.model.generic.ListType::remove(I)(length-1); return proxy; } } } else if (propKey == 'addListener' || propKey == 'on' || propKey == 'onEvent') { return function(listener, index) { if (!listener) return false; var eventTarget = target._delegate; index = Number(index); if (index >=0 && index < length) eventTarget = target._delegate.@org.swellrt.model.generic.ListType::get(I)(index); if (!eventTarget) return false; var proxyListener = _this.@org.swellrt.model.js.ProxyAdapter::setListener(Lorg/swellrt/model/generic/Type;Lcom/google/gwt/core/client/JavaScriptObject;)(eventTarget, listener); // Return an object which can remove the listener return { dispose: function() { _this.@org.swellrt.model.js.ProxyAdapter::removeListener(Lorg/swellrt/model/generic/Type;Lorg/swellrt/model/js/ProxyListener;)(eventTarget, proxyListener); } }; }; } }, set: function(target, propKey, value, receiver) { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var index = Number(propKey); // Should check here index out of bounds? // Collaborative list doesn't support inserting out of bounds if (index >=0 && index <= length) { if (value === undefined || value === null) { var deletedValue = target._delegate.@org.swellrt.model.generic.ListType::remove(I)(index); if (deletedValue) return _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(deletedValue); else return false; } else { return _this.@org.swellrt.model.js.ProxyAdapter::add(Lorg/swellrt/model/generic/ListType;ILcom/google/gwt/core/client/JavaScriptObject;)(target._delegate, propKey, value); } } else { // Should reflect non array properties set? return Reflect.set(target, propKey, value); } }, has: function(target, propKey) { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var index = Number(propKey); if (index >=0 && index < length) return true; else if (propKey === 'length') return true; else Reflect.has(target, propKey); }, ownKeys: function(target) { // keys is just a contiguos list of indexes, because collaborative lists doesn't allow gaps on indexes var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var keys = new Array(); for (var i = 0; i < length; i++) keys.push(""+i); keys.push("length"); return keys; }, getOwnPropertyDescriptor: function(target, propKey) { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var index = Number(propKey); if (index >=0 && index < length) { var descriptor = { value: this.get(target, propKey), writable: true, enumerable: true, configurable: true }; return descriptor; } else if (propKey == 'length') { return { value: this.get(target, 'length'), writable: true, enumerable: false, configurable: false }; } else { return Reflect.getOwnPropertyDescriptor(target, propKey); } }, deleteProperty: function(target, propKey) { var length = target._delegate.@org.swellrt.model.generic.ListType::size()(); var index = Number(propKey); if (index >=0 && index < length) { var deletedValue = target._delegate.@org.swellrt.model.generic.ListType::remove(I)(index); if (deletedValue) return _this.@org.swellrt.model.js.ProxyAdapter::of(Lorg/swellrt/model/generic/Type;)(deletedValue); else return false; } else { return Reflect.deleteProperty(target, propKey); } } }); return proxy; }-*/; public native JavaScriptObject ofString(StringType delegate) /*-{ return delegate.@org.swellrt.model.generic.StringType::getValue()(); }-*/; public native JavaScriptObject ofNumber(NumberType delegate) /*-{ var value = delegate.@org.swellrt.model.generic.NumberType::getValueDouble()(); if (value != null) { return value.@java.lang.Double::doubleValue()(); } return null; }-*/; public native JavaScriptObject ofBoolean(BooleanType delegate) /*-{ return delegate.@org.swellrt.model.generic.BooleanType::getValue()(); }-*/; public final native JavaScriptObject ofParticipant(ParticipantId participant) /*-{ return participant.@org.waveprotocol.wave.model.wave.ParticipantId::getAddress()() }-*/; public final native JavaScriptObject ofPrimitive(String value) /*-{ return value; }-*/; }