/* 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.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; 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.sablo.IChangeListener; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectSpecification; import org.sablo.specification.property.ISmartPropertyValue; import org.sablo.websocket.TypedData; import org.sablo.websocket.utils.DataConversion; import org.sablo.websocket.utils.JSONUtils; import org.sablo.websocket.utils.JSONUtils.ChangesToJSONConverter; import org.sablo.websocket.utils.JSONUtils.FullValueToJSONConverter; import com.servoy.j2db.dataprocessing.IFoundSetInternal; import com.servoy.j2db.dataprocessing.IRecordInternal; import com.servoy.j2db.persistence.IPersist; import com.servoy.j2db.server.ngclient.ComponentContext; import com.servoy.j2db.server.ngclient.ComponentFactory; import com.servoy.j2db.server.ngclient.FormElement; import com.servoy.j2db.server.ngclient.FormElementContext; import com.servoy.j2db.server.ngclient.IDataAdapterList; import com.servoy.j2db.server.ngclient.IDirtyPropertyListener; import com.servoy.j2db.server.ngclient.IWebFormUI; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.WebFormUI; import com.servoy.j2db.server.ngclient.component.RuntimeWebComponent; import com.servoy.j2db.server.ngclient.property.ComponentPropertyType.IModelWriter; import com.servoy.j2db.server.ngclient.property.FoundsetTypeChangeMonitor.RowData; import com.servoy.j2db.server.ngclient.property.types.DataproviderPropertyType; import com.servoy.j2db.server.ngclient.property.types.DataproviderTypeSabloValue; import com.servoy.j2db.server.ngclient.property.types.IDataLinkedType.TargetDataLinks; import com.servoy.j2db.server.ngclient.property.types.IWrapperDataLinkedType; import com.servoy.j2db.server.ngclient.property.types.NGConversions.FormElementToJSON; import com.servoy.j2db.server.ngclient.property.types.NGConversions.InitialToJSONConverter; import com.servoy.j2db.server.ngclient.property.types.NGCustomJSONArrayType; import com.servoy.j2db.server.ngclient.property.types.NGCustomJSONObjectType; import com.servoy.j2db.server.ngclient.property.types.ReadonlyPropertyType; import com.servoy.j2db.server.ngclient.property.types.ReadonlySabloValue; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.UUID; /** * Value used at runtime in Sablo component. * * @author acostescu */ @SuppressWarnings("nls") public class ComponentTypeSabloValue implements ISmartPropertyValue { public static final String NO_OP = "n"; protected WebFormComponent childComponent; protected boolean componentIsCreated = false; protected String forFoundsetTypedPropertyName; protected PropertyChangeListener forFoundsetPropertyListener, readonlyPropertyListener; protected boolean recordBasedPropertiesChanged = false; protected boolean recordBasedPropertiesChangedComparedToTemplate = false; protected ViewportDataChangeMonitor<ComponentViewportRowDataProvider> viewPortChangeMonitor; protected List<Runnable> changesWhileUpdatingFoundsetBasedDPFromClient; protected ComponentDataLinkedPropertyListener dataLinkedPropertyRegistrationListener; // only used in case component is foundset-linked protected final List<String> recordBasedProperties; protected WebFormComponent parentComponent; protected IChangeListener monitor; protected PropertyDescription componentPropertyDescription; protected final ComponentTypeFormElementValue formElementValue; public ComponentTypeSabloValue(ComponentTypeFormElementValue formElementValue, PropertyDescription componentPropertyDescription, String forFoundsetTypedPropertyName) { this.formElementValue = formElementValue; this.forFoundsetTypedPropertyName = forFoundsetTypedPropertyName; this.recordBasedProperties = forFoundsetTypedPropertyName != null ? new ArrayList<>(formElementValue.recordBasedProperties) : null; this.componentPropertyDescription = componentPropertyDescription; } public String getName() { if (formElementValue != null && formElementValue.element != null) { return formElementValue.element.getName(); } return null; } public PropertyDescription getComponentPropertyDescription() { return componentPropertyDescription; } @Override public void attachToBaseObject(IChangeListener changeMonitor, org.sablo.BaseWebObject parentComp) { componentIsCreated = false; this.parentComponent = (WebFormComponent)parentComp; this.monitor = changeMonitor; if (childComponent != null) { childComponent.dispose(); } createComponentIfNeededAndPossible(); if (forFoundsetTypedPropertyName != null) { this.parentComponent.addPropertyChangeListener(forFoundsetTypedPropertyName, forFoundsetPropertyListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() != null) createComponentIfNeededAndPossible(); } }); } } private void setDataproviderNameToFoundset() { FoundsetTypeSabloValue foundsetPropValue = getFoundsetValue(); Collection<PropertyDescription> dp = childComponent.getSpecification().getProperties(DataproviderPropertyType.INSTANCE); if (dp.size() > 0) { //get the first dataprovider property for now PropertyDescription propertyDesc = dp.iterator().next(); Object propertyValue = childComponent.getProperty(propertyDesc.getName()); if (propertyValue != null) { String dataprovider = ((DataproviderTypeSabloValue)propertyValue).getDataProviderID(); foundsetPropValue.setRecordDataLinkedPropertyIDToColumnDP(childComponent.getName(), dataprovider); } } } @Override public void detach() { if (forFoundsetPropertyListener != null) { parentComponent.removePropertyChangeListener(forFoundsetTypedPropertyName, forFoundsetPropertyListener); FoundsetTypeSabloValue foundsetPropValue = getFoundsetValue(); if (foundsetPropValue != null) { if (viewPortChangeMonitor != null) foundsetPropValue.removeViewportDataChangeMonitor(viewPortChangeMonitor); if (dataLinkedPropertyRegistrationListener != null) { FoundsetDataAdapterList dal = foundsetPropValue.getDataAdapterList(); if (dal != null) dal.removeDataLinkedPropertyRegistrationListener(dataLinkedPropertyRegistrationListener); } } } if (readonlyPropertyListener != null) parentComponent.removePropertyChangeListener(WebFormUI.READONLY, readonlyPropertyListener); } private FoundsetTypeSabloValue getFoundsetValue() { if (parentComponent != null) { if (forFoundsetTypedPropertyName != null) { return (FoundsetTypeSabloValue)parentComponent.getProperty(forFoundsetTypedPropertyName); } } return null; } protected void createComponentIfNeededAndPossible() { // this method should get called only after init() got called on all properties from this component (including this one) // so now we should be able to find a potentially linked foundset property value if (componentIsCreated || parentComponent == null) return; final FoundsetTypeSabloValue foundsetPropValue = getFoundsetValue(); if (foundsetPropValue == null && forFoundsetTypedPropertyName != null) return; componentIsCreated = true; IWebFormUI formUI = parentComponent.findParent(IWebFormUI.class); final IDataAdapterList dal = (foundsetPropValue != null ? foundsetPropValue.getDataAdapterList() : formUI.getDataAdapterList()); if (foundsetPropValue != null) { // do this before creating the component so that any attach() methods of it's properties that register data links get caught ((FoundsetDataAdapterList)dal).addDataLinkedPropertyRegistrationListener(createDataLinkedPropertyRegistrationListener()); } childComponent = ComponentFactory.createComponent(dal.getApplication(), dal, formElementValue.element, parentComponent); if (foundsetPropValue != null) { dataLinkedPropertyRegistrationListener.componentIsNowAvailable(); } childComponent.setDirtyPropertyListener(new IDirtyPropertyListener() { @Override public void propertyFlaggedAsDirty(String propertyName, boolean dirty) { if (dirty) { // this gets called whenever a property is flagged as dirty/changed/to be sent to browser if (forFoundsetTypedPropertyName != null && recordBasedProperties.contains(propertyName)) { if (!((FoundsetDataAdapterList)dal).isQuietRecordChangeInProgress() && foundsetPropValue.getFoundset() != null && !foundsetPropValue.getFoundset().isInFindMode()) // if forFoundsetTypedPropertyName != null we are using a foundset DAL, so just cast { // for example valuelist properties can get filtered based on client sent filter in which case the property does change without // any actual change in the record; in this case we need to mark it correctly in viewport as a change IRecordInternal record = dal.getRecord(); Runnable queueChangeRunnable = queueCellChangeOnRecord(propertyName, record); if (changesWhileUpdatingFoundsetBasedDPFromClient != null) { // if for example a dataprovider property change does in its fromJSON a monitor.valueChanged() (for example an integer DP getting client update of 1.15 would want to send back 1.00) // it will end up here; we do want to send that back to the client but as the new value is not // yet pushed to the record, we don't want the new value to be reverted by a DAL.setRecord() that happens when queuing changes for a specific record index // so we need to handle this change at a later time changesWhileUpdatingFoundsetBasedDPFromClient.add(queueChangeRunnable); } else { queueChangeRunnable.run(); } } else { // else this change was probably determined by the fact that we reuse components, changing the record in the DAL to get data for a specific row; // so we need to clear component changes for this property because we do not notify the parent here (we want to ignore the change) so // we shouldn't keep the property marked as dirty - thus blocking future property changes to generate a valueChanged on parent's monitor childComponent.flagPropertyAsDirty(propertyName, false); } } else { // non-record related prop. changed... monitor.valueChanged(); } } } private Runnable queueCellChangeOnRecord(final String propertyName, final IRecordInternal record) { return new Runnable() { @Override public void run() { int idx = foundsetPropValue.getFoundset().getRecordIndex(record); if (idx >= 0) { FoundsetTypeViewport viewPort = foundsetPropValue.getViewPort(); int relativeIdx = idx - viewPort.getStartIndex(); if (relativeIdx >= 0 && relativeIdx < viewPort.getStartIndex() + viewPort.getSize()) { viewPortChangeMonitor.queueCellChange(relativeIdx, idx, propertyName, foundsetPropValue.getFoundset()); } } } }; } }); for (String initialChangedProperty : childComponent.getProperties().content.keySet()) { if (forFoundsetTypedPropertyName == null || !recordBasedProperties.contains(initialChangedProperty)) { // non-record related prop. initially changed... monitor.valueChanged(); } } childComponent.setComponentContext(new ComponentContext(formElementValue.propertyPath)); formUI.contributeComponentToElementsScope(formElementValue.element, formElementValue.element.getWebComponentSpec(), childComponent); for (String handler : childComponent.getFormElement().getHandlers()) { Object value = childComponent.getFormElement().getPropertyValue(handler); if (value instanceof String) { UUID uuid = UUID.fromString((String)value); IPersist function = formUI.getController().getApplication().getFlattenedSolution().searchPersist(uuid); if (function != null) { childComponent.add(handler, function.getID()); } else { Debug.warn("Event handler for " + handler + " not found (form " + formUI.getController().getName() + ", form element " + childComponent.getFormElement().getName() + ")"); } } else if (value instanceof Number && ((Number)value).intValue() > 0) { childComponent.add(handler, ((Number)value).intValue()); } } if (foundsetPropValue != null) { viewPortChangeMonitor = new ViewportDataChangeMonitor<>(monitor, new ComponentViewportRowDataProvider((FoundsetDataAdapterList)dal, childComponent, recordBasedProperties, this)); foundsetPropValue.addViewportDataChangeMonitor(viewPortChangeMonitor); setDataproviderNameToFoundset(); } addPropertyChangeListener(WebFormUI.READONLY, parentComponent.getProperty(WebFormUI.READONLY)); if (childComponent.hasChanges()) monitor.valueChanged(); } private void addPropertyChangeListener(final String property, Object initialValue) { if (parentComponent.getSpecification().getProperty(property) != null && childComponent.getSpecification().getProperty(property) != null) { PropertyDescription propertyDescChild = childComponent.getSpecification().getProperty(property); if (childComponent.getProperty(property) == null || !propertyDescChild.hasDefault() || childComponent.getProperty(property).equals(propertyDescChild.getDefaultValue())) { setChildProperty(property, initialValue); this.parentComponent.addPropertyChangeListener(property, readonlyPropertyListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() != null) { setChildProperty(property, evt.getNewValue()); } } }); } } } private void setChildProperty(String propertyName, Object value) { Object val = value instanceof ReadonlySabloValue ? ((ReadonlySabloValue)value).getValue() : value; if (childComponent.getProperty(propertyName) == null || !childComponent.getProperty(propertyName).equals(val)) //check if the values are different { if (WebFormUI.READONLY.equals(propertyName)) { PropertyDescription propertyDescChild = childComponent.getSpecification().getProperty(WebFormUI.READONLY); if (propertyDescChild.getType() instanceof ReadonlyPropertyType) { val = ReadonlyPropertyType.INSTANCE.toSabloComponentValue(val, (ReadonlySabloValue)childComponent.getProperty(WebFormUI.READONLY), propertyDescChild, childComponent); } } childComponent.setProperty(propertyName, val); } } protected IDataLinkedPropertyRegistrationListener createDataLinkedPropertyRegistrationListener() { return dataLinkedPropertyRegistrationListener = new ComponentDataLinkedPropertyListener(); } protected String findComponentPropertyName(IDataLinkedPropertyValue propertyValueToFind) { Set<String> allPropNames = childComponent.getAllPropertyNames(true); WebObjectSpecification spec = childComponent.getSpecification(); for (String n : allPropNames) if (nestedPropertyFound(propertyValueToFind, childComponent.getProperty(n), spec.getProperty(n))) return n; return null; } /** * Searches for a nested property value "propertyValueToFind" inside a possibly nested object/array "propertyValue" * TODO this is a hackish chunk of code; we should find a cleaner way to identify rootPropertyNames of data linked values! */ protected boolean nestedPropertyFound(IDataLinkedPropertyValue propertyValueToFind, Object propertyValue, PropertyDescription propertyDescription) { if (propertyValue == propertyValueToFind) return true; if (propertyValue == null || propertyDescription == null) return false; if (propertyDescription.getType() instanceof NGCustomJSONObjectType && propertyValue instanceof Map) { PropertyDescription nestedPDs = ((NGCustomJSONObjectType)propertyDescription.getType()).getCustomJSONTypeDefinition(); for (Entry<String, Object> e : ((Map<String, Object>)propertyValue).entrySet()) { if (nestedPropertyFound(propertyValueToFind, e.getValue(), nestedPDs.getProperty(e.getKey()))) return true; } } else if (propertyDescription.getType() instanceof NGCustomJSONArrayType && propertyValue instanceof List) { PropertyDescription nestedPD = ((NGCustomJSONArrayType)propertyDescription.getType()).getCustomJSONTypeDefinition(); for (Object e : (List)propertyValue) { if (nestedPropertyFound(propertyValueToFind, e, nestedPD)) return true; } } else if (propertyDescription.getType() instanceof IWrapperDataLinkedType) { Pair<IDataLinkedPropertyValue, PropertyDescription> tmp = ((IWrapperDataLinkedType)propertyDescription.getType()).getWrappedDataLinkedValue( propertyValue, propertyDescription); return nestedPropertyFound(propertyValueToFind, tmp.getLeft(), tmp.getRight()); } return false; } /** * Writes 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. * @param componentPropertyType */ public JSONWriter initialToJSON(JSONWriter destinationJSON, DataConversion conversionMarkers, ComponentPropertyType componentPropertyType) throws JSONException { if (recordBasedPropertiesChangedComparedToTemplate) return fullToJSON(destinationJSON, conversionMarkers, componentPropertyType); 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 destinationJSON.object(); destinationJSON.key(ComponentPropertyType.PROPERTY_UPDATES_KEY); destinationJSON.object(); // model content TypedData<Map<String, Object>> allProps = childComponent.getProperties(); childComponent.getAndClearChanges(); // just for clear removeRecordDependentProperties(allProps); destinationJSON.key(ComponentPropertyType.MODEL_KEY); destinationJSON.object(); DataConversion conversions = new DataConversion(); // send component model (when linked to foundset only props that are not record related) childComponent.writeProperties(InitialToJSONConverter.INSTANCE, destinationJSON, allProps.content, allProps.contentType, conversions); JSONUtils.writeClientConversions(destinationJSON, conversions); destinationJSON.endObject(); // viewport content writeWholeViewportToJSON(destinationJSON); destinationJSON.endObject(); destinationJSON.endObject(); return destinationJSON; } public JSONWriter changesToJSON(JSONWriter destinationJSON, DataConversion conversionMarkers, ComponentPropertyType componentPropertyType) throws JSONException { if (recordBasedPropertiesChanged) { // just send over the whole thing - viewport and model properties are not the same as they used to be return fullToJSON(destinationJSON, conversionMarkers, componentPropertyType); } 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 TypedData<Map<String, Object>> changes = childComponent.getAndClearChanges(); removeRecordDependentProperties(changes); boolean modelChanged = (changes.content.size() > 0); boolean viewPortChanged = (forFoundsetTypedPropertyName != null && (viewPortChangeMonitor.shouldSendWholeViewport() || viewPortChangeMonitor.getViewPortChanges().size() > 0)); destinationJSON.object(); if (modelChanged || viewPortChanged) { destinationJSON.key(ComponentPropertyType.PROPERTY_UPDATES_KEY); destinationJSON.object(); } if (modelChanged) { destinationJSON.key(ComponentPropertyType.MODEL_KEY); destinationJSON.object(); DataConversion conversions = new DataConversion(); // send component model (when linked to foundset only props that are not record related) childComponent.writeProperties(ChangesToJSONConverter.INSTANCE, destinationJSON, changes.content, changes.contentType, conversions); JSONUtils.writeClientConversions(destinationJSON, conversions); destinationJSON.endObject(); } if (viewPortChanged) { // something in the viewport containing per-record component property values changed - send updates if (viewPortChangeMonitor.shouldSendWholeViewport()) { writeWholeViewportToJSON(destinationJSON); } else // viewPortChanges.size() > 0 { List<RowData> viewPortChanges = viewPortChangeMonitor.getViewPortChanges(); DataConversion clientConversionInfo = new DataConversion(); clientConversionInfo.pushNode(ComponentPropertyType.MODEL_VIEWPORT_CHANGES_KEY); destinationJSON.key(ComponentPropertyType.MODEL_VIEWPORT_CHANGES_KEY).array(); for (int i = 0; i < viewPortChanges.size(); i++) { clientConversionInfo.pushNode(String.valueOf(i)); viewPortChanges.get(i).writeJSONContent(destinationJSON, null, FullValueToJSONConverter.INSTANCE, clientConversionInfo); clientConversionInfo.popNode(); } clientConversionInfo.popNode(); destinationJSON.endArray(); // conversion info for websocket traffic (for example Date objects will turn into long) JSONUtils.writeClientConversions(destinationJSON, clientConversionInfo); } viewPortChangeMonitor.clearChanges(); } if (modelChanged || viewPortChanged) { destinationJSON.endObject(); } else { // no change yet we are still asked to send changes (so not full value); send a dummy NO_OP destinationJSON.key(NO_OP).value(true); } destinationJSON.endObject(); return destinationJSON; } protected void writeWholeViewportToJSON(JSONWriter destinationJSON) throws JSONException { if (forFoundsetTypedPropertyName != null) { FoundsetTypeViewport foundsetPropertyViewPort = getFoundsetValue().getViewPort(); DataConversion clientConversionInfo = new DataConversion(); destinationJSON.key(ComponentPropertyType.MODEL_VIEWPORT_KEY); clientConversionInfo.pushNode(ComponentPropertyType.MODEL_VIEWPORT_KEY); viewPortChangeMonitor.getRowDataProvider().writeRowData(foundsetPropertyViewPort.getStartIndex(), foundsetPropertyViewPort.getStartIndex() + foundsetPropertyViewPort.getSize() - 1, getFoundsetValue().getFoundset(), destinationJSON, clientConversionInfo); clientConversionInfo.popNode(); viewPortChangeMonitor.clearChanges(); // conversion info for websocket traffic (for example Date objects will turn into long) JSONUtils.writeClientConversions(destinationJSON, clientConversionInfo); } } /** * Writes the entire value of this property as JSON. This includes the template values, not just the runtime component properties. * This is currently needed and can get called if the property is nested inside other complex properties (json object/array) that sometimes * might want/need to send again the entire content. */ public JSONWriter fullToJSON(final JSONWriter writer, DataConversion conversionMarkers, ComponentPropertyType componentPropertyType) throws JSONException { 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 FormElement fe = formElementValue.element; writer.object(); // get template model values final TypedData<Map<String, Object>> formElementProperties = fe.propertiesForTemplateJSON(); // we'll need to update them with runtime values final TypedData<Map<String, Object>> runtimeProperties = childComponent.getProperties(); childComponent.getAndClearChanges(); // just for clear // add to useful properties only those formElement properties that didn't get overriden at runtime (so form element value is still used) boolean templateValuesRemoved = false; Iterator<Entry<String, Object>> formElementPropertyIterator = formElementProperties.content.entrySet().iterator(); while (formElementPropertyIterator.hasNext()) { Entry<String, Object> fePropEntry = formElementPropertyIterator.next(); if (runtimeProperties.content.containsKey(fePropEntry.getKey())) { // it has a non-default runtime value; so template value will be ignored/not sent if (!templateValuesRemoved) { formElementProperties.content = new HashMap<String, Object>(formElementProperties.content); // otherwise it's unmodifiable templateValuesRemoved = true; } formElementProperties.content.remove(fePropEntry.getKey()); } } removeRecordDependentProperties(runtimeProperties); removeRecordDependentProperties(formElementProperties); final FormElementContext formElementContext = new FormElementContext(fe); componentPropertyType.writeTemplateJSONContent(writer, formElementValue, forFoundsetTypedPropertyName, formElementContext, new IModelWriter() { @Override public void writeComponentModel() throws JSONException { writer.object(); DataConversion dataConversion = new DataConversion(); JSONUtils.writeData(FormElementToJSON.INSTANCE, writer, formElementProperties.content, formElementProperties.contentType, dataConversion, formElementContext); childComponent.writeProperties(JSONUtils.FullValueToJSONConverter.INSTANCE, writer, runtimeProperties.content, runtimeProperties.contentType, dataConversion); JSONUtils.writeClientConversions(writer, dataConversion); writer.endObject(); } }, recordBasedProperties, false); recordBasedPropertiesChanged = false; writeWholeViewportToJSON(writer); if (viewPortChangeMonitor != null) viewPortChangeMonitor.clearChanges(); writer.endObject(); return writer; } protected void removeRecordDependentProperties(TypedData<Map<String, Object>> changes) { // if the components property type is not linked to a foundset then the dataproviders/tagstring must also be sent when needed // but if it is linked to a foundset those should only be sent through the viewport if (forFoundsetTypedPropertyName != null) { // remove properties that are per record basis from the "per all model" for (String propertyName : recordBasedProperties) { try { changes.content.remove(propertyName); } catch (UnsupportedOperationException e) { changes.content = new HashMap<String, Object>(changes.content); changes.content.remove(propertyName); } if (changes.contentType != null) changes.contentType.putProperty(propertyName, null); } } } public void browserUpdatesReceived(Object jsonValue) { if (childComponent == null) return; try { JSONArray updates = (JSONArray)jsonValue; for (int i = 0; i < updates.length(); i++) { JSONObject update = (JSONObject)updates.get(i); if (update.has("handlerExec")) { // { handlerExec: { // eventType: ..., // args: ..., // rowId : ... // }}); update = update.getJSONObject("handlerExec"); if (update.has("eventType")) { boolean selectionOk = true; if (update.has("rowId")) { String rowId = update.optString("rowId"); if (rowId != null) { FoundsetTypeSabloValue foundsetValue = getFoundsetValue(); if (foundsetValue != null) { if (!foundsetValue.setEditingRowByPkHash(rowId)) { Debug.error("Cannot select row when event was fired; row identifier: " + rowId); selectionOk = false; } } } } if (selectionOk) { String eventType = update.getString("eventType"); // String beanName = update.getString("beanName"); JSONArray jsargs = update.getJSONArray("args"); Object[] args = new Object[jsargs == null ? 0 : jsargs.length()]; for (int j = 0; jsargs != null && j < jsargs.length(); j++) { args[j] = jsargs.get(j); } childComponent.executeEvent(eventType, args); // TODO HANDLE RETURN VALUE } } } else if (update.has("propertyChanges")) { // { propertyChanges : { // prop1: ..., // prop2: ... // }} JSONObject changes = update.getJSONObject("propertyChanges"); Iterator<String> keys = changes.keys(); while (keys.hasNext()) { String key = keys.next(); Object object = changes.get(key); childComponent.putBrowserProperty(key, object); } } else if (update.has(ViewportDataChangeMonitor.VIEWPORT_CHANGED)) { // component is linked to a foundset and the value of a property that depends on the record changed client side; // in this case update DataAdapterList with the correct record and then set the value on the component FoundsetTypeSabloValue foundsetPropertyValue = getFoundsetValue(); if (foundsetPropertyValue != null && foundsetPropertyValue.getFoundset() != null) { JSONObject change = update.getJSONObject(ViewportDataChangeMonitor.VIEWPORT_CHANGED); String rowIDValue = change.getString(FoundsetTypeSabloValue.ROW_ID_COL_KEY); String propertyName = change.getString(FoundsetTypeSabloValue.DATAPROVIDER_KEY); Object value = change.get(FoundsetTypeSabloValue.VALUE_KEY); updatePropertyValueForRecord(foundsetPropertyValue, rowIDValue, propertyName, value); } else { Debug.error("Component updates received for record linked property, but component is not linked to a foundset: " + update.get(ViewportDataChangeMonitor.VIEWPORT_CHANGED)); } } else if (update.has("svyApply")) { // { svyApply: { // rowId: rowId, // only when linked to foundset // propertyName: property, // propertyValue: propertyValue // }} JSONObject changeAndApply = update.getJSONObject("svyApply"); String propertyName = changeAndApply.getString(ComponentPropertyType.PROPERTY_NAME_KEY); Object value = changeAndApply.get(ComponentPropertyType.VALUE_KEY); IDataAdapterList dal; try { if (forFoundsetTypedPropertyName != null && recordBasedProperties.contains(propertyName)) { // changes component record and sets value String rowIDValue = changeAndApply.getString(FoundsetTypeSabloValue.ROW_ID_COL_KEY); changesWhileUpdatingFoundsetBasedDPFromClient = new ArrayList<>(); // we prevent a fromJSON on the dataprovider value that triggers valueChanged (so propertyFlaggedAsDirty) to re-apply (old) record values to DPs (effectively reverting the new value) // this can happen for example with integer DPs that get a double value from the browser and they round/trunc thus need to resend the value to client // we will execute the propertyFlaggedAsDirty code later, after DP value was applied // TODO shouldn't we apply in one go? so apply directly the value to record instead of setting it first in the component DP property? updatePropertyValueForRecord(getFoundsetValue(), rowIDValue, propertyName, value); dal = getFoundsetValue().getDataAdapterList(); } else { childComponent.putBrowserProperty(propertyName, value); IWebFormUI formUI = parentComponent.findParent(IWebFormUI.class); dal = formUI.getDataAdapterList(); } // apply change to record/dp dal.pushChanges(childComponent, propertyName); } finally { if (changesWhileUpdatingFoundsetBasedDPFromClient != null) { for (Runnable r : changesWhileUpdatingFoundsetBasedDPFromClient) r.run(); changesWhileUpdatingFoundsetBasedDPFromClient = null; } } } else if (update.has("svyStartEdit")) { // { svyStartEdit: { // rowId: rowId, // only if linked to foundset // propertyName: property // }} JSONObject startEditData = update.getJSONObject("svyStartEdit"); String propertyName = startEditData.getString(ComponentPropertyType.PROPERTY_NAME_KEY); IDataAdapterList dal; if (forFoundsetTypedPropertyName != null && recordBasedProperties.contains(propertyName)) { String rowIDValue = startEditData.getString(FoundsetTypeSabloValue.ROW_ID_COL_KEY); IFoundSetInternal foundset = getFoundsetValue().getFoundset(); dal = getFoundsetValue().getDataAdapterList(); Pair<String, Integer> splitHashAndIndex = FoundsetTypeSabloValue.splitPKHashAndIndex(rowIDValue); if (foundset != null) { int recordIndex = foundset.getRecordIndex(splitHashAndIndex.getLeft(), splitHashAndIndex.getRight().intValue()); if (recordIndex != -1) { ((FoundsetDataAdapterList)dal).setRecordQuietly(foundset.getRecord(recordIndex)); } else { Debug.error("Cannot find record for foundset linked record dependent component property - startEdit (" + rowIDValue + "); property '" + propertyName, new RuntimeException()); } } else { Debug.error("Foundset is null while trying to startEdit for foundset linked record dependent component property (" + rowIDValue + "); property '" + propertyName, new RuntimeException()); } } else { IWebFormUI formUI = parentComponent.findParent(IWebFormUI.class); dal = formUI.getDataAdapterList(); } dal.startEdit(childComponent, propertyName); } } } catch (Exception ex) { Debug.error(ex); } } protected void updatePropertyValueForRecord(FoundsetTypeSabloValue foundsetPropertyValue, String rowIDValue, String propertyName, Object value) { IFoundSetInternal foundset = foundsetPropertyValue.getFoundset(); Pair<String, Integer> splitHashAndIndex = FoundsetTypeSabloValue.splitPKHashAndIndex(rowIDValue); if (foundset != null) { int recordIndex = foundset.getRecordIndex(splitHashAndIndex.getLeft(), splitHashAndIndex.getRight().intValue()); if (recordIndex != -1) { foundsetPropertyValue.getDataAdapterList().setRecordQuietly(foundset.getRecord(recordIndex)); viewPortChangeMonitor.pauseRowUpdateListener(splitHashAndIndex.getLeft()); try { childComponent.putBrowserProperty(propertyName, value); } catch (JSONException e) { Debug.error( "Setting value for record dependent property '" + propertyName + "' in foundset linked component to value: " + value + " failed.", e); } finally { viewPortChangeMonitor.resumeRowUpdateListener(); } } else { Debug.error("Cannot set foundset linked record dependent component property for (" + rowIDValue + ") property '" + propertyName + "' to value '" + value + "' of component: " + childComponent + ". Record not found.", new RuntimeException()); } } else { Debug.error("Cannot set foundset linked record dependent component property for (" + rowIDValue + ") property '" + propertyName + "' to value '" + value + "' of component: " + childComponent + ". Foundset is null.", new RuntimeException()); } } public Object getRuntimeComponent() { if (childComponent != null) { return new RuntimeWebComponent(childComponent, childComponent.getSpecification()); } return null; } protected final class ComponentDataLinkedPropertyListener implements IDataLinkedPropertyRegistrationListener { private final Map<IDataLinkedPropertyValue, String> oldDataLinkedValuesToRootPropertyName = new HashMap<IDataLinkedPropertyValue, String>(); private List<IDataLinkedPropertyValue> initiallyAddedValuesWhileComponentIsNull = new ArrayList<IDataLinkedPropertyValue>(); @Override public void dataLinkedPropertyRegistered(IDataLinkedPropertyValue propertyValue, TargetDataLinks targetDataLinks) { if (targetDataLinks != TargetDataLinks.NOT_LINKED_TO_DATA && targetDataLinks.recordLinked) { if (childComponent != null) { recordLinkedPropAdded(propertyValue); } else { initiallyAddedValuesWhileComponentIsNull.add(propertyValue); } } } protected void recordLinkedPropAdded(IDataLinkedPropertyValue propertyValue) { String propertyName = findComponentPropertyName(propertyValue); if (propertyName != null) { oldDataLinkedValuesToRootPropertyName.put(propertyValue, propertyName); if (!recordBasedProperties.contains(propertyName)) { recordBasedProperties.add(propertyName); recordBasedPropertiesChanged = true; recordBasedPropertiesChangedComparedToTemplate = true; monitor.valueChanged(); } } } @Override public void dataLinkedPropertyUnregistered(IDataLinkedPropertyValue propertyValue) { if (childComponent != null) { // when this gets called the component property value is probably already changed // as usually data linked property values unregister themselves in detach(); // so we use this map to find the rootPropertyName if it's a value of this child component String propertyName = oldDataLinkedValuesToRootPropertyName.remove(propertyValue); if (propertyName != null && recordBasedProperties.remove(propertyName)) { recordBasedPropertiesChanged = true; recordBasedPropertiesChangedComparedToTemplate = true; monitor.valueChanged(); } } else { initiallyAddedValuesWhileComponentIsNull.remove(propertyValue); } } protected void componentIsNowAvailable() { for (IDataLinkedPropertyValue v : initiallyAddedValuesWhileComponentIsNull) recordLinkedPropAdded(v); initiallyAddedValuesWhileComponentIsNull = null; } } }