/* 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.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import org.sablo.BaseWebObject; import org.sablo.IChangeListener; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectSpecification.PushToServerEnum; import org.sablo.specification.property.BrowserConverterContext; import org.sablo.specification.property.IBrowserConverterContext; import org.sablo.specification.property.IPushToServerSpecialType; import org.sablo.util.ValueReference; 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.server.ngclient.INGFormElement; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.property.FoundsetTypeChangeMonitor.RowData; import com.servoy.j2db.server.ngclient.property.types.IDataLinkedType.TargetDataLinks; import com.servoy.j2db.server.ngclient.property.types.NGConversions; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.UUID; /** * Property value for {@link FoundsetLinkedPropertyType}. * * @author acostescu */ public class FoundsetLinkedTypeSabloValue<YF, YT> implements IDataLinkedPropertyValue { /** * Non-record linked property change received from client... */ protected static final String PROPERTY_CHANGE = "propertyChange"; protected static final String PUSH_TO_SERVER = "w"; protected static final String ID_FOR_FOUNDSET = "idForFoundset"; /** * When non-null then the wrapped property is not yet initialized - waiting for forFoundset property's DAL to be available */ protected InitializingState initializingState; protected YT wrappedSabloValue; protected WebFormComponent component; protected final String forFoundsetPropertyName; protected PropertyChangeListener forFoundsetPropertyListener; protected IDataLinkedPropertyRegistrationListener dataLinkedPropertyRegistrationListener; protected IChangeListener changeMonitor; protected String idForFoundset; protected boolean idForFoundsetChanged = false; protected ViewportDataChangeMonitor<FoundsetLinkedViewportRowDataProvider<YF, YT>> viewPortChangeMonitor; protected class InitializingState { protected final YF formElementValue; protected final INGFormElement formElement; protected final PropertyDescription wrappedPropertyDescription; public InitializingState(PropertyDescription wrappedPropertyDescription, YF formElementValue, INGFormElement formElement) { this.wrappedPropertyDescription = wrappedPropertyDescription; this.formElementValue = formElementValue; this.formElement = formElement; } } /** * Called when we already know the wrapped value (probably default value...) */ public FoundsetLinkedTypeSabloValue(YT wrappedSabloValue, String forFoundsetPropertyName) { this.wrappedSabloValue = wrappedSabloValue; this.forFoundsetPropertyName = forFoundsetPropertyName; // initializingState = null; // it is null by default, just mentioning it } public FoundsetLinkedTypeSabloValue(String forFoundsetPropertyName, YF formElementValue, PropertyDescription wrappedPropertyDescription, INGFormElement formElement, WebFormComponent component) { initializingState = new InitializingState(wrappedPropertyDescription, formElementValue, formElement); this.component = component; this.forFoundsetPropertyName = forFoundsetPropertyName; // this.wrappedSabloValue = null; // for now; waiting for foundset property availability } private FoundsetTypeSabloValue getFoundsetValue() { if (component != null) { return (FoundsetTypeSabloValue)component.getProperty(forFoundsetPropertyName); } return null; } protected void createWrappedSabloValueNeededAndPossible() { if (initializingState == null) return; final FoundsetTypeSabloValue foundsetPropValue = getFoundsetValue(); if (foundsetPropValue == null) return; FoundsetDataAdapterList dal = foundsetPropValue.getDataAdapterList(); idForFoundset = null; dal.addDataLinkedPropertyRegistrationListener(getDataLinkedPropertyRegistrationListener(changeMonitor, foundsetPropValue)); this.wrappedSabloValue = (YT)NGConversions.INSTANCE.convertFormElementToSabloComponentValue(initializingState.formElementValue, initializingState.wrappedPropertyDescription, initializingState.formElement, component, dal); // this conversion also adds dal data listeners when needed and will trigger dataLinkedPropertyRegistrationListener above which updates the record-linked or not state if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).attachToBaseObject(changeMonitor, component); initializingState = null; } @Override public void attachToBaseObject(final IChangeListener monitor, @SuppressWarnings("hiding") BaseWebObject component) { this.component = (WebFormComponent)component; this.changeMonitor = monitor; createWrappedSabloValueNeededAndPossible(); this.component.addPropertyChangeListener(forFoundsetPropertyName, forFoundsetPropertyListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() != null) createWrappedSabloValueNeededAndPossible(); } }); } @Override public void detach() { if (forFoundsetPropertyListener != null) { component.removePropertyChangeListener(forFoundsetPropertyName, forFoundsetPropertyListener); FoundsetTypeSabloValue foundsetPropValue = getFoundsetValue(); if (foundsetPropValue != null && viewPortChangeMonitor != null) { foundsetPropValue.removeViewportDataChangeMonitor(viewPortChangeMonitor); } } if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).detach(); } protected IDataLinkedPropertyRegistrationListener getDataLinkedPropertyRegistrationListener(final IChangeListener changeMonitor, final FoundsetTypeSabloValue foundsetPropValue) { final PropertyDescription pd = initializingState.wrappedPropertyDescription; return dataLinkedPropertyRegistrationListener = new IDataLinkedPropertyRegistrationListener() { @Override public void dataLinkedPropertyRegistered(IDataLinkedPropertyValue propertyValue, TargetDataLinks targetDataLinks) { if (wrappedSabloValue == propertyValue && targetDataLinks != TargetDataLinks.NOT_LINKED_TO_DATA && targetDataLinks.recordLinked) { // wrapped property is record linked so we need to send viewports to client // this could be the result of initialization or it could for example get changed from Rhino boolean changed = (viewPortChangeMonitor == null); if (viewPortChangeMonitor != null) foundsetPropValue.removeViewportDataChangeMonitor(viewPortChangeMonitor); viewPortChangeMonitor = new ViewportDataChangeMonitor<>(changeMonitor, new FoundsetLinkedViewportRowDataProvider<YF, YT>(foundsetPropValue.getDataAdapterList(), pd, FoundsetLinkedTypeSabloValue.this)); foundsetPropValue.addViewportDataChangeMonitor(viewPortChangeMonitor); // register the first dataprovider used by the wrapped property to the foundset for sorting if (idForFoundset == null /* the rest of the condition should always be true */ && targetDataLinks.dataProviderIDs != null && targetDataLinks.dataProviderIDs.length > 0) { idForFoundset = UUID.randomUUID().toString(); idForFoundsetChanged = true; foundsetPropValue.setRecordDataLinkedPropertyIDToColumnDP(idForFoundset, targetDataLinks.dataProviderIDs[0]); } if (changed) changeMonitor.valueChanged(); } // else we will send single value to client as it is not record dependent and the client can just duplicate that to match foundset viewport size } @Override public void dataLinkedPropertyUnregistered(IDataLinkedPropertyValue propertyValue) { if (wrappedSabloValue == propertyValue && viewPortChangeMonitor != null) { // wrapped property is now no longer record linked so we only send one value to be duplicated // this could be the result of initialization or it could for example get changed from Rhino getFoundsetValue().removeViewportDataChangeMonitor(viewPortChangeMonitor); if (idForFoundset != null) { foundsetPropValue.setRecordDataLinkedPropertyIDToColumnDP(idForFoundset, null); idForFoundset = null; idForFoundsetChanged = true; } viewPortChangeMonitor = null; changeMonitor.valueChanged(); } } }; } @Override public void dataProviderOrRecordChanged(IRecordInternal record, String dataProvider, boolean isFormDP, boolean isGlobalDP, boolean fireChangeEvent) { if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).dataProviderOrRecordChanged(record, dataProvider, isFormDP, isGlobalDP, fireChangeEvent); } protected YT getWrappedValue() { return wrappedSabloValue; } public void rhinoToSablo(Object rhinoValue, PropertyDescription wrappedPropertyDescription, BaseWebObject componentOrService) { YT newWrappedVal; newWrappedVal = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(rhinoValue, wrappedSabloValue, wrappedPropertyDescription, componentOrService); if (newWrappedVal != wrappedSabloValue) { // do what component would do when a property changed // TODO should we make current method return a completely new instance instead and leave component code do the rest? if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).detach(); wrappedSabloValue = newWrappedVal; if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).attachToBaseObject(changeMonitor, componentOrService); } } public JSONWriter fullToJSON(JSONWriter writer, String key, DataConversion clientConversion, PropertyDescription wrappedPropertyDescription, IBrowserConverterContext dataConverterContext) throws JSONException { if (initializingState != null) { Debug.warn("Trying to get full value from an uninitialized foundset linked property: " + wrappedPropertyDescription); return writer; } clientConversion.convert(FoundsetLinkedPropertyType.CONVERSION_NAME); JSONUtils.addKeyIfPresent(writer, key); writer.object(); writer.key(FoundsetLinkedPropertyType.FOR_FOUNDSET_PROPERTY_NAME).value(forFoundsetPropertyName); if (idForFoundset != null) writer.key(ID_FOR_FOUNDSET).value(idForFoundset == null ? JSONObject.NULL : idForFoundset); PushToServerEnum pushToServer = BrowserConverterContext.getPushToServerValue(dataConverterContext); if (pushToServer == PushToServerEnum.shallow || pushToServer == PushToServerEnum.deep) { writer.key(PUSH_TO_SERVER).value(pushToServer == PushToServerEnum.shallow ? false : true); } if (viewPortChangeMonitor == null) { // single value; not record dependent DataConversion dataConversions = new DataConversion(); dataConversions.pushNode(FoundsetLinkedPropertyType.SINGLE_VALUE); FullValueToJSONConverter.INSTANCE.toJSONValue(writer, FoundsetLinkedPropertyType.SINGLE_VALUE, wrappedSabloValue, wrappedPropertyDescription, dataConversions, dataConverterContext); JSONUtils.writeClientConversions(writer, dataConversions); } else { viewPortChangeMonitor.getRowDataProvider().initializeIfNeeded(dataConverterContext); // record dependent; viewport value writeWholeViewportToJSON(writer); viewPortChangeMonitor.clearChanges(); } writer.endObject(); return writer; } protected void writeWholeViewportToJSON(JSONWriter destinationJSON) throws JSONException { FoundsetTypeViewport foundsetPropertyViewPort = getFoundsetValue().getViewPort(); DataConversion clientConversionInfo = new DataConversion(); destinationJSON.key(FoundsetLinkedPropertyType.VIEWPORT_VALUE); clientConversionInfo.pushNode(FoundsetLinkedPropertyType.VIEWPORT_VALUE); viewPortChangeMonitor.getRowDataProvider().writeRowData(foundsetPropertyViewPort.getStartIndex(), foundsetPropertyViewPort.getStartIndex() + foundsetPropertyViewPort.getSize() - 1, getFoundsetValue().getFoundset(), destinationJSON, clientConversionInfo); clientConversionInfo.popNode(); // conversion info for websocket traffic (for example Date objects will turn into long) JSONUtils.writeClientConversions(destinationJSON, clientConversionInfo); } public JSONWriter changesToJSON(JSONWriter writer, String key, DataConversion clientConversion, PropertyDescription wrappedPropertyDescription, IBrowserConverterContext dataConverterContext) throws JSONException { if (initializingState != null) { Debug.warn("Trying to get changes from an uninitialized foundset linked property: " + wrappedPropertyDescription); return writer; } clientConversion.convert(FoundsetLinkedPropertyType.CONVERSION_NAME); JSONUtils.addKeyIfPresent(writer, key); writer.object(); if (idForFoundsetChanged) { writer.key(ID_FOR_FOUNDSET).value(idForFoundset == null ? JSONObject.NULL : idForFoundset); } if (viewPortChangeMonitor == null) { // single value; just send it's changes DataConversion dataConversions = new DataConversion(); dataConversions.pushNode(FoundsetLinkedPropertyType.SINGLE_VALUE_UPDATE); ChangesToJSONConverter.INSTANCE.toJSONValue(writer, FoundsetLinkedPropertyType.SINGLE_VALUE_UPDATE, wrappedSabloValue, wrappedPropertyDescription, dataConversions, dataConverterContext); JSONUtils.writeClientConversions(writer, dataConversions); } else { viewPortChangeMonitor.getRowDataProvider().initializeIfNeeded(dataConverterContext); if (viewPortChangeMonitor.shouldSendWholeViewport()) { writeWholeViewportToJSON(writer); } else if (viewPortChangeMonitor.getViewPortChanges().size() > 0) { DataConversion clientConversionInfo = new DataConversion(); writer.key(FoundsetLinkedPropertyType.VIEWPORT_VALUE_UPDATE); clientConversionInfo.pushNode(FoundsetLinkedPropertyType.VIEWPORT_VALUE_UPDATE); List<RowData> viewPortChanges = viewPortChangeMonitor.getViewPortChanges(); writer.array(); for (int i = 0; i < viewPortChanges.size(); i++) { clientConversionInfo.pushNode(String.valueOf(i)); viewPortChanges.get(i).writeJSONContent(writer, null, FullValueToJSONConverter.INSTANCE, clientConversionInfo); clientConversionInfo.popNode(); } writer.endArray(); clientConversionInfo.popNode(); // conversion info for websocket traffic (for example Date objects will turn into long) JSONUtils.writeClientConversions(writer, clientConversionInfo); } // else there is no change to send! viewPortChangeMonitor.clearChanges(); } writer.endObject(); return writer; } public void browserUpdatesReceived(Object newJSONValue, PropertyDescription wrappedPropertyDescription, PropertyDescription pd, IBrowserConverterContext dataConverterContext, ValueReference<Boolean> returnValueAdjustedIncommingValue) { PushToServerEnum pushToServer = BrowserConverterContext.getPushToServerValue(dataConverterContext); if (initializingState != null) { Debug.error("Trying to update state for an uninitialized foundset linked property: " + wrappedPropertyDescription + " | " + component); return; } if ((wrappedPropertyDescription instanceof IPushToServerSpecialType && ((IPushToServerSpecialType)wrappedPropertyDescription).shouldAlwaysAllowIncommingJSON()) || PushToServerEnum.allow.compareTo(pushToServer) <= 0) { try { JSONArray updates = (JSONArray)newJSONValue; for (int i = 0; i < updates.length(); i++) { JSONObject update = (JSONObject)updates.get(i); if (update.has(PROPERTY_CHANGE)) { // for when property is not record dependent // { propertyChange : propValue } if (viewPortChangeMonitor != null) { Debug.error("Trying to update single state value for a foundset linked record dependent property: " + wrappedPropertyDescription + " | " + component); return; } Object object = update.get(PROPERTY_CHANGE); YT newWrappedValue = (YT)JSONUtils.fromJSONUnwrapped(wrappedSabloValue, object, wrappedPropertyDescription, dataConverterContext, returnValueAdjustedIncommingValue); if (newWrappedValue != wrappedSabloValue) { // do what component would do when a property changed // TODO should we make current method return a completely new instance instead and leave component code do the rest? if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).detach(); wrappedSabloValue = newWrappedValue; if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).attachToBaseObject(changeMonitor, component); } } else if (update.has(ViewportDataChangeMonitor.VIEWPORT_CHANGED)) { if (viewPortChangeMonitor == null) { Debug.error("Trying to update some record value for a foundset linked non-record dependent property: " + wrappedPropertyDescription + " | " + component); return; } // property 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 wrapped property 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); Object value = change.get(FoundsetTypeSabloValue.VALUE_KEY); updatePropertyValueForRecord(foundsetPropertyValue, rowIDValue, value, wrappedPropertyDescription, dataConverterContext); } else { Debug.error("Component updates received for record linked property, but component is not linked to a foundset: " + update.get(ViewportDataChangeMonitor.VIEWPORT_CHANGED)); } } } } catch (Exception ex) { Debug.error(ex); } } else { Debug.error("Foundset linked property (" + pd + ") that doesn't define a suitable pushToServer value (allow/shallow/deep) tried to update proxied value(s) serverside. Denying and sending back server value!"); if (viewPortChangeMonitor != null) viewPortChangeMonitor.shouldSendWholeViewport(); else changeMonitor.valueChanged(); } } protected void updatePropertyValueForRecord(FoundsetTypeSabloValue foundsetPropertyValue, String rowIDValue, Object value, PropertyDescription wrappedPropertyDescription, IBrowserConverterContext dataConverterContext) { 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 { ValueReference<Boolean> returnValueAdjustedIncommingValueForRow = new ValueReference<Boolean>(Boolean.FALSE); YT newWrappedValue = (YT)JSONUtils.fromJSONUnwrapped(wrappedSabloValue, value, wrappedPropertyDescription, dataConverterContext, returnValueAdjustedIncommingValueForRow); if (newWrappedValue != wrappedSabloValue) { // do what component would do when a property changed // TODO should we make current method return a completely new instance instead and leave component code do the rest? if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).detach(); wrappedSabloValue = newWrappedValue; if (wrappedSabloValue instanceof IDataLinkedPropertyValue) ((IDataLinkedPropertyValue)wrappedSabloValue).attachToBaseObject(changeMonitor, component); // the full value has changed; the whole viewport might be affected viewPortChangeMonitor.viewPortCompletelyChanged(); } else if (returnValueAdjustedIncommingValueForRow.value.booleanValue()) { FoundsetTypeViewport viewPort = foundsetPropertyValue.getViewPort(); int firstViewPortIndex = Math.max(viewPort.getStartIndex(), recordIndex); int lastViewPortIndex = Math.min(viewPort.getStartIndex() + viewPort.getSize() - 1, recordIndex); if (firstViewPortIndex <= lastViewPortIndex) { viewPortChangeMonitor.queueOperation(firstViewPortIndex - viewPort.getStartIndex(), lastViewPortIndex - viewPort.getStartIndex(), firstViewPortIndex, lastViewPortIndex, foundsetPropertyValue.getFoundset(), RowData.CHANGE); } } } catch (JSONException e) { Debug.error("Setting value for record dependent property '" + wrappedPropertyDescription + "' in foundset linked component to value: " + value + " failed.", e); } finally { viewPortChangeMonitor.resumeRowUpdateListener(); } } else { Debug.error("Cannot set foundset linked record dependent property for (" + rowIDValue + ") property '" + wrappedPropertyDescription + "' to value '" + value + "' of component: " + component + ". Record not found.", new RuntimeException()); } } else { Debug.error("Cannot set foundset linked record dependent property for (" + rowIDValue + ") property '" + wrappedPropertyDescription + "' to value '" + value + "' of component: " + component + ". Foundset is null.", new RuntimeException()); } } }