/*
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 org.json.JSONException;
import org.json.JSONWriter;
import org.sablo.IChangeListener;
import org.sablo.specification.property.IBrowserConverterContext;
import org.sablo.websocket.IToJSONWriter;
import org.sablo.websocket.utils.DataConversion;
import org.sablo.websocket.utils.JSONUtils;
import org.sablo.websocket.utils.JSONUtils.IJSONStringWithConversions;
import org.sablo.websocket.utils.JSONUtils.IToJSONConverter;
/**
* This class is responsible for keeping track of what changes need to be sent to the client (whole thing, selection changes, viewport idx/size change, row data changes...)
*
* @author acostescu
*/
public class FoundsetTypeChangeMonitor
{
/**
* The whole foundset property needs to get sent to the client.
*/
protected static final int SEND_ALL = 0b000000001;
/**
* Only the bounds of the viewPort changed, data is the same; for example records were added/removed before startIndex of viewPort,
* or even inside the viewPort but will be combined by incremental updates (adds/deletes).
*/
protected static final int SEND_VIEWPORT_BOUNDS = 0b000000010;
/**
* Foundset size changed (add/remove of records).
*/
protected static final int SEND_FOUNDSET_SIZE = 0b000001000;
protected static final int SEND_SELECTED_INDEXES = 0b000010000;
protected static final int SEND_SELECTION_DENIED = 0b000100000;
protected static final int SEND_SELECTION_ACCEPTED = 0b001000000;
protected static final int SEND_COLUMN_FORMATS = 0b010000000;
protected static final int SEND_HAD_MORE_ROWS = 0b100000000;
protected boolean lastHadMoreRecords = false;
protected IChangeListener changeNotifier;
protected int changeFlags = 0;
protected final ViewportDataChangeMonitor<FoundsetTypeRowDataProvider> viewPortDataChangeMonitor;
protected final List<ViewportDataChangeMonitor< ? >> viewPortDataChangeMonitors = new ArrayList<>();
protected final FoundsetTypeSabloValue propertyValue; // TODO when we implement merging foundset events based on indexes, data will no longer be needed and this member can be removed
public FoundsetTypeChangeMonitor(FoundsetTypeSabloValue propertyValue, FoundsetTypeRowDataProvider rowDataProvider)
{
this.propertyValue = propertyValue;
viewPortDataChangeMonitor = new ViewportDataChangeMonitor<>(null, rowDataProvider);
addViewportDataChangeMonitor(viewPortDataChangeMonitor);
}
/**
* Called when the foundSet selection request from the client was accepted by the server.
*/
public void selectionAccepted()
{
if (!shouldSendAll())
{
int oldChangeFlags = changeFlags;
changeFlags = changeFlags | SEND_SELECTION_ACCEPTED;
if (oldChangeFlags != changeFlags) notifyChange();
}
}
/**
* Called when the foundSet selection request from the client was denied by the server.
*/
public void selectionDenied()
{
if (!shouldSendAll())
{
int oldChangeFlags = changeFlags;
changeFlags = changeFlags | SEND_SELECTION_DENIED;
if (oldChangeFlags != changeFlags) notifyChange();
}
}
/**
* Called when the foundSet selection needs to be re-sent to client.
*/
public void selectionChanged()
{
if (!shouldSendAll())
{
int oldChangeFlags = changeFlags;
changeFlags = changeFlags | SEND_SELECTED_INDEXES;
if (oldChangeFlags != changeFlags) notifyChange();
}
}
/**
* The foundset's size changed.
* This doesn't notify changes as this is probably part of a larger check which could result in more changes. Notification must be handled by caller.
*/
protected void foundSetSizeChanged()
{
if (!shouldSendAll())
{
changeFlags = changeFlags | SEND_FOUNDSET_SIZE;
}
}
/**
* Called when the viewPort bounds need to be re-sent to client.<br/>
* Only the bounds of the viewPort changed, data is the same; for example records were added/removed before startIndex of viewPort.<br/><br/>
*
* This doesn't notify changes as this is probably part of a larger check which could result in more changes. Notification must be handled by caller.
*/
protected void viewPortBoundsOnlyChanged()
{
if (!shouldSendWholeViewPort() && !shouldSendAll())
{
changeFlags = changeFlags | SEND_VIEWPORT_BOUNDS;
}
}
/**
* Called when viewPort bounds and data changed; for example client requested completely new viewport bounds.
*/
public void viewPortCompletelyChanged()
{
boolean changed = !viewPortDataChangeMonitor.shouldSendWholeViewport();
for (ViewportDataChangeMonitor vdcm : viewPortDataChangeMonitors)
{
vdcm.viewPortCompletelyChanged();
}
if (!shouldSendAll() && changed)
{
// clear all more granular changes as whole viewport will be sent
changeFlags = changeFlags & (~SEND_VIEWPORT_BOUNDS); // clear flag
notifyChange();
}
}
/**
* Called when all foundset info needs to be resent to client.
*/
public void allChanged()
{
int oldChangeFlags = changeFlags;
changeFlags = SEND_ALL; // clears all others as well
for (ViewportDataChangeMonitor vdcm : viewPortDataChangeMonitors)
{
vdcm.viewPortCompletelyChanged();
}
if (oldChangeFlags != changeFlags)
{
notifyChange();
}
}
/**
* Called for example when the used foundset instance changes (for example due to use of related foundset).
* In that case viewPort is set to 0, 0 (so that might have already triggered a notification), but also the server size can change then.
*/
public void newFoundsetSize()
{
int oldChangeFlags = changeFlags;
foundSetSizeChanged();
if (oldChangeFlags != changeFlags) notifyChange();
}
/**
* Called when the find mode changes on this foundset.
*/
public void findModeChanged(boolean newFindMode)
{
allChanged();
if (propertyValue.getDataAdapterList() != null) propertyValue.getDataAdapterList().setFindMode(newFindMode);
}
/**
* Called when the foundset is invalidated.
*/
public void foundsetInvalidated()
{
allChanged();
}
/**
* Called when the dataProviders that this foundset type provides changed.
*/
public void dataProvidersChanged()
{
// this normally happens only before initial send of initial form data so it isn't very useful; will we allow dataProviders to change later on?
if (propertyValue.viewPort.size > 0) allChanged();
}
public void recordsDeleted(int firstRow, int lastRow, FoundsetTypeViewport viewPort)
{
int oldChangeFlags = changeFlags;
boolean viewPortRecordChangesUpdated = false;
if (lastRow - firstRow >= 0) foundSetSizeChanged();
if (!shouldSendAll() && !shouldSendWholeViewPort())
{
int viewPortEndIdx = viewPort.getStartIndex() + viewPort.getSize() - 1;
int slideBy;
if (firstRow < viewPort.getStartIndex())
{
// this will adjust the viewPort startIndex (and size if needed)
slideBy = firstRow - Math.min(viewPort.getStartIndex(), lastRow + 1);
}
else
{
// this will adjust the viewPort size if needed (not enough records to insert in the viewPort to replace deleted ones)
slideBy = 0;
}
if (belongsToInterval(firstRow, viewPort.getStartIndex(), viewPortEndIdx) || belongsToInterval(lastRow, viewPort.getStartIndex(), viewPortEndIdx))
{
// first row to be deleted inside current viewPort
int firstRowDeletedInViewport = Math.max(viewPort.getStartIndex(), firstRow);
int lastRowDeletedInViewport = Math.min(viewPortEndIdx, lastRow);
int relativeFirstRow = firstRowDeletedInViewport - viewPort.getStartIndex();
// number of deletes from current viewPort
int relativeLastRow = lastRowDeletedInViewport - viewPort.getStartIndex();
int numberOfDeletes = lastRowDeletedInViewport - firstRowDeletedInViewport + 1;
// adjust viewPort bounds if necessary
// int oldViewPortStart = viewPort.getStartIndex();
int oldViewPortSize = viewPort.getSize();
// TODO merge changes with previous ones without keeping any actual data (indexes kept in a way should be enough) - implementation started below
// // ok, viewPort bounds are updated; update existing recordChange data if needed; we are working here a lot with viewPort relative indexes (both client side and server side ones)
// ListIterator<RecordChangeDescriptor> iterator = viewPortRecordChanges.listIterator();
// int browserViewPortIdxDelta = 0; // delta between the current client side viewPort data relative "i" index and the old server viewPort relative "i" index
// int toBeDeleted = relativeFirstRow;
// while (iterator.hasNext())
// {
// RecordChangeDescriptor recordChange = iterator.next();
// while (toBeDeleted <= relativeLastRow && toBeDeleted + browserViewPortIdxDelta < recordChange.relativeIndex)
// {
// // record deleted before previous Add/Remove/Update operation; add before
// iterator.add(new RecordChangeDescriptor(RecordChangeDescriptor.Types.REMOVE_FROM_VIEWPORT, browserViewPortIdxDelta + toBeDeleted));
// if (toBeDeleted + browserViewPortIdxDelta >= viewPort.getSize())
// {
//
// }
// toBeDeleted++;
// }
//
// switch (recordChange.type)
// {
// case REMOVE_FROM_VIEWPORT :
// browserViewPortIdxDelta++;
// break;
// case ADD_TO_VIEWPORT :
// // TODO
// break;
// case CHANGE :
// // TODO
// break;
// }
// }
// while (toBeDeleted <= relativeLastRow && )
// {
// // record deleted before previous Add/Remove/Update operation; add before
// iterator.add(new RecordChangeDescriptor(RecordChangeDescriptor.Types.REMOVE_FROM_VIEWPORT, browserViewPortIdxDelta + toBeDeleted));
// toBeDeleted++;
// }
viewPort.slideAndCorrect(slideBy);
viewPortEndIdx = viewPort.getStartIndex() + viewPort.getSize() - 1; // update
// add new records if available
// we need to replace same amount of records in current viewPort; append rows if available
for (ViewportDataChangeMonitor vpdcm : viewPortDataChangeMonitors)
{
vpdcm.queueOperation(relativeFirstRow, relativeLastRow, viewPort.getStartIndex() + oldViewPortSize - numberOfDeletes, viewPortEndIdx,
propertyValue.getFoundset(), RowData.DELETE);
}
viewPortRecordChangesUpdated = true;
}
else if (slideBy != 0)
{
viewPort.slideAndCorrect(slideBy);
}
}
else if (viewPort.getSize() > propertyValue.getFoundset().getSize())
{
// if it will already send the whole viewport then the size needs to be in sync with the foundset.
viewPort.correctAndSetViewportBoundsInternal(viewPort.getStartIndex(), viewPort.getSize());
}
if (oldChangeFlags != changeFlags || viewPortRecordChangesUpdated) notifyChange();
}
/**
* Deals with new records being inserted into the foundset.
* @param firstRow the first row of the insertion.
* @param lastRow the last row of the insertion.
* @param viewPort the current viewPort.
* @param viewPortExpandOnly if false, and records were inserted exactly at the viewPort start index, it will just slide the viewport;
* if true then records were requested for (so not actually inserted in the foundset, just a viewportExpand is happening) exactly
* at the viewPort start index, it will actually insert the new records in the view port.
*/
public void recordsInserted(int firstRow, int lastRow, FoundsetTypeViewport viewPort, boolean viewPortExpandOnly)
{
int oldChangeFlags = changeFlags;
boolean viewPortRecordChangesUpdated = false;
if (!viewPortExpandOnly && lastRow - firstRow >= 0) foundSetSizeChanged();
if (!shouldSendAll() && !shouldSendWholeViewPort())
{
int viewPortEndIdx = viewPort.getStartIndex() + viewPort.getSize() - 1;
if (viewPort.getStartIndex() <= (firstRow + (viewPortExpandOnly ? 1 : 0)) && firstRow <= viewPortEndIdx)
{
int lastViewPortInsert = Math.min(lastRow, viewPortEndIdx);
// add records that were inserted in viewPort
for (ViewportDataChangeMonitor vpdcm : viewPortDataChangeMonitors)
{
vpdcm.queueOperation(firstRow - viewPort.getStartIndex(), viewPort.getSize(), firstRow, lastViewPortInsert, propertyValue.getFoundset(),
RowData.INSERT); // for insert operations client needs to know the new viewport size so that it knows if it should delete records at the end or not; that is done by putting the 'size' in relativeLastRow
}
viewPortRecordChangesUpdated = true;
}
else if (viewPort.getStartIndex() > firstRow)
{
viewPort.slideAndCorrect(lastRow - firstRow + 1);
}
}
if (oldChangeFlags != changeFlags || viewPortRecordChangesUpdated) notifyChange();
}
public void recordsUpdated(int firstRow, int lastRow, int foundSetSize, FoundsetTypeViewport viewPort)
{
if (firstRow == 0 && lastRow == foundSetSize - 1)
{
if (viewPort.getSize() > 0) viewPortCompletelyChanged();
}
else
{
int oldChangeFlags = changeFlags;
boolean viewPortRecordChangesUpdated = false;
if ((propertyValue.getDataAdapterList() == null || !propertyValue.getDataAdapterList().isQuietRecordChangeInProgress()) && !shouldSendAll() &&
!shouldSendWholeViewPort())
{
// get the rows that are changed.
int firstViewPortIndex = Math.max(viewPort.getStartIndex(), firstRow);
int lastViewPortIndex = Math.min(viewPort.getStartIndex() + viewPort.getSize() - 1, lastRow);
if (firstViewPortIndex <= lastViewPortIndex)
{
for (ViewportDataChangeMonitor vpdcm : viewPortDataChangeMonitors)
{
vpdcm.queueOperation(firstViewPortIndex - viewPort.getStartIndex(), lastViewPortIndex - viewPort.getStartIndex(), firstViewPortIndex,
lastViewPortIndex, propertyValue.getFoundset(), RowData.CHANGE);
}
viewPortRecordChangesUpdated = true;
}
}
if (oldChangeFlags != changeFlags || viewPortRecordChangesUpdated) notifyChange();
}
}
protected boolean belongsToInterval(int x, int intervalStartInclusive, int intervalEndInclusive)
{
return intervalStartInclusive <= x && x <= intervalEndInclusive;
}
public boolean shouldSendAll()
{
return (changeFlags & SEND_ALL) != 0;
}
public boolean shouldSendSelectedIndexes()
{
return (changeFlags & SEND_SELECTED_INDEXES) != 0;
}
public boolean shouldSendSelectionAccepted()
{
return (changeFlags & SEND_SELECTION_ACCEPTED) != 0;
}
public boolean shouldSendSelectionDenied()
{
return (changeFlags & SEND_SELECTION_DENIED) != 0;
}
public boolean shouldSendFoundsetSize()
{
return (changeFlags & SEND_FOUNDSET_SIZE) != 0;
}
public boolean shouldSendHadMoreRows()
{
return (changeFlags & SEND_HAD_MORE_ROWS) != 0;
}
public boolean shouldSendColumnFormats()
{
return (changeFlags & SEND_COLUMN_FORMATS) != 0;
}
public boolean shouldSendViewPortBounds()
{
return (changeFlags & SEND_VIEWPORT_BOUNDS) != 0;
}
public boolean shouldSendWholeViewPort()
{
return viewPortDataChangeMonitor.shouldSendWholeViewport();
}
public List<RowData> getViewPortChanges()
{
return viewPortDataChangeMonitor.getViewPortChanges();
}
/**
* Registers the change notifier; this notifier is to be used to fire property change notifications.
* @param changeNotifier the object that should be notified when this property needs to send updates to client.
*/
public void setChangeNotifier(IChangeListener changeNotifier)
{
this.changeNotifier = changeNotifier;
if (hasChanges()) changeNotifier.valueChanged();
}
public boolean hasChanges()
{
return shouldSendAll() || shouldSendFoundsetSize() || shouldSendSelectedIndexes() || shouldSendViewPortBounds() || shouldSendWholeViewPort() ||
shouldSendColumnFormats() || shouldSendSelectionAccepted() || shouldSendSelectionDenied() || getViewPortChanges().size() > 0;
}
public void clearChanges()
{
changeFlags = 0;
viewPortDataChangeMonitor.clearChanges();
}
protected void notifyChange()
{
if (changeNotifier != null) changeNotifier.valueChanged();
}
public static class RowData implements IToJSONWriter<IBrowserConverterContext>
{
public static final int CHANGE = 0;
public static final int INSERT = 1;
public static final int DELETE = 2;
public final int startIndex;
public final int endIndex;
public final int type;
private final IJSONStringWithConversions rowData;
/**
* Null if it's a whole row, and non-null of only one column of the row is in this row data.
*/
public final String columnName;
public RowData(IJSONStringWithConversions rowData, int startIndex, int endIndex, int type)
{
this(rowData, startIndex, endIndex, type, null);
}
public RowData(IJSONStringWithConversions rowData, int startIndex, int endIndex, int type, String columnName)
{
this.rowData = rowData;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.type = type;
this.columnName = columnName;
}
@Override
public boolean writeJSONContent(JSONWriter w, String keyInParent, IToJSONConverter<IBrowserConverterContext> converter,
DataConversion clientDataConversions) throws JSONException
{
JSONUtils.addKeyIfPresent(w, keyInParent);
w.object().key("rows").value(rowData);
clientDataConversions.pushNode("rows").convert(rowData.getDataConversions()).popNode();
w.key("startIndex").value(Integer.valueOf(startIndex)).key("endIndex").value(Integer.valueOf(endIndex)).key("type").value(Integer.valueOf(type)).endObject();
return true;
}
/**
* True if the data of this RowData would be completely replaced by another immediately following RowData.
* @param newOperation the following update operation.
*/
public boolean isMadeIrrelevantBySubsequentRowData(RowData newOperation)
{
// so a change can be made obsolet by a subsequent (imediately after) change or delete of the same row;
// it we're talking about two change operations, it matters as well if one of them is only for a specific column of the row or for the whole row
return (type == CHANGE && (newOperation.type == CHANGE || newOperation.type == DELETE) && startIndex >= newOperation.startIndex &&
endIndex <= newOperation.endIndex && (newOperation.columnName == null || newOperation.columnName.equals(columnName)));
}
}
public void addViewportDataChangeMonitor(ViewportDataChangeMonitor viewPortChangeMonitor)
{
if (!viewPortDataChangeMonitors.contains(viewPortChangeMonitor)) viewPortDataChangeMonitors.add(viewPortChangeMonitor);
}
public void removeViewportDataChangeMonitor(ViewportDataChangeMonitor viewPortChangeMonitor)
{
viewPortDataChangeMonitors.remove(viewPortChangeMonitor);
}
/**
* Ignores update record events for the record with given pkHash.
*/
protected void pauseRowUpdateListener(String pkHash)
{
viewPortDataChangeMonitor.pauseRowUpdateListener(pkHash);
}
/**
* Resumes listening normally to row updates.
*/
protected void resumeRowUpdateListener()
{
viewPortDataChangeMonitor.resumeRowUpdateListener();
}
public void columnFormatsUpdated()
{
if (!shouldSendAll())
{
int oldChangeFlags = changeFlags;
changeFlags = changeFlags | SEND_COLUMN_FORMATS;
if (oldChangeFlags != changeFlags) notifyChange();
}
}
public void checkHadMoreRows()
{
if (propertyValue.getFoundset() != null)
{
boolean newHadMoreRows = propertyValue.getFoundset().hadMoreRows();
boolean changed = (newHadMoreRows != lastHadMoreRecords);
lastHadMoreRecords = newHadMoreRows;
if (changed && !shouldSendAll())
{
int oldChangeFlags = changeFlags;
changeFlags = changeFlags | SEND_HAD_MORE_ROWS;
if (oldChangeFlags != changeFlags) notifyChange();
}
}
}
// protected static class RecordChangeDescriptor implements JSONWritable
// {
//
// public static enum Types
// {
// CHANGE(0), ADD_TO_VIEWPORT(1), REMOVE_FROM_VIEWPORT(2);
//
// public final int v;
//
// private Types(int v)
// {
// this.v = v;
// }
// };
//
// private final int relativeIndex;
// private final Types type;
//
// /**
// * @param type one of {@link #CHANGE}, {@link #ADD_TO_VIEWPORT} or {@link #REMOVE_FROM_VIEWPORT}
// * @param relativeIndex viewPort relative index of the change.
// */
// public RecordChangeDescriptor(Types type, int relativeIndex)
// {
// this.type = type;
// this.relativeIndex = relativeIndex;
// }
//
// public Map<String, Object> toMap()
// {
// Map<String, Object> retValue = new HashMap<>();
// retValue.put("relativeIndex", Integer.valueOf(relativeIndex));
// retValue.put("type", Integer.valueOf(type.v));
// return retValue;
// }
// }
}