/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 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.dataprocessing; import java.lang.ref.ReferenceQueue; import java.rmi.RemoteException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import com.servoy.base.query.IBaseSQLCondition; import com.servoy.j2db.dataprocessing.RowManager.RowFireNotifyChange.CalculationDependencyData; import com.servoy.j2db.dataprocessing.ValueFactory.BlobMarkerValue; import com.servoy.j2db.dataprocessing.ValueFactory.DbIdentValue; import com.servoy.j2db.persistence.Column; import com.servoy.j2db.persistence.ColumnInfo; import com.servoy.j2db.persistence.IRepository; import com.servoy.j2db.persistence.IServer; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.Table; import com.servoy.j2db.query.AbstractBaseQuery; import com.servoy.j2db.query.IQuerySelectValue; import com.servoy.j2db.query.ISQLUpdate; import com.servoy.j2db.query.QueryColumn; import com.servoy.j2db.query.QueryDelete; import com.servoy.j2db.query.QueryInsert; import com.servoy.j2db.query.QuerySelect; import com.servoy.j2db.query.QueryTable; import com.servoy.j2db.query.QueryUpdate; import com.servoy.j2db.query.SetCondition; import com.servoy.j2db.query.TablePlaceholderKey; import com.servoy.j2db.scripting.GlobalScope; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.SafeArrayList; import com.servoy.j2db.util.ServoyException; import com.servoy.j2db.util.SoftReferenceWithData; import com.servoy.j2db.util.Utils; /** * Manager for rows from one table * * @author jblok */ public class RowManager implements IModificationListener, IFoundSetEventListener { private final FoundSetManager fsm; private final ReferenceQueue<Row> referenceQueue; private final Map<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>> pkRowMap; // pkString -> SoftReference(Row) private final SQLSheet sheet; private final WeakHashMap<IRowListener, Object> listeners; private final Set<NamedLock> lockedRowPKs; private final Map<String, Set<String>> globalCalcDependencies = new HashMap<String, Set<String>>(); private final Map<String, Set<String>> relationsUsedInCalcs = new HashMap<String, Set<String>>(); private final Map<String, Map<String, Set<String>>> aggregateCalcDependencies = new HashMap<String, Map<String, Set<String>>>(); private Set<String> deleteSet; RowManager(FoundSetManager fsm, SQLSheet sheet) { this.fsm = fsm; this.sheet = sheet; pkRowMap = new ConcurrentHashMap<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>>(64); referenceQueue = new ReferenceQueue<Row>(); listeners = new WeakHashMap<IRowListener, Object>(10); lockedRowPKs = Collections.synchronizedSet(new HashSet<NamedLock>());//my locks } public void dispose() { fsm.removeGlobalFoundsetEventListener(this); } private static Object dummy = new Object(); void register(IRowListener fs) { synchronized (listeners) { listeners.put(fs, dummy); } } FoundSetManager getFoundsetManager() { return fsm; } //See ALSO Row.getPKHashKey public static String createPKHashKey(Object[] pk) { StringBuilder sb = new StringBuilder(); if (pk != null) { for (Object val : pk) { if (val instanceof DbIdentValue) { Object identValue = ((DbIdentValue)val).getPkValue(); if (identValue == null) { val = "_svdbi" + val.hashCode(); // DbIdentValue.hashCode() must be stable, i.e. not change when value is set //$NON-NLS-1$ } else { val = identValue; } } String str; if (val instanceof byte[]) { str = Utils.encodeBASE64((byte[])val); // UUID } else if (val instanceof String && ((String)val).length() == 36 && ((String)val).split("-").length == 5) //$NON-NLS-1$ { // make sure UUID PKs are matched regardless of casing (MSQ Sqlserver returns uppercase UUID strings for uniqueidentifier columns) str = ((String)val).toLowerCase(); } else if (val instanceof Date) { str = Long.toString(((Date)val).getTime()); } else { str = Utils.convertToString(val); } if (val != null) { sb.append(str.length()); } sb.append('.'); sb.append(str); sb.append(';'); } } return sb.toString(); } Pair<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> getCachedRow(Object[] pk) { return getCachedRow(createPKHashKey(pk)); } private Pair<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> getCachedRow(String pkhashKey) { Row rowData = null; Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = null; SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkhashKey); if (sr != null) { data = sr.getData(); rowData = sr.get(); if (rowData == null) { Debug.trace("-----------CacheMiss"); //$NON-NLS-1$ if (canRemove(sr)) { removeRowReferences(pkhashKey, null); pkRowMap.remove(pkhashKey); data = null; } } } return new Pair<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>(rowData, data); } Row getRowBasedonPKFromEntireColumnArray(Object[] columndata) { return getRowBasedonPKFromEntireColumnArrayEx(columndata, null /* row notifies are fired immediately */); } /* * Called from RelatedFoundSet constructor, do not fire the row notify changes, but leave them to caller */ Row getRowBasedonPKFromEntireColumnArray(Object[] columndata, List<Runnable> fireRunnables) { final List<RowFireNotifyChange> fires = new ArrayList<RowFireNotifyChange>(); Row row = getRowBasedonPKFromEntireColumnArrayEx(columndata, fires); if (fires.size() > 0) { fireRunnables.add(new Runnable() { public void run() { fireRowNotifyChanges(fires); } }); } return row; } //creates if not existent private Row getRowBasedonPKFromEntireColumnArrayEx(Object[] columndata, List<RowFireNotifyChange> fires) { int[] pkpos = sheet.getPKIndexes(); Object[] pk = new Object[pkpos.length]; for (int i = 0; i < pkpos.length; i++) { Object val = columndata[pkpos[i]]; pk[i] = val; } Row rowData = null; String pkHashKey = createPKHashKey(pk); boolean fireCalcs = false; synchronized (this) { Pair<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> cachedRow = getCachedRow(pkHashKey); rowData = cachedRow.getLeft(); Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = cachedRow.getRight(); if (rowData == null) { rowData = createExistInDBRowObject(columndata); SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = new SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>( rowData, referenceQueue); pkRowMap.put(rowData.getPKHashKey(), sr); if (data != null) { // use existing dependencies if row was GD'd before sr.setData(data); fireCalcs = true; } clearAndCheckCache(); } } if (fireCalcs) { fireDependingCalcs(pkHashKey, null, fires); } return rowData; } Row createNotYetExistInDBRowObject(Object[] data, boolean addToMap) { return createRowObject(data, false, addToMap); } Row createExistInDBRowObject(Object[] data) { return createRowObject(data, true, false); } private Row createRowObject(Object[] data, boolean existInDB, boolean addToMap) { Row row = new Row(this, data, sheet.getAllUnstoredCalculationNamesWithNoValue(), existInDB); if (addToMap) { pkRowMap.put(row.getPKHashKey(), new SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>(row, referenceQueue)); clearAndCheckCache(); } return row; } /** * Rollback data from db, return whether the data was found * @param row * @param doFires * @param mode * @return * @throws ServoyException */ boolean rollbackFromDB(Row row, boolean doFires, Row.ROLLBACK_MODE mode) throws ServoyException { if (!row.existInDB()) { return false; } Object[] pk = row.getPK(); QuerySelect select = (QuerySelect)AbstractBaseQuery.deepClone(sheet.getSQL(SQLSheet.SELECT)); if (!select.setPlaceholderValue(new TablePlaceholderKey(select.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY), pk)) { Debug.error(new RuntimeException("Could not set placeholder " + new TablePlaceholderKey(select.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY) + //$NON-NLS-1$ " in query " + select + "-- continuing")); //$NON-NLS-1$//$NON-NLS-2$ } IDataSet formdata; try { String transaction_id = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { transaction_id = gt.getTransactionID(sheet.getServerName()); } formdata = fsm.getDataServer().performQuery(fsm.getApplication().getClientID(), sheet.getServerName(), transaction_id, select, fsm.getTableFilterParams(sheet.getServerName(), select), false, 0, 1, false); } catch (RemoteException e) { throw new RepositoryException(e); } // construct Rows boolean found = formdata.getRowCount() >= 1; if (found) { row.setRollbackData(formdata.getRow(0), mode); } // else // whoa, row is deleted or pk is updated! if (doFires) fireNotifyChange(null, row, null, found ? RowEvent.UPDATE : RowEvent.DELETE); return found; } private final ThreadLocal<String> adjustingForChangeByOtherPKHashKey = new ThreadLocal<String>(); //return true if i had row and did update @SuppressWarnings("nls") boolean changeByOther(String pkHashKey, int action, Object[] insertColumnDataOrChangedColumns, Row insertedRow) { Pair<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> cachedRow; synchronized (this) { cachedRow = getCachedRow(pkHashKey); } Row rowData = cachedRow.getLeft(); if (rowData != null && action != ISQLActionTypes.INSERT_ACTION) // in case of rawSQL insert & notify, insertColumnDataOrChangedColumns is null, so the row corresponding to the pk was taken from DB and cached { if (action == ISQLActionTypes.DELETE_ACTION) { fireNotifyChange(null, rowData, null, RowEvent.DELETE); } if (rowData.hasListeners() && action == ISQLActionTypes.UPDATE_ACTION) { if (lockedByMyself(rowData)) { Debug.error("the row with pk: " + rowData.getPKHashKey() + " from datasource " + sheet.getServerName() + "/" + sheet.getTable().getName() + " was updating by another client when this client had a lock on it, this shouldn't be possible, the other client should have tried to get the lock first."); } try { adjustingForChangeByOtherPKHashKey.set(rowData.getPKHashKey()); boolean found = rollbackFromDB(rowData, false, Row.ROLLBACK_MODE.UPDATE_CHANGES); int eventType = RowEvent.UPDATE; if (!found && rowData.existInDB()) { // row was not found, check if the pk was updated eventType = RowEvent.DELETE; if (insertColumnDataOrChangedColumns != null) { List<String> pkdps = Arrays.asList(sheet.getPKColumnDataProvidersAsArray()); for (Object changedColumnName : insertColumnDataOrChangedColumns) { if (pkdps.contains(changedColumnName)) { eventType = RowEvent.PK_UPDATED; break; } } } } fireNotifyChange(null, rowData, insertColumnDataOrChangedColumns, eventType); } catch (Exception e) { Debug.error(e);//what can we do here } finally { adjustingForChangeByOtherPKHashKey.remove(); } return true; } else { // the row is in memory but not longer referenced from any record or it was deleted. // do remove it so that it will be re queried when needed (when it was not deleted) removeRowReferences(pkHashKey, null); pkRowMap.remove(pkHashKey); } return false; } if (action == ISQLActionTypes.INSERT_ACTION) { if (insertedRow != null) { fireNotifyChange(null, insertedRow, null, RowEvent.INSERT); if (!insertedRow.hasListeners() && canRemove(pkRowMap.get(pkHashKey))) //new row is not in use { removeRowReferences(pkHashKey, null); pkRowMap.remove(pkHashKey); return false; } return true; } else if (insertColumnDataOrChangedColumns != null && insertColumnDataOrChangedColumns.length == sheet.getColumnNames().length) //last test is just to make sure { rowData = createExistInDBRowObject(insertColumnDataOrChangedColumns); fireNotifyChange(null, rowData, null, RowEvent.INSERT); if (rowData.hasListeners())//new row is in use { boolean fireCalcs = false; synchronized (this) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = new SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>( rowData, referenceQueue); if (cachedRow.getRight() != null) { sr.setData(cachedRow.getRight()); fireCalcs = true; } pkRowMap.put(rowData.getPKHashKey(), sr); } if (fireCalcs) { fireDependingCalcs(pkHashKey, null, null); } return true; } } } else if (action == ISQLActionTypes.UPDATE_ACTION && insertColumnDataOrChangedColumns != null) { // update of row that is not cached by this client fireNotifyChange(null, null, insertColumnDataOrChangedColumns, RowEvent.UPDATE); } //we did not have row cached in memory no update is need, we will get the change if we query for row when needed return false; } synchronized List<Row> getRows(IDataSet pks, int row, int sizeHint, boolean queryAll) throws ServoyException { List<Row> retval = new SafeArrayList<Row>(); if (row >= pks.getRowCount()) return retval; Object[] pk = pks.getRow(row); Row rowData = queryAll ? null : getCachedRow(pk).getLeft(); if (rowData == null) { String transaction_id = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { transaction_id = gt.getTransactionID(sheet.getServerName()); } IDataSet formdata = null; QuerySelect select = (QuerySelect)sheet.getSQL(SQLSheet.SELECT); int maxRow = Math.min(row + sizeHint, pks.getRowCount()); // get the PK array int ncols = pks.getColumnCount(); int nvals = 0; @SuppressWarnings("unchecked") List<Object>[] valueLists = new List[ncols]; for (int c = 0; c < ncols; c++) { valueLists[c] = new ArrayList<Object>(); } for (int i = 0; i < maxRow - row; i++) { Object[] data = pks.getRow(row + i); if (data != null) { if (data.length != ncols) { throw new RuntimeException("Inconsistent PK set width"); //$NON-NLS-1$ } boolean add = true; for (int c = 0; add && c < ncols; c++) { add = !(data[c] instanceof DbIdentValue); } if (add) { nvals++; for (int c = 0; c < ncols; c++) { valueLists[c].add(data[c]); } } } } Object[][] values = new Object[ncols][]; for (int c = 0; c < ncols; c++) { values[c] = valueLists[c].toArray(); } if (!select.setPlaceholderValue(new TablePlaceholderKey(select.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY), values)) { Debug.error(new RuntimeException( "Could not set placeholder " + new TablePlaceholderKey(select.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY) + //$NON-NLS-1$ " in query " + select + "-- continuing")); //$NON-NLS-1$//$NON-NLS-2$ } long time = System.currentTimeMillis(); try { SQLStatement trackingInfo = null; if (fsm.getEditRecordList().hasAccess(sheet.getTable(), IRepository.TRACKING_VIEWS)) { trackingInfo = new SQLStatement(ISQLActionTypes.SELECT_ACTION, sheet.getServerName(), sheet.getTable().getName(), pks, null); trackingInfo.setTrackingData(sheet.getColumnNames(), new Object[][] { }, new Object[][] { }, fsm.getApplication().getUserUID(), fsm.getTrackingInfo(), fsm.getApplication().getClientID()); } formdata = fsm.getDataServer().performQuery(fsm.getApplication().getClientID(), sheet.getServerName(), transaction_id, select, fsm.getTableFilterParams(sheet.getServerName(), select), false, 0, nvals, IDataServer.FOUNDSET_LOAD_QUERY, trackingInfo); if (Debug.tracing()) { Debug.trace(Thread.currentThread().getName() + ": getting RowData time: " + (System.currentTimeMillis() - time) + ", SQL: " + //$NON-NLS-1$ //$NON-NLS-2$ select.toString()); } } catch (RemoteException e) { throw new RepositoryException(e); } //construct Rows for (int k = row; k < maxRow; k++) { String pkHash = createPKHashKey(pks.getRow(k)); //reorder based on pk in mem,cannot do related sort icw SELECT_IN for (int r = 0; r < formdata.getRowCount(); r++) { Object[] columndata = formdata.getRow(r); rowData = getRowBasedonPKFromEntireColumnArray(columndata); if (pkHash.equals(createPKHashKey(rowData.getPK()))) { retval.set(k - row, rowData); break; } } } if (retval.size() < maxRow - row) { retval.set(maxRow - row - 1, null); } } else { retval.add(rowData); if (sizeHint > 1) { int maxRow = Math.min(row + fsm.chunkSize, pks.getRowCount()); for (int r = row + 1; r < maxRow; r++) { Object[] data = pks.getRow(r); Row r2 = getCachedRow(data).getLeft(); if (r2 == null) { // if there is no row te be found, just break and return the currently found retval. break; } retval.add(r2); } } } return retval; } void fireNotifyChange(IRowListener skip, Row r, Object[] changedColumns, int eventType) { if (listeners.size() > 0) { RowEvent e = new RowEvent(this, r, eventType, changedColumns); // First copy it to a array list for concurrent mod... Object[] array = null; synchronized (listeners) { array = listeners.keySet().toArray(); } for (Object element2 : array) { IRowListener element = (IRowListener)element2; if (element != skip) element.notifyChange(e); } } fsm.notifyChange(sheet.getTable()); } void firePKUpdated(Row row, String oldKeyHash) { if (listeners.size() > 0) { RowEvent e = new RowEvent(this, row, RowEvent.PK_UPDATED, oldKeyHash); // First copy it to a array list for concurrent mod... Object[] array = null; synchronized (listeners) { array = listeners.keySet().toArray(); } for (Object listener : array) { ((IRowListener)listener).notifyChange(e); } } } RowUpdateInfo getRowUpdateInfo(Row row, boolean tracking) throws ServoyException { try { if (row.getRowManager() != this) { throw new IllegalArgumentException("I'm not the row manager from row"); //$NON-NLS-1$ } if (adjustingForChangeByOtherPKHashKey.get() != null && adjustingForChangeByOtherPKHashKey.get().equals(row.getPKHashKey())) { row.flagExistInDB(); //we ignore changes here because stored calc with time element are always changed,resulting in endlessloop between clients return null; } if (row.getLastException() instanceof DataException) { //cannot update an row which is not changed (which clears the dataexception) return null; } if (!row.isChanged()) return null; boolean mustRequeryRow = false; List<Column> dbPKReturnValues = new ArrayList<Column>(); SQLSheet.SQLDescription sqlDesc = null; int statement_action; ISQLUpdate sqlUpdate = null; IServer server = fsm.getApplication().getSolution().getServer(sheet.getServerName()); boolean oracleServer = SQLSheet.isOracleServer(server); boolean usesLobs = false; Table table = sheet.getTable(); boolean doesExistInDB = row.existInDB(); List<String> aggregatesToRemove = new ArrayList<String>(8); List<String> changedColumns = null; if (doesExistInDB) { statement_action = ISQLActionTypes.UPDATE_ACTION; sqlDesc = sheet.getSQLDescription(SQLSheet.UPDATE); sqlUpdate = (QueryUpdate)AbstractBaseQuery.deepClone(sqlDesc.getSQLQuery()); List<String> req = sqlDesc.getRequiredDataProviderIDs(); List<String> old = sqlDesc.getOldRequiredDataProviderIDs(); Object[] olddata = row.getRawOldColumnData(); if (olddata == null)//for safety only, nothing changed { return null; } Object[] newdata = row.getRawColumnData(); for (int i = 0; i < olddata.length; i++) { String dataProviderID = req.get(i); Column c = table.getColumn(dataProviderID); ColumnInfo ci = c.getColumnInfo(); if (ci != null && ci.isDBManaged()) { mustRequeryRow = true; } else { Object modificationValue = c.getModificationValue(fsm.getApplication()); if (modificationValue != null) { row.setRawValue(dataProviderID, modificationValue); } if (newdata[i] instanceof BlobMarkerValue) { // if it is a blob marker then it isn't something that is changed // because that would be a byte[] continue; } if ((olddata[i] != null && !olddata[i].equals(newdata[i])) || (newdata[i] != null && !newdata[i].equals(olddata[i]))) { if (sheet.isUsedByAggregate(dataProviderID)) { aggregatesToRemove.addAll(sheet.getAggregateName(dataProviderID)); } Object robj = c.getAsRightType(newdata[i]); if (robj == null) robj = ValueFactory.createNullValue(c.getType()); ((QueryUpdate)sqlUpdate).addValue( new QueryColumn(((QueryUpdate)sqlUpdate).getTable(), c.getID(), c.getSQLName(), c.getType(), c.getLength(), c.getScale(), c.getFlags()), robj); if (changedColumns == null) { changedColumns = new ArrayList<String>(olddata.length - i); } changedColumns.add(c.getName()); if (oracleServer && !usesLobs) { int type = c.getType(); if (type == Types.BLOB && robj instanceof byte[] && ((byte[])robj).length > 4000) { usesLobs = true; } else if (type == Types.CLOB && robj instanceof String && ((String)robj).length() > 4000) { usesLobs = true; } } } } } if (changedColumns == null)//nothing changed after all { // clear the old data now else it will be kept and in a changed state. row.flagExistInDB(); return null; } //add PK Object[] pkValues = new Object[old.size()]; for (int j = 0; j < old.size(); j++) { String dataProviderID = old.get(j); pkValues[j] = row.getOldRequiredValue(dataProviderID); } // TODO: ckeck for success AbstractBaseQuery.setPlaceholderValue(sqlUpdate, new TablePlaceholderKey(((QueryUpdate)sqlUpdate).getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY), pkValues); } else { List<Object> argsArray = new ArrayList<Object>(); statement_action = ISQLActionTypes.INSERT_ACTION; sqlDesc = sheet.getSQLDescription(SQLSheet.INSERT); sqlUpdate = (ISQLUpdate)AbstractBaseQuery.deepClone(sqlDesc.getSQLQuery()); List<String> req = sqlDesc.getRequiredDataProviderIDs(); if (Debug.tracing()) Debug.trace(sqlUpdate.toString()); for (int i = 0; i < req.size(); i++) { String dataProviderID = req.get(i); if (sheet.isUsedByAggregate(dataProviderID)) { aggregatesToRemove.addAll(sheet.getAggregateName(dataProviderID)); } Column c = table.getColumn(dataProviderID); QueryColumn queryColumn = new QueryColumn(((QueryInsert)sqlUpdate).getTable(), c.getID(), c.getSQLName(), c.getType(), c.getLength(), c.getScale(), c.getFlags()); ColumnInfo ci = c.getColumnInfo(); if (c.isDBIdentity()) { dbPKReturnValues.add(c); argsArray.add(row.getDbIdentValue()); } else if (ci != null && ci.isDBManaged()) { mustRequeryRow = true; } else { int columnIndex = getSQLSheet().getColumnIndex(dataProviderID); // HACK: DIRTY way, should use some kind of identifier preferably if (c.getDatabaseDefaultValue() != null && row.getRawValue(columnIndex, false) == null && c.getRowIdentType() == Column.NORMAL_COLUMN) { // The database has a default value, and the value is null, and this is an insert... // Remove the column from the query entirely and make sure the default value is requeried from the db. mustRequeryRow = true; ((QueryInsert)sqlUpdate).removeColumn(queryColumn); } else { Object robj = c.getAsRightType(row.getRawValue(columnIndex, false)); if (robj == null) robj = ValueFactory.createNullValue(c.getType()); argsArray.add(robj); if (oracleServer && !usesLobs) { int type = c.getType(); if (type == Types.BLOB && robj instanceof byte[] && ((byte[])robj).length > 4000) { usesLobs = true; } else if (type == Types.CLOB && robj instanceof String && ((String)robj).length() > 4000) { usesLobs = true; } } } } } AbstractBaseQuery.setPlaceholderValue(sqlUpdate, new TablePlaceholderKey(((QueryInsert)sqlUpdate).getTable(), SQLGenerator.PLACEHOLDER_INSERT_KEY), argsArray.toArray()); } Object[] pk = row.getPK(); IDataSet pks = new BufferedDataSet(); pks.addRow(pk); String tid = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { tid = gt.getTransactionID(sheet.getServerName()); } QuerySelect requerySelect = null; if (mustRequeryRow) { requerySelect = (QuerySelect)AbstractBaseQuery.deepClone(sheet.getSQL(SQLSheet.SELECT)); if (!requerySelect.setPlaceholderValue(new TablePlaceholderKey(requerySelect.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY), pk)) { Debug.error(new RuntimeException( "Could not set placeholder " + new TablePlaceholderKey(requerySelect.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY) + //$NON-NLS-1$ " in query " + requerySelect + "-- continuing")); //$NON-NLS-1$//$NON-NLS-2$ } } SQLStatement statement = new SQLStatement(statement_action, sheet.getServerName(), table.getName(), pks, tid, sqlUpdate, fsm.getTableFilterParams( sheet.getServerName(), sqlUpdate), requerySelect); if (doesExistInDB) statement.setExpectedUpdateCount(1); // check that the row is updated (skip check for inert) if (changedColumns != null) { statement.setChangedColumns(changedColumns.toArray(new String[changedColumns.size()])); } statement.setOracleFixTrackingData(usesLobs && !tracking); statement.setIdentityColumn(dbPKReturnValues.size() == 0 ? null : dbPKReturnValues.get(0)); if (tracking || usesLobs) { statement.setTrackingData(sheet.getColumnNames(), row.getRawOldColumnData() != null ? new Object[][] { row.getRawOldColumnData() } : null, row.getRawColumnData() != null ? new Object[][] { row.getRawColumnData() } : null, fsm.getApplication().getUserUID(), fsm.getTrackingInfo(), fsm.getApplication().getClientID()); } return new RowUpdateInfo(row, statement, dbPKReturnValues, aggregatesToRemove); } catch (RemoteException e) { throw new RepositoryException(e); } } void rowUpdated(final Row row, final String oldKeyHash, final IRowListener src, List<Runnable> runnables) { final boolean doesExistInDB = row.existInDB(); row.flagExistInDB();//always needed flushes stuff String newKeyHash = row.recalcPKHashKey(); if (!oldKeyHash.equals(newKeyHash)) { // fire pk updated to IRowListeners runnables.add(new Runnable() { public void run() { firePKUpdated(row, oldKeyHash); } }); } // run fires later (add this runnable here first because the runnables in EditRecordList are processed in reverse order) runnables.add(new Runnable() { public void run() { fireNotifyChange(src, row, null, doesExistInDB ? RowEvent.UPDATE : RowEvent.INSERT); } }); // may add fires for depending calcs fireDependingCalcsForPKUpdate(row, oldKeyHash, runnables); } void fireDependingCalcsForPKUpdate(Row row, String oldKeyHash) { List<Runnable> runnables = new ArrayList<Runnable>(1); fireDependingCalcsForPKUpdate(row, oldKeyHash, runnables); for (Runnable runnable : runnables) { runnable.run(); } } /** * PK of a row may have been updated, the row may not have been saved yet. * @param row * @param oldKeyHash pkhash * @param runnables */ synchronized void fireDependingCalcsForPKUpdate(final Row row, final String oldKeyHash, List<Runnable> runnables) { // do recalcPKHashKey incase its called before and pk did not yet exist String newKeyHash = row.recalcPKHashKey(); if (!oldKeyHash.equals(newKeyHash)) { final SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> srOld = pkRowMap.get(oldKeyHash); pkRowMap.put(newKeyHash, new SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>(row, referenceQueue));// (over)write new if (srOld != null) { // run fires later runnables.add(new Runnable() { public void run() { // calcs depending on old pk are invalid fireDependingCalcs(srOld, null, null); pkRowMap.remove(oldKeyHash);//remove old } }); } } else { if (!pkRowMap.containsKey(newKeyHash)) { pkRowMap.put(newKeyHash, new SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>(row, referenceQueue)); clearAndCheckCache(); } } } void clearAndCheckCache() { if (referenceQueue.poll() != null) { // quicly clear the whole queue, so that it is empty for the next time. while (referenceQueue.poll() != null) { } // test the hashmap for empty Softreferences Iterator<Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>>> it = pkRowMap.entrySet().iterator(); while (it.hasNext()) { Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>> entry = it.next(); SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> value = entry.getValue(); if (value == null || (value.get() == null && canRemove(value))) { removeRowReferences(entry.getKey(), null); it.remove(); } } } } int getRowCount() { return pkRowMap.size(); } /** * Returns the sheet. * * @return SQLSheet */ SQLSheet getSQLSheet() { return sheet; } void deleteRow(IRowListener src, Row r, boolean tracking, boolean partOfBiggerDelete) throws ServoyException { if (r.getRowManager() != this) throw new IllegalArgumentException("I'm not the row manager from row"); //$NON-NLS-1$ r.flagExistInDB();//prevent it processed by any update, changed is false now if (!partOfBiggerDelete) { QueryDelete sqlDelete = AbstractBaseQuery.deepClone((QueryDelete)sheet.getSQLDescription(SQLSheet.DELETE).getSQLQuery()); Object[] pk = r.getPK(); if (!sqlDelete.setPlaceholderValue(new TablePlaceholderKey(sqlDelete.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY), pk)) { Debug.error(new RuntimeException( "Could not set placeholder " + new TablePlaceholderKey(sqlDelete.getTable(), SQLGenerator.PLACEHOLDER_PRIMARY_KEY) + //$NON-NLS-1$ " in query " + sqlDelete + "-- continuing")); //$NON-NLS-1$ //$NON-NLS-2$ } IDataSet pks = new BufferedDataSet(); pks.addRow(pk); ISQLStatement[] stats_a = new ISQLStatement[1]; String tid = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { tid = gt.getTransactionID(sheet.getServerName()); } SQLStatement statement = new SQLStatement(ISQLActionTypes.DELETE_ACTION, sheet.getServerName(), sheet.getTable().getName(), pks, tid, sqlDelete, fsm.getTableFilterParams(sheet.getServerName(), sqlDelete)); statement.setExpectedUpdateCount(1); // check that 1 record is deleted stats_a[0] = statement; if (tracking) { statement.setTrackingData(sheet.getColumnNames(), r.getRawColumnData() != null ? new Object[][] { r.getRawColumnData() } : null, null, fsm.getApplication().getUserUID(), fsm.getTrackingInfo(), fsm.getApplication().getClientID()); } try { Object[] results = fsm.getDataServer().performUpdates(fsm.getApplication().getClientID(), stats_a); for (int i = 0; results != null && i < results.length; i++) { if (results[i] instanceof ServoyException) { throw (ServoyException)results[i]; } } } catch (RemoteException e) { throw new RepositoryException(e); } SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> removed; synchronized (this) { removed = pkRowMap.remove(r.getPKHashKey()); } fireDependingCalcs(removed, null, null); } else { synchronized (this) { pkRowMap.remove(r.getPKHashKey()); } } fireNotifyChange(src, r, null, RowEvent.DELETE); } void clearRow(Row r) { if (r != null) { synchronized (this) { pkRowMap.remove(r.getPKHashKey()); } } } boolean lockedByMyself(Row r) { return lockedRowPKs.contains(new NamedLock(r.getPKHashKey(), null)); // searches on lock, not on name } public void addLocks(Set<Object> pkhashkeys, String lockName) { for (Object lock : pkhashkeys) { lockedRowPKs.add(new NamedLock(lock, lockName)); } } public boolean acquireLock(String client_id, QuerySelect lockSelect, String lockName, Set<Object> ids) { try { String transaction_id = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { transaction_id = gt.getTransactionID(sheet.getServerName()); } IDataSet dataset = fsm.getApplication().getDataServer().acquireLocks(client_id, sheet.getServerName(), sheet.getTable().getName(), ids, lockSelect, transaction_id, getFoundsetManager().getTableFilterParams(sheet.getServerName(), lockSelect), getFoundsetManager().chunkSize); if (dataset != null) { addLocks(ids, lockName); for (int i = 0; i < dataset.getRowCount(); i++) { Object[] data = dataset.getRow(i); Row rowData = getRowBasedonPKFromEntireColumnArray(data); Object[] currentData = rowData.getRawColumnData(); if (!Utils.equalObjects(data, currentData)) { rowData.setRollbackData(data, Row.ROLLBACK_MODE.UPDATE_CHANGES); fireNotifyChange(null, rowData, null, RowEvent.UPDATE); } } return true; } } catch (Exception e) { Debug.error(e);//TODO:put error code in app } return false; } //return true if was forced remove public void removeLocks(Set<Object> pkhashkeys) { for (Object lock : pkhashkeys) { lockedRowPKs.remove(new NamedLock(lock, null));// searches on lock, not on name } } public Set<Object> getOwnLocks(String lockName) { Set<Object> locks = new HashSet<Object>(lockedRowPKs.size()); for (NamedLock namedLock : lockedRowPKs) { if (lockName == null || lockName.equals(namedLock.name)) { locks.add(namedLock.lock); } } return locks; } public boolean hasOwnLocks(String lockName) { for (NamedLock namedLock : lockedRowPKs) { if (lockName == null || lockName.equals(namedLock.name)) { return true; } } return false; } /** * Flushes all cached rows which are not edited. */ synchronized void flushAllCachedRows() { //expensive but safe @SuppressWarnings("unchecked") Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>>[] array = pkRowMap.entrySet().toArray( new Entry[pkRowMap.size()]); for (Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>> entry : array) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> ref = entry.getValue(); Row row = ref.get(); if (row == null || !row.isChanged()) { if (canRemove(ref)) { removeRowReferences(entry.getKey(), null); pkRowMap.remove(entry.getKey()); } else { ref.clear(); } } } } /** * @param row * @return */ Blob getBlob(Row row, int columnIndex) throws Exception { QuerySelect blobSelect = new QuerySelect(new QueryTable(sheet.getTable().getSQLName(), sheet.getTable().getDataSource(), sheet.getTable().getCatalog(), sheet.getTable().getSchema())); String blobColumnName = sheet.getColumnNames()[columnIndex]; Column blobColumn = sheet.getTable().getColumn(blobColumnName); blobSelect.addColumn(new QueryColumn(blobSelect.getTable(), blobColumn.getID(), blobColumn.getSQLName(), blobColumn.getType(), blobColumn.getLength(), blobColumn.getScale(), blobColumn.getFlags(), false)); String[] pkColumnNames = sheet.getPKColumnDataProvidersAsArray(); IQuerySelectValue[] pkQuerycolumns = new IQuerySelectValue[pkColumnNames.length]; Object[][] pkValues = new Object[pkColumnNames.length][]; Object[] pk = row.getPK(); for (int k = 0; k < pkValues.length; k++) { Column pkcolumn = sheet.getTable().getColumn(pkColumnNames[k]); pkQuerycolumns[k] = new QueryColumn(blobSelect.getTable(), pkcolumn.getID(), pkcolumn.getSQLName(), pkcolumn.getType(), pkcolumn.getLength(), pkcolumn.getScale(), pkcolumn.getFlags(), false); pkValues[k] = new Object[] { pk[k] }; } blobSelect.addCondition("blobselect", new SetCondition(IBaseSQLCondition.EQUALS_OPERATOR, pkQuerycolumns, pkValues, true)); //$NON-NLS-1$ String serverName = sheet.getServerName(); String transaction_id = null; GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { transaction_id = gt.getTransactionID(sheet.getServerName()); } return fsm.getApplication().getDataServer().getBlob(fsm.getApplication().getClientID(), serverName, blobSelect, fsm.getTableFilterParams(sheet.getServerName(), blobSelect), transaction_id); } /** * Value changed, clear depending calcs */ public void valueChanged(ModificationEvent e) { Collection<String> calcs = null; if (e.getSource() instanceof GlobalScope) { // global changed synchronized (globalCalcDependencies) { Set<String> calcSet = globalCalcDependencies.get(e.getName()); if (calcSet != null) { calcs = new ArrayList<String>(calcSet); } } } else if (e.getSource() instanceof IFoundSet) { // aggregate changed synchronized (aggregateCalcDependencies) { Map<String, Set<String>> dependingAggregateCalcs = aggregateCalcDependencies.get(((IFoundSet)e.getSource()).getDataSource()); if (dependingAggregateCalcs != null) { if (e.getName() == null) { // all aggregates Set<String> dependingCalcs = new HashSet<String>(); for (Set<String> set : dependingAggregateCalcs.values()) { dependingCalcs.addAll(set); } calcs = dependingCalcs; } else { // named aggregate Set<String> dependingCalcs = dependingAggregateCalcs.get(e.getName()); if (dependingCalcs != null) { calcs = new ArrayList<String>(dependingCalcs); } } } } } clearCalcs(null, calcs); } /* * Called from FoundsetManager GlobalFoundSetEventListener */ public void foundSetChanged(FoundSetEvent e) { IFoundSet sourceFoundset = e.getSourceFoundset(); // only act on new foundsets or size changes for related foundsets if (e.getType() == FoundSetEvent.NEW_FOUNDSET || (e.getType() == FoundSetEvent.CONTENTS_CHANGED && (e.getChangeType() == FoundSetEvent.CHANGE_INSERT || e.getChangeType() == FoundSetEvent.FOUNDSET_INVALIDATED || e.getChangeType() == FoundSetEvent.CHANGE_DELETE))) { if (sourceFoundset instanceof RelatedFoundSet && !sourceFoundset.isInFindMode()) { String relationName = sourceFoundset.getRelationName(); // related foundset changed List<String> calcs = null; // first test if there are calcs that depend on this relation, to filter out relations that are never used in calcs synchronized (relationsUsedInCalcs) { Set<String> calcSet = relationsUsedInCalcs.get(relationName); if (calcSet != null) { calcs = new ArrayList<String>(calcSet); } } if (calcs != null) { // some calcs depend on a related foundset with this name, search by whereArgs String whereArgsHash = ((RelatedFoundSet)sourceFoundset).getWhereArgsHash(); List<CalculationDependency> calculationDependencies = new ArrayList<CalculationDependency>(); // go over each row to see if there are calcs depending on the RFS(whereArgs) Iterator<Map.Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>>> it = pkRowMap.entrySet().iterator(); while (it.hasNext()) { Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>> entry = it.next(); String pkHash = entry.getKey(); SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = entry.getValue(); synchronized (sr) { Row row = sr.get(); if (row != null) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data != null) { CalculationDependencyData calcRowrefs = data.getRight(); if (calcRowrefs != null) { for (String calc : calcs) { List<RelationDependency> deps = calcRowrefs.getRelationDependencies(calc); if (deps != null) { for (RelationDependency dep : deps) { if (relationName.equals(dep.relationName) && whereArgsHash.equals(dep.whereArgsHash)) { // the calc depends on this related foundset calculationDependencies.add(new CalculationDependency(sheet.getTable().getDataSource(), pkHash, calc)); } } } } } } } } } if (calculationDependencies.size() > 0) { List<RowFireNotifyChange> fires = new ArrayList<RowFireNotifyChange>(); for (CalculationDependency dep : calculationDependencies) { fireCalculationFlagged(dep.pkHashKey, dep.calc, fires); } if (fires.size() > 0) { fireRowNotifyChanges(fires); fireNotifyChange(null, null, null, RowEvent.UPDATE); } } } } } } /** * @param calcs */ boolean clearingCalcs = false; public void clearCalcs(IRowListener source, Collection<String> calcs) { if (!clearingCalcs && calcs != null && calcs.size() > 0) { try { clearingCalcs = true; List<RowFireNotifyChange> fires = new ArrayList<RowFireNotifyChange>(); for (String calc : calcs) { clearCalc(calc, fires); } if (fires.size() > 0) { fireRowNotifyChanges(fires); fireNotifyChange(source, null, null, RowEvent.UPDATE); } } finally { clearingCalcs = false; } } } private static void fireRowNotifyChanges(List<RowFireNotifyChange> fires) { if (fires != null && fires.size() > 0) { FireCollector collector = FireCollector.getFireCollector(); try { Set<RowFireNotifyChange> fired = new HashSet<RowFireNotifyChange>(); for (RowFireNotifyChange fire : fires) { if (fired.add(fire)) { fire.row.fireNotifyChange(fire.name, fire.value, collector); } } } finally { collector.done(); } } } /** * @param dp */ private synchronized boolean clearCalc(String dp, List<RowFireNotifyChange> fires) { boolean changed = false; Iterator<Map.Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>>> it = pkRowMap.entrySet().iterator(); while (it.hasNext()) { Entry<String, SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>>> entry = it.next(); SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = entry.getValue(); Row row = sr.get(); if (row != null) { if (fireCalculationFlagged(row.getPKHashKey(), dp, fires) && !changed) changed = true; } else if (canRemove(sr)) { removeRowReferences(entry.getKey(), dp); it.remove();//was empty remove while we are here anyway... } } return changed; } public void flagAllRowCalcsForRecalculation(String pkHashKey) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { List<String> calcsUptodate = null; synchronized (sr) { Row row = sr.get(); if (row != null) { calcsUptodate = row.getCalcsUptodate(); } } flagRowCalcsForRecalculation(pkHashKey, calcsUptodate); } } public void flagRowCalcsForRecalculation(String pkHashKey, List<String> calcs) { if (calcs != null && calcs.size() > 0) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { for (String calc : calcs) { flagRowCalcForRecalculation(pkHashKey, calc); } } } } public boolean flagRowCalcForRecalculation(String pkHashKey, String calc) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { Row row = sr.get(); if (row != null && row.internalFlagCalcForRecalculation(calc)) { // check the calculation dependencies registered for the calc removeRowReferences(pkHashKey, calc); return true; } } return false; } /** * Remove references in other RowManagers to this row. * @param pkHashKey * @param calc, null for all */ public void removeRowReferences(String pkHashKey, String calc) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { CalculationDependencyData rowRefs = null; synchronized (sr) { // check the calculation dependencies registered for the calc Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data != null) { CalculationDependencyData calcRowrefs = data.getRight(); if (calcRowrefs != null) { // remove both row refs and relation dependencies if (calc == null) { rowRefs = calcRowrefs; data.setRight(null); } else { List<RowReference> refs = calcRowrefs.removeReferences(calc); if (refs != null) { rowRefs = new CalculationDependencyData(); rowRefs.putRowReferences(calc, refs); } } } } } if (rowRefs != null) { String dataSource = fsm.getDataSource(sheet.getTable()); for (Entry<String, List<RowReference>> entry : rowRefs.getRowReferencesEntrySet()) { for (RowReference reference : entry.getValue()) { try { getFoundsetManager().getRowManager(reference.dataSource).removeCalculationDependency(reference.pkHashKey, reference.dataproviderId, dataSource, pkHashKey, entry.getKey()); } catch (ServoyException e) { Debug.log(e); } } } } } } public void clearDeleteSet() { deleteSet = null; } public boolean addRowToDeleteSet(String pkHashKey) { if (deleteSet == null) { deleteSet = new HashSet<String>(); } return deleteSet.add(pkHashKey); } /** * Wrapper for lock object with name. <br> * Note: equality is only based on lock object so that locks with different names can be found in a set of NamedLocks. */ private static class NamedLock { public final Object lock; public final String name; public NamedLock(Object lock, String name) { this.lock = lock; this.name = name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((lock == null) ? 0 : lock.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final NamedLock other = (NamedLock)obj; if (lock == null) { if (other.lock != null) return false; } else if (!lock.equals(other.lock)) return false; return true; } } public void addCalculationGlobalDependency(String global, String calc) { boolean first; synchronized (globalCalcDependencies) { first = globalCalcDependencies.size() == 0; Set<String> dependingCalcs = globalCalcDependencies.get(global); if (dependingCalcs == null) { dependingCalcs = new HashSet<String>(); globalCalcDependencies.put(global, dependingCalcs); } dependingCalcs.add(calc); } if (first) { fsm.getApplication().getScriptEngine().getScopesScope().getModificationSubject().addModificationListener(this); } } public void addCalculationAggregateDependency(IFoundSetInternal foundSet, String aggregateName, String calc) { foundSet.addAggregateModificationListener(this); synchronized (aggregateCalcDependencies) { String dataSource = foundSet.getDataSource(); Map<String, Set<String>> dependingAggregateCalcs = aggregateCalcDependencies.get(dataSource); if (dependingAggregateCalcs == null) { dependingAggregateCalcs = new HashMap<String, Set<String>>(); aggregateCalcDependencies.put(dataSource, dependingAggregateCalcs); } Set<String> dependingCalcs = dependingAggregateCalcs.get(aggregateName); if (dependingCalcs == null) { dependingCalcs = new HashSet<String>(); dependingAggregateCalcs.put(aggregateName, dependingCalcs); } dependingCalcs.add(calc); } } private boolean canRemove(SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr) { if (sr != null) { List<CalculationDependency> dependencies = new ArrayList<CalculationDependency>(); Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = null; synchronized (sr) { data = sr.getData(); if (data != null) { Map<String, List<CalculationDependency>> dependencyMap = data.getLeft(); if (dependencyMap != null) { for (List<CalculationDependency> lst : dependencyMap.values()) { dependencies.addAll(lst); } } sr.setData(null); } } try { for (CalculationDependency dep : dependencies) { RowManager rm; try { rm = fsm.getRowManager(dep.dataSource); } catch (ServoyException e) { Debug.error(e); return false; } if (rm != null && rm.getCachedRow(dep.pkHashKey).getLeft() != null) { return false; } } } finally { synchronized (sr) { sr.setData(data); } } } return true; } /** * Fire calcs depending on dataproviders. * * @param pkHashKey * @param dataProviderId, null for all */ public void fireDependingCalcs(String pkHashKey, String dataProviderId, List<RowFireNotifyChange> fires) { fireDependingCalcs(pkRowMap.get(pkHashKey), dataProviderId, fires); } /** * Fire calcs depending on dataproviders. * * @param pkHashKey * @param dataProviderId, null for all */ protected void fireDependingCalcs(SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr, String dataProviderId, List<RowFireNotifyChange> fires) { List<CalculationDependency> deps = null; if (sr != null) { synchronized (sr) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data != null) { Map<String, List<CalculationDependency>> dependencyMap = data.getLeft(); if (dependencyMap != null) { if (dataProviderId == null) { deps = new ArrayList<CalculationDependency>(); for (List<CalculationDependency> lst : dependencyMap.values()) { deps.addAll(lst); } dependencyMap.clear(); } else { deps = dependencyMap.remove(dataProviderId); } } } } } if (deps != null && deps.size() > 0) { List<RowFireNotifyChange> myFires; if (fires == null) { myFires = new ArrayList<RowFireNotifyChange>(); } else { myFires = fires; } for (CalculationDependency dep : deps) { RowManager rm; try { rm = fsm.getRowManager(dep.dataSource); if (rm != null) { rm.fireCalculationFlagged(dep.pkHashKey, dep.calc, myFires); } } catch (ServoyException e) { Debug.error(e); } } if (fires == null) { // my own fires list fireRowNotifyChanges(myFires); } // else caller handles fires } } /** * Fire through that the calc has flagged, fire calcs depending on this calc * @param pkHashKey * @param calc * @param fires * @return */ private boolean fireCalculationFlagged(String pkHashKey, String calc, List<RowFireNotifyChange> fires) { if (flagRowCalcForRecalculation(pkHashKey, calc)) { Row row = getCachedRow(pkHashKey).getLeft(); if (row != null) { List<RowFireNotifyChange> myFires; if (fires == null) { myFires = new ArrayList<RowFireNotifyChange>(); } else { myFires = fires; } row.getRowManager().fireDependingCalcs(row.getPKHashKey(), calc, myFires); myFires.add(new RowFireNotifyChange(row, null, calc, null)); if (fires == null) { // fire now fireRowNotifyChanges(myFires); } // else caller will fire } return true; } return false; } /** * Register a calculation dependency, dependingCalc in row(dependingDataSource, dependingPkHashKey) was calculated using row(pkHashKey) in this datasource. * @param pkHashKey * @param dataproviderId * @param dependingDataSource * @param dependingPkHashKey * @param dependingCalc */ public void addCalculationDependency(String pkHashKey, String dataproviderId, String dependingDataSource, String dependingPkHashKey, String dependingCalc) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { synchronized (sr) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data == null) { data = new Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>(null, null); sr.setData(data); } Map<String, List<CalculationDependency>> dependencyMap = data.getLeft(); if (dependencyMap == null) { dependencyMap = new HashMap<String, List<CalculationDependency>>(); data.setLeft(dependencyMap); } List<CalculationDependency> dependencies = dependencyMap.get(dataproviderId); if (dependencies == null) { dependencies = new ArrayList<CalculationDependency>(); dependencyMap.put(dataproviderId, dependencies); } dependencies.add(new CalculationDependency(dependingDataSource, dependingPkHashKey, dependingCalc)); } // inform depending rowmanager as well (including its own) try { getFoundsetManager().getRowManager(dependingDataSource).addCalculationDependencyBackReference( new RowReference(fsm.getDataSource(sheet.getTable()), dataproviderId, pkHashKey), dependingPkHashKey, dependingCalc); } catch (ServoyException e) { Debug.error(e); } } } /** * Remove a calculation dependency, dependingCalc in row(dependingDataSource, dependingPkHashKey) was calculated using row(pkHashKey) in this datasource. * @param pkHashKey * @param dataproviderId * @param dependingDataSource * @param dependingPkHashKey * @param dependingCalc */ public void removeCalculationDependency(String pkHashKey, String dataproviderId, String dependingDataSource, String dependingPkHashKey, String dependingCalc) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { synchronized (sr) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data != null) { Map<String, List<CalculationDependency>> deps = data.getLeft(); if (deps != null) { List<CalculationDependency> list = deps.get(dataproviderId); if (list != null) { Iterator<CalculationDependency> iterator = list.iterator(); while (iterator.hasNext()) { CalculationDependency dep = iterator.next(); if (dep.pkHashKey.equals(dependingPkHashKey) && dep.calc.equals(dependingCalc)) { iterator.remove(); } } if (list.size() == 0) { deps.remove(dataproviderId); } } if (deps.size() == 0) { data.setLeft(null); } } } } } } /** * Rowmanager(dataSource) keeps a calculation dependency for my calc in row(pkHashKey). * @param dataSource * @param pkHashKey * @param calc */ private void addCalculationDependencyBackReference(RowReference rowReference, String pkHashKey, String calc) { SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(pkHashKey); if (sr != null) { synchronized (sr) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data == null) { data = new Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>(null, null); sr.setData(data); } CalculationDependencyData rowRefs = data.getRight(); if (rowRefs == null) { rowRefs = new CalculationDependencyData(); data.setRight(rowRefs); } rowRefs.addRowReference(calc, rowReference); } } } /** * Calculation depends on relation identified by whereArgsHash. * @param whereArgsHash * @param relationName * @param dependingDataSource * @param dependingPkHashKey * @param dependingCalc */ public void addCalculationRelationDependency(String whereArgsHash, String relationName, String dependingDataSource, String dependingPkHashKey, String dependingCalc) { // keep a global list of relations that some calcs depend on boolean first; synchronized (relationsUsedInCalcs) { first = relationsUsedInCalcs.size() == 0; Set<String> dependingCalcs = relationsUsedInCalcs.get(relationName); if (dependingCalcs == null) { dependingCalcs = new HashSet<String>(); relationsUsedInCalcs.put(relationName, dependingCalcs); } dependingCalcs.add(dependingCalc); } // add a relation dependency for the calc SoftReferenceWithData<Row, Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>> sr = pkRowMap.get(dependingPkHashKey); if (sr != null) { synchronized (sr) { Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData> data = sr.getData(); if (data == null) { data = new Pair<Map<String, List<CalculationDependency>>, CalculationDependencyData>(null, null); sr.setData(data); } CalculationDependencyData rowRefs = data.getRight(); if (rowRefs == null) { rowRefs = new CalculationDependencyData(); data.setRight(rowRefs); } rowRefs.addRelationDependency(dependingCalc, new RelationDependency(relationName, whereArgsHash)); } } if (first) { // listen for foundset events to all foundsets fsm.addGlobalFoundsetEventListener(this); } } /** * @see java.lang.Object#toString() */ @Override public String toString() { return "RowManager:" + sheet.getTable().getDataSource(); //$NON-NLS-1$ } public static class CalculationDependency { public final String dataSource; public final String pkHashKey; public final String calc; public CalculationDependency(String dataSource, String pkHashKey, String calc) { this.dataSource = dataSource; this.pkHashKey = pkHashKey; this.calc = calc; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("CalculationDependency [calc=").append(calc).append(", dataSource=").append(dataSource).append(", pkHashKey=").append(pkHashKey).append( //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ ']'); return builder.toString(); } } public static class RelationDependency { public final String relationName; public final String whereArgsHash; public RelationDependency(String relationName, String whereArgsHash) { this.relationName = relationName; this.whereArgsHash = whereArgsHash; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((relationName == null) ? 0 : relationName.hashCode()); result = prime * result + ((whereArgsHash == null) ? 0 : whereArgsHash.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; RelationDependency other = (RelationDependency)obj; if (relationName == null) { if (other.relationName != null) return false; } else if (!relationName.equals(other.relationName)) return false; if (whereArgsHash == null) { if (other.whereArgsHash != null) return false; } else if (!whereArgsHash.equals(other.whereArgsHash)) return false; return true; } @Override public String toString() { return "RelationDependency [relationName=" + relationName + ", whereArgsHash=" + whereArgsHash + ']'; //$NON-NLS-1$ //$NON-NLS-2$ } } public static class RowReference { public final String dataSource; public final String dataproviderId; public final String pkHashKey; public RowReference(String dataSource, String dataproviderId, String pkHashKey) { this.dataSource = dataSource; this.dataproviderId = dataproviderId; this.pkHashKey = pkHashKey; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("RowReference [dataSource=").append(dataSource).append(", dataproviderId=").append(dataproviderId).append(", pkHashKey=").append( //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ pkHashKey).append(']'); return builder.toString(); } } public static class RowFireNotifyChange { public final Row row; public final IRowChangeListener skip; public final String name; public final Object value; public RowFireNotifyChange(Row row, IRowChangeListener skip, String name, Object value) { this.row = row; this.skip = skip; this.name = name; this.value = value; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((row == null) ? 0 : row.hashCode()); result = prime * result + ((skip == null) ? 0 : skip.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; RowFireNotifyChange other = (RowFireNotifyChange)obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (row == null) { if (other.row != null) return false; } else if (!row.equals(other.row)) return false; if (skip == null) { if (other.skip != null) return false; } else if (!skip.equals(other.skip)) return false; if (value == null) { if (other.value != null) return false; } else if (!value.equals(other.value)) return false; return true; } @Override public String toString() { return "RowFireNotifyChange [name=" + name + ", row=" + row + ", skip=" + skip + ", value=" + value + ']'; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } /** * Container for what a calculation depends on: which dataproviders in which row (RowReference) and which relations with which whereargs (RelationDependency). * @author rgansevles * */ public static class CalculationDependencyData { private Map<String, List<RowReference>> rowReferences; private Map<String, List<RelationDependency>> relationDependencies; public void addRowReference(String calc, RowReference rowReference) { if (rowReferences == null) { rowReferences = new HashMap<String, List<RowReference>>(); } List<RowReference> list = rowReferences.get(calc); if (list == null) { list = new ArrayList<RowReference>(); rowReferences.put(calc, list); } list.add(rowReference); } public void addRelationDependency(String calc, RelationDependency relationDependency) { if (relationDependencies == null) { relationDependencies = new HashMap<String, List<RelationDependency>>(); } List<RelationDependency> list = relationDependencies.get(calc); if (list == null) { list = new ArrayList<RelationDependency>(); relationDependencies.put(calc, list); } if (!list.contains(relationDependency)) { list.add(relationDependency); } } public void putRowReferences(String calc, List<RowReference> calcRowRefs) { if (rowReferences == null) { rowReferences = new HashMap<String, List<RowReference>>(); } rowReferences.put(calc, calcRowRefs); } /** * Remove both rowReferences and relationDependencies for the calc, return the removed rowReferences * @param calc * @return */ public List<RowReference> removeReferences(String calc) { if (relationDependencies != null) { relationDependencies.remove(calc); } if (rowReferences == null) return null; return rowReferences.remove(calc); } public Set<Entry<String, List<RowReference>>> getRowReferencesEntrySet() { if (rowReferences == null) return Collections.<Entry<String, List<RowReference>>> emptySet(); return rowReferences.entrySet(); } public List<RelationDependency> getRelationDependencies(String calc) { if (relationDependencies == null) return null; return relationDependencies.get(calc); } } } }