/* 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.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeJavaArray; import org.mozilla.javascript.Scriptable; import org.sablo.BaseWebObject; import org.sablo.specification.PropertyDescription; import org.sablo.specification.property.ChangeAwareList; import org.sablo.specification.property.CustomJSONArrayType; 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.INGFormElement; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.component.RhinoMapOrArrayWrapper; 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 */ public class NGCustomJSONArrayType<SabloT, SabloWT> extends CustomJSONArrayType<SabloT, SabloWT> implements IDesignToFormElement<JSONArray, Object[], Object>, IFormElementToTemplateJSON<Object[], Object>, IFormElementToSabloComponent<Object[], Object>, ISabloComponentToRhino<Object>, IRhinoToSabloComponent<Object>, ISupportTemplateValue<Object[]>, ITemplateValueUpdaterType<ChangeAwareList<SabloT, SabloWT>>, IFindModeAwareType<Object[], Object>, IDataLinkedType<Object[], Object> { public NGCustomJSONArrayType(PropertyDescription definition) { super(definition); } @Override public Object[] toFormElementValue(JSONArray designValue, PropertyDescription pd, FlattenedSolution flattenedSolution, INGFormElement formElement, PropertyPath propertyPath) { if (designValue != null) { Object[] formElementValues = new Object[designValue.length()]; for (int i = designValue.length() - 1; i >= 0; i--) { try { propertyPath.add(i); formElementValues[i] = NGConversions.INSTANCE.convertDesignToFormElementValue(designValue.opt(i), getCustomJSONTypeDefinition(), flattenedSolution, formElement, propertyPath); } finally { propertyPath.backOneLevel(); } } return formElementValues; } return null; } @Override public JSONWriter initialToJSON(JSONWriter writer, String key, ChangeAwareList<SabloT, SabloWT> changeAwareList, PropertyDescription pd, DataConversion conversionMarkers, IBrowserConverterContext dataConverterContext) throws JSONException { return toJSON(writer, key, changeAwareList, conversionMarkers, true, InitialToJSONConverter.INSTANCE, dataConverterContext); } @Override public JSONWriter toTemplateJSONValue(JSONWriter writer, String key, Object[] formElementValue, PropertyDescription pd, DataConversion conversionMarkers, FormElementContext formElementContext) throws JSONException { JSONUtils.addKeyIfPresent(writer, key); if (conversionMarkers != null) conversionMarkers.convert(CustomJSONArrayType.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).array(); DataConversion arrayConversionMarkers = new DataConversion(); for (int i = 0; i < formElementValue.length; i++) { arrayConversionMarkers.pushNode(String.valueOf(i)); NGConversions.INSTANCE.convertFormElementToTemplateJSONValue(writer, null, formElementValue[i], getCustomJSONTypeDefinition(), arrayConversionMarkers, formElementContext); arrayConversionMarkers.popNode(); } writer.endArray(); if (arrayConversionMarkers.getConversions().size() > 0) { // elements from the array may have been skipped when writing to template, // so make sure the conversions keys are correct Map<String, Object> conversions = arrayConversionMarkers.getConversions(); Map<String, Object> rebasedConversions = new HashMap<String, Object>(); int index = 0; for (Map.Entry<String, Object> entry : conversions.entrySet()) { rebasedConversions.put(String.valueOf(index++), entry.getValue()); } writer.key("conversions").object(); JSONUtils.writeConversions(writer, rebasedConversions); writer.endObject(); } writer.endObject(); } else { writer.value(JSONObject.NULL); } return writer; } @Override public Object toSabloComponentValue(Object[] formElementValue, PropertyDescription pd, INGFormElement formElement, WebFormComponent component, DataAdapterList dal) { if (formElementValue != null) { List<SabloT> list = new ArrayList<>(formElementValue.length); for (Object element : formElementValue) { Object v = NGConversions.INSTANCE.convertFormElementToSabloComponentValue(element, getCustomJSONTypeDefinition(), formElement, component, dal); if (v != null) list.add((SabloT)v); } return list; } return null; } @Override public Object toSabloComponentValue(final Object rhinoValue, Object previousComponentValue, PropertyDescription pd, final BaseWebObject componentOrService) { if (rhinoValue == null || rhinoValue == Scriptable.NOT_FOUND) return null; final ChangeAwareList<SabloT, SabloWT> previousSpecialArray = (ChangeAwareList<SabloT, SabloWT>)previousComponentValue; if (rhinoValue instanceof RhinoMapOrArrayWrapper) { return ((RhinoMapOrArrayWrapper)rhinoValue).getWrappedValue(); } else if (previousSpecialArray != null && previousSpecialArray.getBaseList() instanceof IRhinoNativeProxy && ((IRhinoNativeProxy)previousSpecialArray.getBaseList()).getBaseRhinoScriptable() == rhinoValue) { return previousComponentValue; // this can get called a lot when a native Rhino wrapper list and proxy are in use; don't create new values each time // something is accessed in the wrapper+converter+proxy list cause that messes up references } else { // if it's some kind of array List<SabloT> rhinoArray = null; if (rhinoValue instanceof NativeArray) { rhinoArray = new RhinoNativeArrayWrapperList<SabloT, SabloWT>((NativeArray)rhinoValue, getCustomJSONTypeDefinition(), previousSpecialArray, componentOrService); } else if (rhinoValue instanceof NativeJavaArray) { // rhinoValue.unwrap() will be a java static array [] rhinoArray = new RhinoNativeArrayWrapperList<SabloT, SabloWT>(Arrays.asList(((NativeJavaArray)rhinoValue).unwrap()), getCustomJSONTypeDefinition(), previousSpecialArray, componentOrService, (Scriptable)rhinoValue); } if (rhinoArray != null) { ChangeAwareList<SabloT, SabloWT> cal = wrap(rhinoArray, (ChangeAwareList<SabloT, SabloWT>)previousComponentValue, pd, new WrappingContext(componentOrService, pd.getName())); cal.markAllChanged(); return cal; // if we really want to remove the extra-conversion list above and convert all to a new list 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 list, not anymore the array 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... // int i = 0; // for (Object rv : rhinoArray) // { // convertedArray.add(NGConversions.INSTANCE.convertRhinoToSabloComponentValue(rv, // (previousSpecialArray != null && previousSpecialArray.size() > i) ? previousSpecialArray.get(i) : null, getCustomJSONTypeDefinition(), // componentOrService)); // i++; // } } } return previousComponentValue; // or should we return null or throw exception here? incompatible thing was assigned } @Override public boolean isValueAvailableInRhino(Object webComponentValue, PropertyDescription pd, BaseWebObject componentOrService) { return true; } @Override public Object toRhinoValue(Object webComponentValue, PropertyDescription pd, BaseWebObject componentOrService, Scriptable startScriptable) { return webComponentValue == null ? null : new RhinoMapOrArrayWrapper(webComponentValue, componentOrService, pd, startScriptable); } @Override public boolean valueInTemplate(Object[] values, PropertyDescription pd, FormElementContext formElementContext) { if (values != null && values.length > 0) { PropertyDescription desc = getCustomJSONTypeDefinition(); if (desc.getType() instanceof ISupportTemplateValue) { ISupportTemplateValue<Object> type = (ISupportTemplateValue<Object>)desc.getType(); for (Object object : values) { if (!type.valueInTemplate(object, desc, formElementContext)) { return false; } } } } return true; } @Override public boolean isFindModeAware(Object[] formElementValue, PropertyDescription pd, FlattenedSolution flattenedSolution, FormElement formElement) { if (formElementValue == null) return false; boolean isFindModeAware = false; PropertyDescription entryPD = getCustomJSONTypeDefinition(); for (Object value : formElementValue) { // as array element property descriptions can describe multiple property values in the same bean - we won't cache those if (entryPD.getType() instanceof IFindModeAwareType) { if (((IFindModeAwareType)entryPD.getType()).isFindModeAware(ServoyJSONObject.jsonNullToNull(value), entryPD, flattenedSolution, formElement)) { isFindModeAware = true; break; } } } return isFindModeAware; } @Override public TargetDataLinks getDataLinks(Object[] formElementValue, PropertyDescription pd, FlattenedSolution flattenedSolution, FormElement formElement) { if (formElementValue == null) return TargetDataLinks.NOT_LINKED_TO_DATA; ArrayList<String> dps = new ArrayList<>(); boolean recordLinked = false; PropertyDescription entryPD = getCustomJSONTypeDefinition(); for (Object value : formElementValue) { // as array element property descriptions can describe multiple property values in the same bean - we won't cache those; // if we ever need to cache these for performance we have to use something more unique (like property path) for formElement.getOrCreatePreprocessedPropertyInfoMap(...).put(...) and cache them for arrays as well if (entryPD.getType() instanceof IDataLinkedType) { TargetDataLinks entryDPs = ((IDataLinkedType)entryPD.getType()).getDataLinks(ServoyJSONObject.jsonNullToNull(value), entryPD, flattenedSolution, formElement); 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); } }