/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.server.ngclient.property.types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;
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;
import org.sablo.specification.property.CustomJSONObjectType;
import org.sablo.specification.property.IBrowserConverterContext;
import org.sablo.specification.property.WrappingContext;
import org.sablo.websocket.utils.DataConversion;
import org.sablo.websocket.utils.JSONUtils;
import com.servoy.j2db.FlattenedSolution;
import com.servoy.j2db.server.ngclient.DataAdapterList;
import com.servoy.j2db.server.ngclient.FormElement;
import com.servoy.j2db.server.ngclient.FormElementContext;
import com.servoy.j2db.server.ngclient.FormElementExtension;
import com.servoy.j2db.server.ngclient.INGFormElement;
import com.servoy.j2db.server.ngclient.WebFormComponent;
import com.servoy.j2db.server.ngclient.component.RhinoMapOrArrayWrapper;
import com.servoy.j2db.server.ngclient.property.ComponentTypeFormElementValue;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.IDesignDefaultToFormElement;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.IDesignToFormElement;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.IFormElementToSabloComponent;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.IFormElementToTemplateJSON;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.IRhinoToSabloComponent;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.ISabloComponentToRhino;
import com.servoy.j2db.server.ngclient.property.types.NGConversions.InitialToJSONConverter;
import com.servoy.j2db.util.ServoyJSONObject;
/**
* A JSON array type that is Servoy NG client aware as well.
* So it adds all conversions from {@link NGConversions}.
*
* @author acostescu
*/
//TODO these SabloT, SabloWT and FormElementT 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 NGCustomJSONObjectType<SabloT, SabloWT, FormElementT> extends CustomJSONObjectType<SabloT, SabloWT>
implements IDesignToFormElement<JSONObject, Map<String, FormElementT>, Map<String, SabloT>>,
IFormElementToTemplateJSON<Map<String, FormElementT>, Map<String, SabloT>>, IFormElementToSabloComponent<Map<String, FormElementT>, Map<String, SabloT>>,
ISabloComponentToRhino<Map<String, SabloT>>, IRhinoToSabloComponent<Map<String, SabloT>>, ISupportTemplateValue<Map<String, FormElementT>>,
ITemplateValueUpdaterType<ChangeAwareMap<SabloT, SabloWT>>, IFindModeAwareType<Map<String, FormElementT>, Map<String, SabloT>>,
IDataLinkedType<Map<String, FormElementT>, Map<String, SabloT>>
{
public NGCustomJSONObjectType(String typeName, PropertyDescription definition)
{
super(typeName, definition);
}
@Override
public Map<String, FormElementT> toFormElementValue(JSONObject designValue, PropertyDescription mainProperty, FlattenedSolution flattenedSolution,
INGFormElement formElement, PropertyPath propertyPath)
{
if (designValue != null)
{
Map<String, FormElementT> formElementValues = new HashMap<>(designValue.length());
Iterator<String> keys = designValue.keys();
while (keys.hasNext())
{
String key = keys.next();
try
{
propertyPath.add(key);
PropertyDescription property = getCustomJSONTypeDefinition().getProperty(key);
if (property != null) formElementValues.put(key, (FormElementT)NGConversions.INSTANCE.convertDesignToFormElementValue(designValue.opt(key),
property, flattenedSolution, formElement, propertyPath));
}
finally
{
propertyPath.backOneLevel();
}
}
for (PropertyDescription pd : getCustomJSONTypeDefinition().getProperties().values())
{
if (!formElementValues.containsKey(pd.getName()))
{
if (pd.hasDefault())
{
propertyPath.add(pd.getName());
formElementValues.put(pd.getName(), (FormElementT)NGConversions.INSTANCE.convertDesignToFormElementValue(pd.getDefaultValue(), pd,
flattenedSolution, formElement, propertyPath));
propertyPath.backOneLevel();
}
else if (pd.getType() instanceof IDesignDefaultToFormElement< ? , ? , ? >)
{
propertyPath.add(pd.getName());
formElementValues.put(pd.getName(), (FormElementT)((IDesignDefaultToFormElement< ? , ? , ? >)pd.getType()).toDefaultFormElementValue(pd,
flattenedSolution, formElement, propertyPath));
propertyPath.backOneLevel();
}
}
}
return formElementValues;
}
return null;
}
@Override
public JSONWriter toTemplateJSONValue(JSONWriter writer, String key, Map<String, FormElementT> formElementValue, PropertyDescription pd,
DataConversion conversionMarkers, FormElementContext formElementContext) throws JSONException
{
JSONUtils.addKeyIfPresent(writer, key);
if (conversionMarkers != null) conversionMarkers.convert(CustomJSONObjectType.TYPE_NAME); // so that the client knows it must use the custom client side JS for what JSON it gets
if (formElementValue != null)
{
writer.object().key(CONTENT_VERSION).value(1).key(VALUE).object();
DataConversion arrayConversionMarkers = new DataConversion();
for (Entry<String, FormElementT> e : formElementValue.entrySet())
{
arrayConversionMarkers.pushNode(e.getKey());
NGConversions.INSTANCE.convertFormElementToTemplateJSONValue(writer, e.getKey(), e.getValue(),
getCustomJSONTypeDefinition().getProperty(e.getKey()), arrayConversionMarkers, formElementContext);
arrayConversionMarkers.popNode();
}
writer.endObject();
if (arrayConversionMarkers.getConversions().size() > 0)
{
writer.key("conversions").object();
JSONUtils.writeConversions(writer, arrayConversionMarkers.getConversions());
writer.endObject();
}
writer.endObject();
}
else
{
writer.value(JSONObject.NULL);
}
return writer;
}
@Override
public JSONWriter initialToJSON(JSONWriter writer, String key, ChangeAwareMap<SabloT, SabloWT> changeAwareMap, PropertyDescription pd,
DataConversion conversionMarkers, IBrowserConverterContext dataConverterContext) throws JSONException
{
return toJSON(writer, key, changeAwareMap, conversionMarkers, true, InitialToJSONConverter.INSTANCE, dataConverterContext);
}
@Override
public Map<String, SabloT> toSabloComponentValue(Map<String, FormElementT> formElementValue, PropertyDescription pd, INGFormElement formElement,
WebFormComponent component, DataAdapterList dal)
{
if (formElementValue != null)
{
Map<String, SabloT> map = new HashMap<>(formElementValue.size());
for (Entry<String, FormElementT> e : formElementValue.entrySet())
{
if (e.getValue() instanceof ComponentTypeFormElementValue &&
!((ComponentTypeFormElementValue)e.getValue()).isSecurityViewable(dal.getApplication().getFlattenedSolution()))
{
continue;
}
Object v = NGConversions.INSTANCE.convertFormElementToSabloComponentValue(e.getValue(), getCustomJSONTypeDefinition().getProperty(e.getKey()),
new FormElementExtension(formElement, formElementValue, getCustomJSONTypeDefinition()), component, dal);
if (v != null) map.put(e.getKey(), (SabloT)v);
}
return map;
}
return null;
}
@Override
public Map<String, SabloT> toSabloComponentValue(final Object rhinoValue, final Map<String, SabloT> previousComponentValue, PropertyDescription pd,
final BaseWebObject componentOrService)
{
if (rhinoValue == null || rhinoValue == Scriptable.NOT_FOUND) return null;
final ChangeAwareMap<SabloT, SabloWT> previousSpecialMap = (ChangeAwareMap<SabloT, SabloWT>)previousComponentValue;
if (rhinoValue instanceof RhinoMapOrArrayWrapper)
{
return (Map<String, SabloT>)((RhinoMapOrArrayWrapper)rhinoValue).getWrappedValue();
}
else if (previousSpecialMap != null && previousSpecialMap.getBaseMap() instanceof IRhinoNativeProxy &&
((IRhinoNativeProxy)previousSpecialMap.getBaseMap()).getBaseRhinoScriptable() == rhinoValue)
{
return previousComponentValue; // this can get called a lot when a native Rhino wrapper map and proxy are in use; don't create new values each time
// something is accessed in the wrapper+converter+proxy map cause that messes up references
}
else
{
// if it's some kind of object, convert it (in depth, iterate over children)
RhinoNativeObjectWrapperMap<SabloT, SabloWT> rhinoMap = null;
if (rhinoValue instanceof NativeObject)
{
rhinoMap = new RhinoNativeObjectWrapperMap<SabloT, SabloWT>((NativeObject)rhinoValue, getCustomJSONTypeDefinition(), previousComponentValue,
componentOrService, getChildPropsThatNeedWrapping());
ChangeAwareMap<SabloT, SabloWT> cam = wrap(rhinoMap, previousSpecialMap, pd, new WrappingContext(componentOrService, pd.getName()));
cam.markAllChanged();
return cam;
// if we really want to remove the extra-conversion map above and convert all to a new map we could do it by executing the code below after a toJSON is called (so after a request finishes,
// we consider that in the next request the user will only use property reference again taken from service/component, so the new converted map, not anymore the object that was created in JS directly,
// but this still won't work if the user really holds on to that old/initial reference and changes it...); actually if the initial value is used, it will not be change-aware anyway...
// for (Entry<String, Object> e : rhinoMap.entrySet())
// {
// convertedMap.put(
// e.getKey(),
// NGConversions.INSTANCE.convertRhinoToSabloComponentValue(e.getValue(),
// previousComponentValue != null ? previousComponentValue.get(e.getKey()) : null,
// getCustomJSONTypeDefinition().getProperty(e.getKey()), componentOrService));
// }
}
}
return previousComponentValue; // or should we return null or throw exception here? incompatible thing was assigned
}
@Override
public boolean isValueAvailableInRhino(Map<String, SabloT> webComponentValue, PropertyDescription pd, BaseWebObject componentOrService)
{
return true;
}
@Override
public Object toRhinoValue(Map<String, SabloT> webComponentValue, PropertyDescription pd, BaseWebObject componentOrService, Scriptable startScriptable)
{
return webComponentValue == null ? null : new RhinoMapOrArrayWrapper(webComponentValue, componentOrService, pd, startScriptable);
}
@Override
public boolean valueInTemplate(Map<String, FormElementT> object, PropertyDescription pd, FormElementContext formElementContext)
{
if (object != null)
{
PropertyDescription desc = getCustomJSONTypeDefinition();
for (Entry<String, PropertyDescription> entry : desc.getProperties().entrySet())
{
FormElementT value = object.get(entry.getKey());
if (value != null && entry.getValue().getType() instanceof ISupportTemplateValue< ? >)
{
if (!((ISupportTemplateValue)entry.getValue().getType()).valueInTemplate(value, entry.getValue(), formElementContext))
{
return false;
}
}
}
}
return true;
}
@Override
public boolean isFindModeAware(Map<String, FormElementT> formElementValue, PropertyDescription pd, FlattenedSolution flattenedSolution,
FormElement formElement)
{
if (formElementValue == null) return false;
boolean isFindModeAware = false;
// just to give a chance to nested find mode aware properties to register themselves in FormElement
for (Entry<String, PropertyDescription> entry : pd.getProperties().entrySet())
{
FormElementT value = formElementValue.get(entry.getKey());
PropertyDescription entryPD = entry.getValue();
// as array element property descriptions can describe multiple property values in the same bean - we won't cache those
if (entryPD.getType() instanceof IFindModeAwareType)
{
boolean b = ((IFindModeAwareType)entryPD.getType()).isFindModeAware(ServoyJSONObject.jsonNullToNull(value), entryPD, flattenedSolution,
formElement);
isFindModeAware |= b;
formElement.getOrCreatePreprocessedPropertyInfoMap(IFindModeAwareType.class).put(entryPD, Boolean.valueOf(b));
}
}
return isFindModeAware;
}
@Override
public TargetDataLinks getDataLinks(Map<String, FormElementT> formElementValue, PropertyDescription pd, FlattenedSolution flattenedSolution,
FormElement formElement)
{
if (ServoyJSONObject.isJavascriptNullOrUndefined(formElementValue)) return TargetDataLinks.NOT_LINKED_TO_DATA;
ArrayList<String> dps = new ArrayList<>();
boolean recordLinked = false;
// just to give a chance to nested find mode aware properties to register themselves in FormElement
for (Entry<String, PropertyDescription> entry : pd.getProperties().entrySet())
{
FormElementT value = formElementValue.get(entry.getKey());
PropertyDescription entryPD = entry.getValue();
// as array element property descriptions can describe multiple property values in the same bean - we won't cache those
if (entryPD.getType() instanceof IDataLinkedType)
{
TargetDataLinks entryDPs = ((IDataLinkedType)entryPD.getType()).getDataLinks(ServoyJSONObject.jsonNullToNull(value), entryPD, flattenedSolution,
formElement);
formElement.getOrCreatePreprocessedPropertyInfoMap(IDataLinkedType.class).put(entryPD, entryDPs);
if (entryDPs != null && entryDPs != TargetDataLinks.NOT_LINKED_TO_DATA)
{
dps.addAll(Arrays.asList(entryDPs.dataProviderIDs));
recordLinked |= entryDPs.recordLinked;
}
}
}
if (dps.size() == 0 && recordLinked == false) return TargetDataLinks.NOT_LINKED_TO_DATA;
else return new TargetDataLinks(dps.toArray(new String[dps.size()]), recordLinked);
}
}