/*
* 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.AbstractList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.sablo.BaseWebObject;
import org.sablo.specification.PropertyDescription;
import org.sablo.specification.property.ChangeAwareList.IAttachAware;
import org.sablo.specification.property.ChangeAwareList.IAttachHandler;
import org.sablo.specification.property.ConvertedList;
import org.sablo.specification.property.IWrappedBaseListProvider;
import org.sablo.specification.property.IWrapperType;
import org.sablo.specification.property.WrappingContext;
/**
* This list is able to act as a Sablo wrap-aware map that is based on a (native or otherwise) Rhino JS array 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 JS Rhino array, 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 list behaves a bit strange as it will not keep an array of Wrapped sablo values, but it will keep a JS Rhino array based conversion array and when a wrapped sablo value is needed
* is needed it will offer another converted list 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
*/
public class RhinoNativeArrayWrapperList<SabloT, SabloWT> extends ConvertedList<SabloT, Object>
implements IWrappedBaseListProvider, IRhinoNativeProxy, IAttachAware<SabloWT>
{
protected Map<Integer, SabloT> previousValues;
protected BaseWebObject componentOrService;
protected final PropertyDescription elementTypeDefinition;
protected final Scriptable rhinoScriptable;
protected ConvertedList<SabloWT, SabloT> sabloWrappedBaseList;
protected IAttachHandler<SabloWT> attachHandler;
public RhinoNativeArrayWrapperList(NativeArray rhinoArray, PropertyDescription elementTypeDefinition, List<SabloT> previousComponentValue,
BaseWebObject componentOrService)
{
this(new NativeArrayProxyList<Object>(rhinoArray), elementTypeDefinition, previousComponentValue, componentOrService, rhinoArray);
}
public Scriptable getBaseRhinoScriptable()
{
return rhinoScriptable;
}
@Override
public void setAttachHandler(IAttachHandler<SabloWT> attachHandler)
{
this.attachHandler = attachHandler;
}
public RhinoNativeArrayWrapperList(List<Object> rhinoBasedList, PropertyDescription elementTypeDefinition, List<SabloT> previousComponentValue,
BaseWebObject componentOrService, Scriptable rhinoScriptable)
{
super(rhinoBasedList);
this.previousValues = new HashMap<Integer, SabloT>();
if (previousComponentValue != null)
{
for (int i = previousComponentValue.size() - 1; i >= 0; i--)
this.previousValues.put(Integer.valueOf(i), previousComponentValue.get(i));
}
this.componentOrService = componentOrService;
this.elementTypeDefinition = elementTypeDefinition;
this.rhinoScriptable = rhinoScriptable;
}
@Override
protected SabloT convertFromBase(int i, Object value)
{
SabloT old = (previousValues != null && previousValues.size() > i) ? previousValues.get(Integer.valueOf(i)) : null;
SabloT v = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(value, old, elementTypeDefinition, componentOrService);
// previousComponentValue.get(i) 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(Integer.valueOf(i), v);
if (old != v)
{
if (attachHandler != null)
{
attachHandler.detachFromBaseObjectIfNeeded(i, wrap(old));
attachHandler.attachToBaseObjectIfNeeded(i, wrap(v));
}
}
return v;
}
@Override
protected Object convertToBase(int i, SabloT value)
{
return NGConversions.INSTANCE.convertSabloComponentToRhinoValue(value, elementTypeDefinition, componentOrService, rhinoScriptable);
}
public List<SabloWT> getWrappedBaseList()
{
if (!(elementTypeDefinition.getType() instanceof IWrapperType< ? , ? >)) return (List<SabloWT>)this; // sablo type == wrapped type, no wrapping will happen
if (sabloWrappedBaseList == 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
sabloWrappedBaseList = new ConvertedList<SabloWT, SabloT>(this)
{
@Override
protected SabloWT convertFromBase(int i, SabloT value)
{
return wrap(value);
}
@Override
protected SabloT convertToBase(int i, SabloWT value)
{
IWrapperType<SabloT, SabloWT> wt = (IWrapperType<SabloT, SabloWT>)elementTypeDefinition.getType();
return wt.unwrap(value);
}
};
}
return sabloWrappedBaseList;
}
protected SabloWT wrap(SabloT value)
{
if (elementTypeDefinition.getType() instanceof IWrapperType)
{
IWrapperType<SabloT, SabloWT> wt = (IWrapperType<SabloT, SabloWT>)elementTypeDefinition.getType();
return wt.wrap(value, null /* we never store the wrapped value here... */, elementTypeDefinition,
new WrappingContext(componentOrService, elementTypeDefinition.getName()));
}
else return (SabloWT)value;
}
/**
* This is a proxy List to the underlying Rhino NativeArray. It works directly with scriptable values, doesn't take into consideration
* Rhino wrap/unwrap operations and NativeArray (that itself implements List) in it's List impl.
*
* @author acostescu
*/
public static class NativeArrayProxyList<T> extends AbstractList<T>
{
private final NativeArray nativeArray;
public NativeArrayProxyList(NativeArray nativeArray)
{
this.nativeArray = nativeArray;
}
protected Object executeNativeFunc(String functionName, Object[] args)
{
Context cx = Context.enter();
try
{
return ScriptableObject.callMethod(cx, nativeArray, functionName, args);
}
finally
{
Context.exit();
}
}
@Override
public T get(int index)
{
Context.enter();
try
{
return (T)nativeArray.get(index, nativeArray);
}
finally
{
Context.exit();
}
}
@Override
public T set(int index, T element)
{
Context.enter();
try
{
T old = size() > index ? (T)nativeArray.get(index, nativeArray) : null;
nativeArray.put(index, nativeArray, element);
return old;
}
finally
{
Context.exit();
}
}
@Override
public boolean add(T e)
{
int oldSize = size();
executeNativeFunc("push", new Object[] { e });
return oldSize != size();
}
@Override
public T remove(int index)
{
T oldValue = size() > index ? get(index) : null;
executeNativeFunc("splice", new Object[] { Integer.valueOf(index), Integer.valueOf(1) });
return oldValue;
}
@Override
public int size()
{
return nativeArray.size();
}
}
}