/* 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; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import org.mozilla.javascript.Scriptable; import org.sablo.BaseWebObject; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectApiDefinition; import org.sablo.specification.property.CustomJSONPropertyType; import org.sablo.specification.property.IBrowserConverterContext; import org.sablo.specification.property.IConvertedPropertyType; import org.sablo.specification.property.ISupportsGranularUpdates; import org.sablo.util.ValueReference; import org.sablo.websocket.TypedData; import org.sablo.websocket.utils.DataConversion; import org.sablo.websocket.utils.JSONUtils; import com.servoy.j2db.FlattenedSolution; import com.servoy.j2db.persistence.ChildWebComponent; 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.property.types.IDataLinkedType; import com.servoy.j2db.server.ngclient.property.types.IDataLinkedType.TargetDataLinks; import com.servoy.j2db.server.ngclient.property.types.ITemplateValueUpdaterType; import com.servoy.j2db.server.ngclient.property.types.NGConversions.FormElementToJSON; 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.ISabloComponentToRhino; import com.servoy.j2db.server.ngclient.property.types.PropertyPath; import com.servoy.j2db.util.Debug; /** * Implementation for the complex custom type "component". * * @author acostescu */ public class ComponentPropertyType extends CustomJSONPropertyType<ComponentTypeSabloValue> implements IDesignToFormElement<JSONObject, ComponentTypeFormElementValue, ComponentTypeSabloValue>, IFormElementToTemplateJSON<ComponentTypeFormElementValue, ComponentTypeSabloValue>, IFormElementToSabloComponent<ComponentTypeFormElementValue, ComponentTypeSabloValue>, IConvertedPropertyType<ComponentTypeSabloValue>, ISabloComponentToRhino<ComponentTypeSabloValue>, ISupportsGranularUpdates<ComponentTypeSabloValue>, ITemplateValueUpdaterType<ComponentTypeSabloValue> { public static final ComponentPropertyType INSTANCE = new ComponentPropertyType(null); public static final String TYPE_NAME = ChildWebComponent.COMPONENT_PROPERTY_TYPE_NAME; // START keys and values used in JSON public final static String API_CALL_TYPES_KEY = "apiCallTypes"; public final static String FUNCTION_NAME_KEY = "functionName"; public final static String CALL_ON_KEY = "callOn"; public final static int CALL_ON_SELECTED_RECORD = 0; public final static int CALL_ON_ALL_RECORDS = 1; protected static final String PROPERTY_UPDATES_KEY = "propertyUpdates"; protected static final String MODEL_KEY = "model"; protected static final String MODEL_VIEWPORT_KEY = "model_vp"; protected static final String MODEL_VIEWPORT_CHANGES_KEY = "model_vp_ch"; protected static final String FOUNDSET_CONFIG_PROPERTY_NAME = "foundsetConfig"; protected static final String RECORD_BASED_PROPERTIES = "recordBasedProperties"; public static final String PROPERTY_NAME_KEY = "pn"; public static final String VALUE_KEY = "v"; // END keys and values used in JSON protected int uniqueId = 1; public ComponentPropertyType(PropertyDescription definition) { super(TYPE_NAME, definition); } @Override public String getName() { return TYPE_NAME; } @Override public ComponentTypeFormElementValue toFormElementValue(JSONObject designValue, PropertyDescription pd, FlattenedSolution flattenedSolution, INGFormElement fe, PropertyPath propertyPath) { try { FormElement element = new FormElement((String)designValue.get(ChildWebComponent.TYPE_NAME_KEY), (JSONObject)designValue.get(ChildWebComponent.DEFINITION_KEY), fe.getForm(), fe.getName() + (uniqueId++), flattenedSolution, propertyPath, fe.getDesignId() != null); return getFormElementValue(designValue.optJSONArray(API_CALL_TYPES_KEY), pd, propertyPath, element, flattenedSolution); } catch (JSONException e) { Debug.error(e); return null; } } public ComponentTypeFormElementValue getFormElementValue(JSONArray callTypes, PropertyDescription pd, PropertyPath propertyPath, FormElement element, FlattenedSolution flattenedSolution) throws JSONException { List<String> apisOnAll = null; List<String> recordBasedProperties = null; if (forFoundsetTypedPropertyName(pd) != null) { if (callTypes == null) apisOnAll = findCallTypesInApiSpecDefinition(element.getWebComponentSpec().getApiFunctions()); else { apisOnAll = new ArrayList<String>(); for (int j = 0; j < callTypes.length(); j++) { JSONObject o = callTypes.getJSONObject(j); if (o.getInt(CALL_ON_KEY) == CALL_ON_ALL_RECORDS) apisOnAll.add(o.getString(FUNCTION_NAME_KEY)); } } recordBasedProperties = findRecordAwareRootProperties(element, flattenedSolution); } // else viewPortData and apisOnAll are not relevant return new ComponentTypeFormElementValue(element, apisOnAll, recordBasedProperties, propertyPath.currentPathCopy()); } public String forFoundsetTypedPropertyName(PropertyDescription pd) { return pd.getConfig() instanceof ComponentTypeConfig ? ((ComponentTypeConfig)pd.getConfig()).forFoundset : null; } protected List<String> findRecordAwareRootProperties(FormElement formElement, FlattenedSolution flattenedSolution) { List<String> m = new ArrayList<>(); // tagstrings, valuelists, tab seq, ... must be implemented separately and provided as a // viewport containing these values as part of 'components' property Set<Entry<String, PropertyDescription>> propertyDescriptors = formElement.getWebComponentSpec().getProperties().entrySet(); for (Entry<String, PropertyDescription> propertyDescriptorEntry : propertyDescriptors) { if (propertyDescriptorEntry.getValue().getType() instanceof IDataLinkedType) { // as these are root-level component properties, their TargetDataLinks will always be cached (only array element values are not cached) TargetDataLinks dataLinks = (TargetDataLinks)formElement.getPreprocessedPropertyInfo(IDataLinkedType.class, propertyDescriptorEntry.getValue()); if (dataLinks != TargetDataLinks.NOT_LINKED_TO_DATA && dataLinks != null && dataLinks.recordLinked) { m.add(propertyDescriptorEntry.getKey()); } } } return m; } protected List<String> findCallTypesInApiSpecDefinition(Map<String, WebObjectApiDefinition> apis) { List<String> arr = null; if (apis != null) { arr = new ArrayList<String>(); for (Entry<String, WebObjectApiDefinition> apiMethod : apis.entrySet()) { JSONObject apiConfigOptions = apiMethod.getValue().getCustomConfigOptions(); if (apiConfigOptions != null && apiConfigOptions.optInt(CALL_ON_KEY, CALL_ON_SELECTED_RECORD) == CALL_ON_ALL_RECORDS) { arr.add(apiMethod.getKey()); } } if (arr.size() == 0) arr = null; } return arr; } @Override public JSONWriter toTemplateJSONValue(final JSONWriter writer, String key, ComponentTypeFormElementValue formElementValue, PropertyDescription pd, DataConversion conversionMarkers, final FormElementContext formElementContext) throws JSONException { FlattenedSolution clientFlattenedSolution = (formElementContext != null && formElementContext.getContext() != null) ? formElementContext.getContext().getSolution() : null; if (!formElementValue.isSecurityViewable(clientFlattenedSolution)) { return writer; } if (conversionMarkers != null) conversionMarkers.convert(ComponentPropertyType.TYPE_NAME); // so that the client knows it must use the custom client side JS for what JSON it gets // create children of component as specified by this property final FormElementContext feContext = new FormElementContext(formElementValue.element, formElementContext.getContext()); JSONUtils.addKeyIfPresent(writer, key); writer.object(); writeTemplateJSONContent(writer, formElementValue, forFoundsetTypedPropertyName(pd), feContext, new IModelWriter() { @Override public void writeComponentModel() throws JSONException { // TODO here we could remove record based props from fe.propertiesForTemplateJSON(); but normally record based props will not write any value in template anyway TypedData<Map<String, Object>> modelProperties = feContext.getFormElement().propertiesForTemplateJSON(); writer.object(); JSONUtils.writeDataWithConversions(FormElementToJSON.INSTANCE, writer, modelProperties.content, modelProperties.contentType, feContext); writer.endObject(); } }, formElementValue.recordBasedProperties, true); writer.endObject(); return writer; } protected <ContextT> void writeTemplateJSONContent(JSONWriter writer, ComponentTypeFormElementValue formElementValue, String forFoundsetPropertyType, FormElementContext componentFormElementContext, IModelWriter modelWriter, List<String> recordBasedProperties, boolean writeViewportIfFoundsetBased) throws JSONException { if (forFoundsetPropertyType != null) writer.key(FoundsetLinkedPropertyType.FOR_FOUNDSET_PROPERTY_NAME).value(forFoundsetPropertyType); writer.key("componentDirectiveName").value(componentFormElementContext.getFormElement().getTypeName()); writer.key("name").value(componentFormElementContext.getName()); if (componentFormElementContext.getFormElement().getPropertyValue("componentIndex") != null) { writer.key("componentIndex").value(componentFormElementContext.getFormElement().getPropertyValue("componentIndex")); } if (componentFormElementContext.getFormElement().getPropertyValue("headerIndex") != null) { writer.key("headerIndex").value(componentFormElementContext.getFormElement().getPropertyValue("headerIndex")); } writer.key("model"); try { modelWriter.writeComponentModel(); } catch (JSONException | IllegalArgumentException e) { Debug.error("Problem detected when handling a component's (" + componentFormElementContext.getFormElement().getTagname() + ") properties / events.", e); throw e; } writer.key("handlers").object(); for (String handleMethodName : componentFormElementContext.getFormElement().getHandlers()) { writer.key(handleMethodName); JSONObject handlerInfo = new JSONObject(); handlerInfo.put("formName", componentFormElementContext.getFormElement().getForm().getName()); handlerInfo.put("beanName", componentFormElementContext.getName()); writer.value(handlerInfo); } writer.endObject(); if (forFoundsetPropertyType != null) { if (writeViewportIfFoundsetBased) writer.key(MODEL_VIEWPORT_KEY).array().endArray(); // this will contain record based properties for the foundset's viewPort writer.key(FOUNDSET_CONFIG_PROPERTY_NAME).object(); if (recordBasedProperties != null) { writer.key(RECORD_BASED_PROPERTIES).array(); for (String propertyName : recordBasedProperties) { writer.value(propertyName); } writer.endArray(); } if (formElementValue.apisOnAll != null) { writer.key(API_CALL_TYPES_KEY).array(); for (String methodName : formElementValue.apisOnAll) { writer.object().key(methodName).value(CALL_ON_ALL_RECORDS).endObject(); } writer.endArray(); } writer.endObject(); } } @Override public ComponentTypeSabloValue toSabloComponentValue(ComponentTypeFormElementValue formElementValue, PropertyDescription pd, INGFormElement formElement, WebFormComponent component, DataAdapterList dal) { if (!formElementValue.isSecurityViewable(dal.getApplication().getFlattenedSolution())) { return null; } return new ComponentTypeSabloValue(formElementValue, pd, forFoundsetTypedPropertyName(pd)); } @Override public ComponentTypeSabloValue fromJSON(Object newJSONValue, ComponentTypeSabloValue previousSabloValue, PropertyDescription pd, IBrowserConverterContext dataConverterContext, ValueReference<Boolean> returnValueAdjustedIncommingValue) { if (previousSabloValue != null) { previousSabloValue.browserUpdatesReceived(newJSONValue); } // else there's nothing to do here / this type can't receive browser updates when server has no value for it return previousSabloValue; } @Override public JSONWriter initialToJSON(JSONWriter writer, String key, ComponentTypeSabloValue sabloValue, PropertyDescription pd, DataConversion clientConversion, IBrowserConverterContext dataConverterContext) throws JSONException { // this sends a diff update between the value it has in the template and the initial data requested after runtime components were created or during a page refresh. if (sabloValue != null) { JSONUtils.addKeyIfPresent(writer, key); sabloValue.initialToJSON(writer, clientConversion, this); } return writer; } @Override public JSONWriter changesToJSON(JSONWriter writer, String key, ComponentTypeSabloValue sabloValue, PropertyDescription pd, DataConversion clientConversion, IBrowserConverterContext dataConverterContext) throws JSONException { if (sabloValue != null) { JSONUtils.addKeyIfPresent(writer, key); sabloValue.changesToJSON(writer, clientConversion, this); } return writer; } @Override public JSONWriter toJSON(JSONWriter writer, String key, ComponentTypeSabloValue sabloValue, PropertyDescription pd, DataConversion clientConversion, IBrowserConverterContext dataConverterContext) throws JSONException { if (sabloValue != null) { JSONUtils.addKeyIfPresent(writer, key); sabloValue.fullToJSON(writer, clientConversion, this); } return writer; } @Override public boolean isValueAvailableInRhino(ComponentTypeSabloValue webComponentValue, PropertyDescription pd, BaseWebObject componentOrService) { return false; } @Override public Object toRhinoValue(ComponentTypeSabloValue webComponentValue, PropertyDescription pd, BaseWebObject componentOrService, Scriptable startScriptable) { if (webComponentValue != null) { return webComponentValue.getRuntimeComponent(); } return null; } @Override public Object parseConfig(JSONObject config) { if (config == null) return null; String tmp = config.optString("forFoundset"); return tmp == null || tmp.length() == 0 ? null : new ComponentTypeConfig(tmp); } protected interface IModelWriter { void writeComponentModel() throws JSONException; } }