/* * Copyright (C) 2014 Servoy BV * * 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.servoy.j2db.server.ngclient.property.types; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.mozilla.javascript.Context; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.Scriptable; import org.sablo.BaseWebObject; import org.sablo.specification.PropertyDescription; import org.sablo.specification.property.ChangeAwareMap.IAttachAware; import org.sablo.specification.property.ChangeAwareMap.IAttachHandler; import org.sablo.specification.property.ConvertedMap; import org.sablo.specification.property.IWrappedBaseMapProvider; import org.sablo.specification.property.IWrapperType; import org.sablo.specification.property.WrappingContext; /** * This map is able to act as a Sablo wrap-aware map that is based on a native Rhino JS object value. * It is required when server side JS code does something like this: * <pre> * var x = []; * window.popupMenus = [{ "name": "a", "items": x } // window is a service or component here * x.push(...) * </pre> * * So in this case window.popupMenus when it's assigned can't just rebase all it's nested values on Java maps/lists cause then * changing x in JS will have no effect on the mirrored property value (so one could only do window.popupMenus[0].items.push in order for it to work correctly). * <br/><br/> * We need then the window.popupMenus property to really be based on the javascript underlying NativeObject, not on a copy made on one point in time of it so * that changing previously kept references to subtrees of it will be reflected in Java side as well. * <br><br> * This map behaves a bit strange as it will not keep a map of Wrapped sablo values, but it will keep a NativeObject based conversion map and when a wrapped sablo value * is needed it will offer another converted map that converts from the sablo value (that is converted from Rhino) to wrapped sablo values on the fly. * * @author acostescu * @param <SabloT> the Sablo value type * @param <SabloWT> the Sablo wrapped value type */ //TODO these ET and WT are improper - as for object type they can represent multiple types (a different set for each child key), but they help to avoid some bugs at compile-time public class RhinoNativeObjectWrapperMap<SabloT, SabloWT> extends ConvertedMap<SabloT, Object> implements IWrappedBaseMapProvider, IRhinoNativeProxy, IAttachAware<SabloWT> { protected Map<String, IWrapperType<SabloT, SabloWT>> childPropsThatNeedWrapping; protected Map<String, SabloT> previousValues; protected BaseWebObject componentOrService; protected final PropertyDescription customJSONTypeDefinition; protected final NativeObject rhinoObject; protected ConvertedMap<SabloWT, SabloT> sabloWrappedBaseMap; protected IAttachHandler<SabloWT> attachHandler; public RhinoNativeObjectWrapperMap(NativeObject rhinoObject, PropertyDescription customJSONTypeDefinition, Map<String, SabloT> previousComponentValue, BaseWebObject componentOrService, Map<String, IWrapperType<SabloT, SabloWT>> childPropsThatNeedWrapping) { super(new NativeObjectProxyMap<String, Object>(rhinoObject)); this.childPropsThatNeedWrapping = childPropsThatNeedWrapping; this.previousValues = previousComponentValue != null ? new HashMap<String, SabloT>(previousComponentValue) : new HashMap<String, SabloT>(); this.componentOrService = componentOrService; this.customJSONTypeDefinition = customJSONTypeDefinition; this.rhinoObject = rhinoObject; } public Scriptable getBaseRhinoScriptable() { return rhinoObject; } @Override public void setAttachHandler(IAttachHandler<SabloWT> attachHandler) { this.attachHandler = attachHandler; } @Override protected SabloT convertFromBase(String key, Object value) { SabloT old = previousValues != null ? previousValues.get(key) : null; SabloT v = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(value, old, customJSONTypeDefinition.getProperty(key), componentOrService); // previousComponentValue.get(key) might have been null if native JS Rhino objects/arrays were set there directly // and the parent was also a native object that had previously been already attached to a component (but used via the initial Rhino reference not the Rhino wrapper // so it doesn't trigger anything); in this case getting it now will actually create the ChangeAware instance - which needs to be attached to the component previousValues.put(key, v); if (old != v) { if (attachHandler != null) { attachHandler.detachFromBaseObjectIfNeeded(key, wrap(key, old)); attachHandler.attachToBaseObjectIfNeeded(key, wrap(key, v)); } } return v; } @Override protected Object convertToBase(String key, boolean ignoreOldValue, SabloT value) { return NGConversions.INSTANCE.convertSabloComponentToRhinoValue(value, customJSONTypeDefinition.getProperty(key), componentOrService, rhinoObject); } public Map<String, SabloWT> getWrappedBaseMap() { if (childPropsThatNeedWrapping == null || childPropsThatNeedWrapping.size() == 0) return (Map<String, SabloWT>)this; // sablo type == wrapped type, no wrapping will happen if (sabloWrappedBaseMap == null) { // here in this converted map, the "base" is "sablo type" and the external is the "sablo wrapped type"; the other way around then when storage is java maps or arrays sabloWrappedBaseMap = new ConvertedMap<SabloWT, SabloT>(this) { @Override protected SabloWT convertFromBase(String forKey, SabloT value) { return wrap(forKey, value); } @Override protected SabloT convertToBase(String forKey, boolean ignoreOldValue, SabloWT value) { IWrapperType<SabloT, SabloWT> wt = childPropsThatNeedWrapping.get(forKey); return wt != null ? wt.unwrap(value) : (SabloT)value; } }; } return sabloWrappedBaseMap; } protected SabloWT wrap(String forKey, SabloT value) { IWrapperType<SabloT, SabloWT> wt = childPropsThatNeedWrapping.get(forKey); return wt != null ? wt.wrap(value, null /* we never store the wrapped value here... */, customJSONTypeDefinition.getProperty(forKey), new WrappingContext(componentOrService, forKey)) : (SabloWT)value; } /** * This is a proxy Map to the underlying Rhino NativeObject. It works directly with scriptable values, doesn't take into consideration * Rhino wrap/unwrap operations and NativeObject (that itself implements Map) in it's Map impl. * * K and V generic types are not really useful but they help make code more readable. K can be a String or Number, V can be anything. * * * @author acostescu */ public static class NativeObjectProxyMap<K, V> extends AbstractMap<K, V> { // TODO improve runtime performance of this map private final NativeObject nativeObject; protected NativeObjectProxyMap<K, V>.EntrySet entrySet; public NativeObjectProxyMap(NativeObject nativeObject) { this.nativeObject = nativeObject; } @Override public V put(K key, V value) { Context.enter(); try { if (key instanceof String) { V old = (V)nativeObject.get((String)key, nativeObject); nativeObject.put((String)key, nativeObject, value); return old; } else if (key instanceof Number) { V old = (V)nativeObject.get(((Number)key).intValue(), nativeObject); nativeObject.put(((Number)key).intValue(), nativeObject, value); return old; } throw new RuntimeException("JS Object key must be either string or number"); } finally { Context.exit(); } } @Override public Set<Map.Entry<K, V>> entrySet() { Set<Map.Entry<K, V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } protected final class EntrySet extends AbstractSet<Map.Entry<K, V>> { @Override public Iterator<Map.Entry<K, V>> iterator() { return new Iterator<Map.Entry<K, V>>() { Object[] ids = nativeObject.getIds(); Object key = null; int index = 0; public boolean hasNext() { return index < ids.length; } public Map.Entry<K, V> next() { final Object ekey = key = ids[index++]; final Object value; Context.enter(); try { if (ekey instanceof String) { value = nativeObject.get((String)ekey, nativeObject); } else if (ekey instanceof Number) { value = nativeObject.get(((Number)ekey).intValue(), nativeObject); } else throw new RuntimeException("JS Object key must be either String or Number."); } finally { Context.exit(); } return new Map.Entry<K, V>() { public K getKey() { return (K)ekey; } public V getValue() { return (V)value; } public V setValue(Object v) { throw new UnsupportedOperationException(); } @Override public boolean equals(Object other) { if (!(other instanceof Map.Entry)) { return false; } Map.Entry e = (Map.Entry)other; return (ekey == null ? e.getKey() == null : ekey.equals(e.getKey())) && (value == null ? e.getValue() == null : value.equals(e.getValue())); } @Override public int hashCode() { return (ekey == null ? 0 : ekey.hashCode()) ^ (value == null ? 0 : value.hashCode()); } @Override public String toString() { return ekey + "=" + value; } }; } public void remove() { if (key == null) { throw new IllegalStateException(); } nativeObject.remove(key); key = null; } }; } @Override public int size() { return nativeObject.size(); } } } }