/* 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.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.ReentrantLock; import org.mozilla.javascript.JavaScriptException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.servoy.j2db.ApplicationException; import com.servoy.j2db.IPrepareForSave; 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.IRepository; import com.servoy.j2db.persistence.StaticContentSpecLoader; import com.servoy.j2db.persistence.Table; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.IntHashMap; import com.servoy.j2db.util.ServoyException; import com.servoy.j2db.util.Utils; /** * Keeps track of all the edited records and handles the save * * @author jcompagner */ public class EditRecordList { protected static final Logger log = LoggerFactory.getLogger("com.servoy.j2db.dataprocessing.editedRecords"); //$NON-NLS-1$ private final FoundSetManager fsm; private final List<IPrepareForSave> prepareForSaveListeners = new ArrayList<IPrepareForSave>(2); private final List<IGlobalEditListener> editListeners = new ArrayList<IGlobalEditListener>(); private final Map<Table, Integer> accessMap = Collections.synchronizedMap(new HashMap<Table, Integer>());//per table (could be per column in future) private boolean autoSave = true; private ConcurrentMap<FoundSet, int[]> fsEventMap; private final ReentrantLock editRecordsLock = new ReentrantLock(); private final List<IRecordInternal> editedRecords = Collections.synchronizedList(new ArrayList<IRecordInternal>(32)); private final List<IRecordInternal> failedRecords = Collections.synchronizedList(new ArrayList<IRecordInternal>(2)); private final List<IRecordInternal> recordTested = Collections.synchronizedList(new ArrayList<IRecordInternal>(2)); //tested for form.OnRecordEditStop event private boolean preparingForSave; private final boolean disableInsertsReorder; public EditRecordList(FoundSetManager fsm) { this.fsm = fsm; disableInsertsReorder = Utils.getAsBoolean(fsm.getApplication().getSettings().getProperty("servoy.disable.record.insert.reorder", "false")); //$NON-NLS-1$ //$NON-NLS-2$ } public IRecordInternal[] getFailedRecords() { editRecordsLock.lock(); try { return failedRecords.toArray(new IRecordInternal[failedRecords.size()]); } finally { editRecordsLock.unlock(); } } /** * @param record * @return */ public boolean isEditing(IRecordInternal record) { editRecordsLock.lock(); try { return editedRecords.contains(record); } finally { editRecordsLock.unlock(); } } /** * @return */ public boolean isEditing() { editRecordsLock.lock(); try { return editedRecords.size() + failedRecords.size() > 0; } finally { editRecordsLock.unlock(); } } /** * @param set * @return */ public IRecordInternal[] getEditedRecords(IFoundSet set) { return getEditedRecords(set, false); } public IRecordInternal[] getEditedRecords(IFoundSet set, boolean removeUnchanged) { if (removeUnchanged) { removeUnChangedRecords(true, false, set); } List<IRecordInternal> al = new ArrayList<IRecordInternal>(); editRecordsLock.lock(); try { for (int i = editedRecords.size(); --i >= 0;) { IRecordInternal record = editedRecords.get(i); if (record.getParentFoundSet() == set) { al.add(record); } } } finally { editRecordsLock.unlock(); } return al.toArray(new IRecordInternal[al.size()]); } public IRecordInternal[] getFailedRecords(IFoundSet set) { List<IRecordInternal> al = new ArrayList<IRecordInternal>(); editRecordsLock.lock(); try { for (int i = failedRecords.size(); --i >= 0;) { IRecordInternal record = failedRecords.get(i); if (record.getParentFoundSet() == set) { al.add(record); } } } finally { editRecordsLock.unlock(); } return al.toArray(new IRecordInternal[al.size()]); } public boolean hasEditedRecords(IFoundSet foundset) { return hasEditedRecords(foundset, true); } public boolean hasEditedRecords(IFoundSet foundset, boolean testForRemoves) { // TODO don't we have to check for any related foundset edits here as well? if (testForRemoves) { removeUnChangedRecords(false, false, foundset); } editRecordsLock.lock(); try { List<IRecordInternal> allEditing = new ArrayList<IRecordInternal>(); allEditing.addAll(editedRecords); allEditing.addAll(failedRecords); for (int i = allEditing.size(); --i >= 0;) { IRecordInternal record = allEditing.get(i); if (record.getParentFoundSet() == foundset) { return true; } } } finally { editRecordsLock.unlock(); } return false; } private boolean isSavingAll = false; private List<IRecord> savingRecords = new ArrayList<IRecord>(); private Exception lastStopEditingException; private boolean ignoreSave; public int stopIfEditing(IFoundSet fs) { if (hasEditedRecords(fs)) { return stopEditing(false); } return ISaveConstants.STOPPED; } public int stopEditing(boolean javascriptStop) { return stopEditing(javascriptStop, (List<IRecord>)null, 0); } /** * stop/save * * @param javascriptStop * @param recordToSave null means all records * @return IRowChangeListener static final */ public int stopEditing(boolean javascriptStop, IRecord recordToSave) { return stopEditing(javascriptStop, Arrays.asList(new IRecord[] { recordToSave }), 0); } /** * stop/save * * @param javascriptStop * @param recordsToSave null means all records * @return IRowChangeListener static final */ public int stopEditing(boolean javascriptStop, List<IRecord> recordsToSave) { return stopEditing(javascriptStop, recordsToSave, 0); } private int stopEditing(final boolean javascriptStop, List<IRecord> recordsToSave, int recursionDepth) { if (recursionDepth > 50) { fsm.getApplication().reportJSError("stopEditing max recursion exceeded", new RuntimeException()); return ISaveConstants.SAVE_FAILED; } if (ignoreSave) { return ISaveConstants.AUTO_SAVE_BLOCKED; } if (isSavingAll) { // we are saving all, no need to save anything more return ISaveConstants.STOPPED; } if (recordsToSave == null && savingRecords.size() > 0) { // we are saving some records, cannot call save all now, not supported return ISaveConstants.STOPPED; } if (recordsToSave != null && savingRecords.size() > 0) { // make a copy to be sure that removeAll is supported recordsToSave = new ArrayList<IRecord>(recordsToSave); recordsToSave.removeAll(savingRecords); } if (recordsToSave != null) { boolean hasEditedRecords = false; editRecordsLock.lock(); try { for (IRecord record : recordsToSave) { if (editedRecords.contains(record)) { hasEditedRecords = true; break; } } } finally { editRecordsLock.unlock(); } if (!hasEditedRecords) return ISaveConstants.STOPPED; } // here we can't have a test if editedRecords is empty (and return stop) // because for just globals or findstates (or deleted records) // we need to pass prepareForSave. final List<IRecord> recordsToSaveFinal = recordsToSave; if (!fsm.getApplication().isEventDispatchThread()) { // only the event dispatch thread can stop an current edit. // this is a fix for innerworkings because background aggregate queries seems to also trigger saves Debug.trace("Stop edit postponend because it is not in the event dispatch thread: " + Thread.currentThread().getName()); //$NON-NLS-1$ // calculations running from lazy table view loading threads may trigger stopEditing fsm.getApplication().invokeLater(new Runnable() { public void run() { // do not stop if the user is editing something else. boolean stop; editRecordsLock.lock(); try { stop = editedRecords.size() == 1 && recordsToSaveFinal != null && recordsToSaveFinal.size() == 1 && editedRecords.get(0) == recordsToSaveFinal.get(0); } finally { editRecordsLock.unlock(); } if (stop) { stopEditing(javascriptStop, recordsToSaveFinal); } else { Debug.trace("Stop edit skipped because other records are being edited"); //$NON-NLS-1$ } } }); return ISaveConstants.AUTO_SAVE_BLOCKED; } int editedRecordsSize; try { int p = prepareForSave(true); if (p != ISaveConstants.STOPPED) { return p; } if (recordsToSave == null) { isSavingAll = true; } else { savingRecords.addAll(recordsToSave); } //remove any non referenced failed records boolean fireChange = false; editRecordsLock.lock(); try { if (failedRecords.size() != 0) { Iterator<IRecordInternal> it = failedRecords.iterator(); while (it.hasNext()) { IRecordInternal rec = it.next(); if (rec != null) { if (rec.getParentFoundSet() == null) { it.remove(); } else if (rec.getParentFoundSet().getRecordIndex(rec) == -1) { it.remove(); } } } if (failedRecords.size() == 0) { fireChange = true; } } } finally { editRecordsLock.unlock(); } if (fireChange) fireEditChange(); // remove the unchanged, really calculate when it is a real stop (autosave = true or it is a javascript stop) removeUnChangedRecords(autoSave || javascriptStop, true); //check if anything left int editRecordListSize; editRecordsLock.lock(); try { editRecordListSize = editedRecords.size(); if (editRecordListSize == 0) return ISaveConstants.STOPPED; } finally { editRecordsLock.unlock(); } //cannot stop, its blocked if (!autoSave && !javascriptStop) { return ISaveConstants.AUTO_SAVE_BLOCKED; } int failedCount = 0; lastStopEditingException = null; List<RowUpdateInfo> rowUpdates = new ArrayList<RowUpdateInfo>(editRecordListSize); editRecordsLock.lock(); try { Map<IRecordInternal, Integer> processed = new HashMap<>(); for (IRecordInternal tmp = getFirstElement(editedRecords, recordsToSave); tmp != null; tmp = getFirstElement(editedRecords, recordsToSave)) { // check if we do not have an infinite recursive loop Integer count = processed.get(tmp); if (count != null && count.intValue() > 50) { fsm.getApplication().reportJSError( "stopEditing max loop counter exceeded on " + tmp.getParentFoundSet().getDataSource() + "/" + tmp.getPKHashKey(), new RuntimeException()); return ISaveConstants.SAVE_FAILED; } processed.put(tmp, Integer.valueOf(count == null ? 1 : (count.intValue() + 1))); if (tmp instanceof Record) { Record record = (Record)tmp; //prevent multiple update for the same row (from multiple records) for (int j = 0; j < rowUpdates.size(); j++) { if (rowUpdates.get(j).getRow() == record.getRawData()) { // create a new rowUpdate that contains both updates RowUpdateInfo removed = rowUpdates.remove(j); recordTested.remove(removed.getRecord()); break; } } try { // test for table events; this may execute table events if the user attached JS methods to them; // the user might add/delete/edit records in the JS - thus invalidating a normal iterator (it) // - edited record list changes; this is why an AllowListModificationIterator is used // Note that the behaviour is different when trigger returns false or when it throws an exception. // when the trigger returns false, record must stay in editedRecords. // this is needed because the trigger may be used as validation to keep the user in the record when autosave=true. // when the trigger throws an exception, the record must move from editedRecords to failedRecords so that in // scripting the failed records can be examined (the thrown value is retrieved via record.exception.getValue()) editRecordsLock.unlock(); try { if (!((FoundSet)record.getParentFoundSet()).executeFoundsetTriggerBreakOnFalse(new Object[] { record }, record.existInDataSource() ? StaticContentSpecLoader.PROPERTY_ONUPDATEMETHODID : StaticContentSpecLoader.PROPERTY_ONINSERTMETHODID, true)) // throws ServoyException when trigger method throws exception { // just directly return if one returns false. return ISaveConstants.VALIDATION_FAILED; } } finally { editRecordsLock.lock(); } RowUpdateInfo rowUpdateInfo = getRecordUpdateInfo(record); if (rowUpdateInfo != null) { rowUpdateInfo.setRecord(record); rowUpdates.add(rowUpdateInfo); } else { recordTested.remove(record); } } catch (ServoyException e) { log.debug("stopEditing(" + javascriptStop + ") encountered an exception - could be expected and treated by solution code or not", //$NON-NLS-1$//$NON-NLS-2$ e); // trigger method threw exception lastStopEditingException = e; failedCount++; record.getRawData().setLastException(e); //set latest if (!failedRecords.contains(record)) { failedRecords.add(record); } recordTested.remove(record); } catch (Exception e) { Debug.error("Not a normal Servoy/Db Exception generated in saving record: " + record + " removing the record", e); //$NON-NLS-1$ //$NON-NLS-2$ recordTested.remove(record); } } else { // find state recordTested.remove(tmp); } editedRecords.remove(tmp); } } finally { editRecordsLock.unlock(); } if (failedCount > 0) { if (!(lastStopEditingException instanceof ServoyException)) { lastStopEditingException = new ApplicationException(ServoyException.SAVE_FAILED, lastStopEditingException); } if (!javascriptStop) fsm.getApplication().handleException(fsm.getApplication().getI18NMessage("servoy.formPanel.error.saveFormData"), //$NON-NLS-1$ lastStopEditingException); return ISaveConstants.SAVE_FAILED; } if (rowUpdates.size() == 0) { fireEditChange(); if (Debug.tracing()) { Debug.trace("no records to update anymore, failed: " + failedRecords.size()); //$NON-NLS-1$ } return ISaveConstants.STOPPED; } if (Debug.tracing()) { Debug.trace("Updating/Inserting " + rowUpdates.size() + " records: " + rowUpdates.toString()); //$NON-NLS-1$ //$NON-NLS-2$ } RowUpdateInfo[] infos = rowUpdates.toArray(new RowUpdateInfo[rowUpdates.size()]); if (infos.length > 1 && !disableInsertsReorder) { // search if there are new row pks used that are // used in records before this record and sort it based on that. boolean changed = false; List<RowUpdateInfo> al = new ArrayList<RowUpdateInfo>(Arrays.asList(infos)); int prevI = -1; outer : for (int i = al.size(); --i > 0;) { Row row = al.get(i).getRow(); // only test for new rows and its pks. if (row.existInDB()) continue; String[] pkColumns = row.getRowManager().getSQLSheet().getPKColumnDataProvidersAsArray(); Object[] pk = row.getPK(); for (int j = 0; j < pk.length; j++) { Object pkObject = pk[j]; // special case if pk was db ident and that value was copied from another row. if (pkObject instanceof DbIdentValue && ((DbIdentValue)pkObject).getRow() != row) continue; for (int k = 0; k < i; k++) { RowUpdateInfo updateInfo = al.get(k); Object[] values = updateInfo.getRow().getRawColumnData(); int[] pkIndexes = updateInfo.getFoundSet().getSQLSheet().getPKIndexes(); IntHashMap<String> pks = new IntHashMap<String>(pkIndexes.length, 1); for (int pkIndex : pkIndexes) { pks.put(pkIndex, ""); //$NON-NLS-1$ } for (int l = 0; l < values.length; l++) { // skip all pk column indexes (except from dbidents from other rows, this may need resort). Those shouldn't be resorted if (!(values[l] instanceof DbIdentValue && ((DbIdentValue)values[l]).getRow() != updateInfo.getRow()) && pks.containsKey(l)) continue; boolean same = values[l] == pkObject; if (!same && values[l] != null) { Column pkColumn = row.getRowManager().getSQLSheet().getTable().getColumn(pkColumns[j]); if (pkColumn.hasFlag(Column.UUID_COLUMN)) { // same uuids are the same even if not the same object same = Utils.equalObjects(pkObject, values[l], 0, true); } } if (same) { al.add(k, al.remove(i)); // watch out for endless loops when 2 records both with pk's point to each other... if (prevI != i) { prevI = i; i++; } changed = true; continue outer; } } } } } if (changed) { infos = al.toArray(infos); } } ISQLStatement[] statements = new ISQLStatement[infos.length]; for (int i = 0; i < infos.length; i++) { statements[i] = infos[i].getISQLStatement(); } // TODO if one statement fails in a transaction how do we know which one? and should we rollback all rows in these statements? Object[] idents = null; try { idents = fsm.getDataServer().performUpdates(fsm.getApplication().getClientID(), statements); } catch (Exception e) { log.debug("stopEditing(" + javascriptStop + ") encountered an exception - could be expected and treated by solution code or not", e); //$NON-NLS-1$//$NON-NLS-2$ lastStopEditingException = e; if (!javascriptStop) fsm.getApplication().handleException(fsm.getApplication().getI18NMessage("servoy.formPanel.error.saveFormData"), //$NON-NLS-1$ new ApplicationException(ServoyException.SAVE_FAILED, lastStopEditingException)); return ISaveConstants.SAVE_FAILED; } if (idents.length != infos.length) { Debug.error("Should be of same size!!"); //$NON-NLS-1$ } List<RowUpdateInfo> infosToBePostProcessed = new ArrayList<RowUpdateInfo>(); Map<FoundSet, List<Record>> foundsetToRecords = new HashMap<FoundSet, List<Record>>(); Map<FoundSet, List<String>> foundsetToAggregateDeletes = new HashMap<FoundSet, List<String>>(); List<Runnable> fires = new ArrayList<Runnable>(infos.length); // Walk in reverse over it, so that related rows are update in there row manger before they are required by there parents. for (int i = infos.length; --i >= 0;) { RowUpdateInfo rowUpdateInfo = infos[i]; FoundSet foundSet = rowUpdateInfo.getFoundSet(); Row row = rowUpdateInfo.getRow(); String oldKey = row.getPKHashKey(); Record record = rowUpdateInfo.getRecord(); if (idents != null && idents.length != 0 && idents[i] != null) { Object retValue = idents[i]; if (retValue instanceof Exception) { log.debug("stopEditing(" + javascriptStop + ") encountered an exception - could be expected and treated by solution code or not", //$NON-NLS-1$//$NON-NLS-2$ (Exception)retValue); lastStopEditingException = (Exception)retValue; failedCount++; if (retValue instanceof ServoyException) { ((ServoyException)retValue).fillScriptStack(); } row.setLastException((Exception)retValue); markRecordAsFailed(record); continue; } else if (retValue instanceof Object[]) { Object[] rowData = (Object[])retValue; Object[] oldRowData = row.getRawColumnData(); if (oldRowData != null) { if (oldRowData.length == rowData.length) { for (int j = 0; j < rowData.length; j++) { if (rowData[j] instanceof BlobMarkerValue) { rowData[j] = oldRowData[j]; } if (oldRowData[j] instanceof DbIdentValue) { row.setDbIdentValue(rowData[j]); } } } else { Debug.error("Requery data has different length from row data."); } } row.setRollbackData(rowData, Row.ROLLBACK_MODE.UPDATE_CHANGES); } else if (!Boolean.TRUE.equals(retValue)) { // is db ident, can only be one column row.setDbIdentValue(retValue); } } editRecordsLock.lock(); try { recordTested.remove(record); } finally { editRecordsLock.unlock(); } if (!row.existInDB()) { // when row was not saved yet row pkhash will be with new value, pksAndRecordsHolder will have initial value foundSet.updatePk(record); } try { row.getRowManager().rowUpdated(row, oldKey, foundSet, fires); } catch (Exception e) { log.debug("stopEditing(" + javascriptStop + ") encountered an exception - could be expected and treated by solution code or not", e); //$NON-NLS-1$//$NON-NLS-2$ lastStopEditingException = e; failedCount++; row.setLastException(e); editRecordsLock.lock(); try { if (!failedRecords.contains(record)) { failedRecords.add(record); } } finally { editRecordsLock.unlock(); } } infosToBePostProcessed.add(infos[i]); List<Record> lst = foundsetToRecords.get(foundSet); if (lst == null) { lst = new ArrayList<Record>(3); foundsetToRecords.put(foundSet, lst); } lst.add(record); List<String> aggregates = foundsetToAggregateDeletes.get(foundSet); if (aggregates == null) { foundsetToAggregateDeletes.put(foundSet, rowUpdateInfo.getAggregatesToRemove()); } else { List<String> toMerge = rowUpdateInfo.getAggregatesToRemove(); for (int j = 0; j < toMerge.size(); j++) { String aggregate = toMerge.get(j); if (!aggregates.contains(aggregate)) { aggregates.add(aggregate); } } } } // run rowmanager fires in reverse order (original order because info's were processed in reverse order) -> first inserted record is fired first for (int i = fires.size(); --i >= 0;) { fires.get(i).run(); } // get the size of the edited records before the table events, so that we can look if those events did change records again. editedRecordsSize = editedRecords.size(); Record rowUpdateInfoRecord = null; for (RowUpdateInfo rowUpdateInfo : infosToBePostProcessed) { try { rowUpdateInfoRecord = rowUpdateInfo.getRecord(); ((FoundSet)rowUpdateInfoRecord.getParentFoundSet()).executeFoundsetTrigger(new Object[] { rowUpdateInfoRecord }, rowUpdateInfo.getISQLStatement().getAction() == ISQLActionTypes.INSERT_ACTION ? StaticContentSpecLoader.PROPERTY_ONAFTERINSERTMETHODID : StaticContentSpecLoader.PROPERTY_ONAFTERUPDATEMETHODID, true); } catch (ServoyException e) { if (e instanceof DataException && e.getCause() instanceof JavaScriptException) { // trigger method threw exception log.debug("stopEditing(" + javascriptStop + ") encountered an exception - could be expected and treated by solution code or not", e); //$NON-NLS-1$//$NON-NLS-2$ lastStopEditingException = e; failedCount++; rowUpdateInfoRecord.getRawData().setLastException(e); editRecordsLock.lock(); try { if (!failedRecords.contains(rowUpdateInfoRecord)) { failedRecords.add(rowUpdateInfoRecord); } } finally { editRecordsLock.unlock(); } } else { fsm.getApplication().handleException("Failed to execute after update/insert trigger.", e); //$NON-NLS-1$ } } } for (Map.Entry<FoundSet, List<Record>> entry : foundsetToRecords.entrySet()) { FoundSet fs = entry.getKey(); fs.recordsUpdated(entry.getValue(), foundsetToAggregateDeletes.get(fs)); } boolean shouldFireEditChange; editRecordsLock.lock(); try { shouldFireEditChange = editedRecords.size() == 0; } finally { editRecordsLock.unlock(); } if (shouldFireEditChange) { fireEditChange(); } if (failedCount > 0) { if (!javascriptStop) { lastStopEditingException = new ApplicationException(ServoyException.SAVE_FAILED, lastStopEditingException); fsm.getApplication().handleException(fsm.getApplication().getI18NMessage("servoy.formPanel.error.saveFormData"), //$NON-NLS-1$ lastStopEditingException); } return ISaveConstants.SAVE_FAILED; } } catch (RuntimeException e) { if (e instanceof IllegalArgumentException) { fsm.getApplication().handleException(null, new ApplicationException(ServoyException.INVALID_INPUT, e)); return ISaveConstants.SAVE_FAILED; } else if (e instanceof IllegalStateException) { fsm.getApplication().handleException(fsm.getApplication().getI18NMessage("servoy.formPanel.error.saveFormData"), e); //$NON-NLS-1$ return ISaveConstants.SAVE_FAILED; } else { Debug.error(e); throw e; } } finally { if (recordsToSave == null) { isSavingAll = false; } else { savingRecords.removeAll(recordsToSave); } fireEvents(); } if (editedRecords.size() != editedRecordsSize && recordsToSave == null) { // records where changed by the after insert/update table events, call stop edit again if this was not a specific record save. return stopEditing(javascriptStop, null, recursionDepth + 1); } return ISaveConstants.STOPPED; } /** * Get the first element of a list, filter on subList when not null; */ private static IRecordInternal getFirstElement(List<IRecordInternal> records, List<IRecord> subList) { for (IRecordInternal record : records) { if (subList == null || subList.contains(record)) { return record; } } return null; } /** * Mark record as failed, move to failed records, remove from editedRecords if it was in there. * @param record */ void markRecordAsFailed(IRecordInternal record) { editRecordsLock.lock(); try { editedRecords.remove(record); if (!failedRecords.contains(record)) { failedRecords.add(record); } recordTested.remove(record); } finally { editRecordsLock.unlock(); } } public int prepareForSave(boolean looseFocus) { if (preparingForSave) { return ISaveConstants.STOPPED; } try { preparingForSave = true; //stop UI editing and fire onRecordEditStop Event if (!firePrepareForSave(looseFocus)) { Debug.trace("no stop editing because prepare for save stopped it"); //$NON-NLS-1$ return ISaveConstants.VALIDATION_FAILED; } return ISaveConstants.STOPPED; } finally { preparingForSave = false; } } /* * _____________________________________________________________ Methods for data manipulation */ private RowUpdateInfo getRecordUpdateInfo(IRecordInternal state) throws ServoyException { Table table = state.getParentFoundSet().getSQLSheet().getTable(); RowManager rowManager = fsm.getRowManager(fsm.getDataSource(table)); Row rowData = state.getRawData(); boolean doesExistInDB = rowData.existInDB(); if (doesExistInDB && !hasAccess(table, IRepository.UPDATE)) { throw new ApplicationException(ServoyException.NO_MODIFY_ACCESS); } GlobalTransaction gt = fsm.getGlobalTransaction(); if (gt != null) { gt.addRecord(table.getServerName(), state); } RowUpdateInfo rowUpdateInfo = rowManager.getRowUpdateInfo(rowData, hasAccess(table, IRepository.TRACKING)); return rowUpdateInfo; } private boolean testIfRecordIsChanged(IRecordInternal record, boolean checkCalcValues) { Row rowData = record.getRawData(); if (rowData == null) return false; if (checkCalcValues && record instanceof Record) { ((Record)record).validateStoredCalculations(); } return rowData.isChanged(); } public void removeUnChangedRecords(boolean checkCalcValues, boolean doActualRemove) { removeUnChangedRecords(checkCalcValues, doActualRemove, null); } public void removeUnChangedRecords(boolean checkCalcValues, boolean doActualRemove, IFoundSet foundset) { if (preparingForSave) return; // Test the edited records if they are changed or not. Object[] editedRecordsArray = null; editRecordsLock.lock(); try { editedRecordsArray = editedRecords.toArray(); } finally { editRecordsLock.unlock(); } for (Object element : editedRecordsArray) { IRecordInternal record = (IRecordInternal)element; if ((foundset == null || record.getParentFoundSet() == foundset) && !testIfRecordIsChanged(record, checkCalcValues)) { if (doActualRemove) { removeEditedRecord(record); } else { stopEditing(true, record); } } } } public void removeEditedRecord(IRecordInternal r) { int size; editRecordsLock.lock(); try { editedRecords.remove(r); recordTested.remove(r); failedRecords.remove(r); size = editedRecords.size(); } finally { editRecordsLock.unlock(); } if (size == 0) { fireEditChange(); } } void removeEditedRecords(FoundSet set) { Object[] editedRecordsArray = null; editRecordsLock.lock(); try { editedRecordsArray = editedRecords.toArray(); } finally { editRecordsLock.unlock(); } for (Object element : editedRecordsArray) { IRecordInternal record = (IRecordInternal)element; if (record.getParentFoundSet() == set) { removeEditedRecord(record); } } } public boolean startEditing(IRecordInternal record, boolean mustFireEditRecordChange) { if (record == null) { throw new IllegalArgumentException(fsm.getApplication().getI18NMessage("servoy.foundSet.error.editNullRecord")); //$NON-NLS-1$ } // only IRowListeners can go into edit.. (SubSummaryFoundSet records can't) if (!(record.getParentFoundSet() instanceof IRowListener)) return true; if (isEditing(record)) { editRecordsLock.lock(); try { recordTested.remove(record); } finally { editRecordsLock.unlock(); } return true; } boolean isEditing = record instanceof FindState; if (!isEditing) { if (record.existInDataSource()) { if (hasAccess(record.getParentFoundSet().getSQLSheet().getTable(), IRepository.UPDATE)) { isEditing = !record.isLocked();// enable only if not locked by someone else } else { // TODO throw exception?? // Error handler ??? } } else { if (hasAccess(record.getParentFoundSet().getSQLSheet().getTable(), IRepository.INSERT)) { isEditing = true; } else { // TODO throw exception?? // Error handler ??? } } } if (isEditing) { if (mustFireEditRecordChange) isEditing = fireEditRecordStart(record); // find states also to the global foundset manager?? if (isEditing) { int editRecordsSize = 0; editRecordsLock.lock(); try { // editRecordStop should be called for this record to match the editRecordStop call recordTested.remove(record); // extra check if no other thread already added this record if (!editedRecords.contains(record)) { editedRecords.add(record); editRecordsSize = editedRecords.size(); } failedRecords.remove(record); // reset the exception so that it is tried again. record.getRawData().setLastException(null); } finally { editRecordsLock.unlock(); } if (editRecordsSize == 1) { fireEditChange(); } } } return isEditing; } /** * */ public void clearSecuritySettings() { accessMap.clear(); } public boolean hasAccess(Table table, int flag) { if (table == null) { // no table, cannot insert/update/delete return (flag & (IRepository.INSERT | IRepository.UPDATE | IRepository.DELETE)) == 0; } Integer access = accessMap.get(table); if (access == null) { try { Iterator<String> it = table.getRowIdentColumnNames(); if (it.hasNext()) { String cname = it.next(); //access is based on columns, but we don't yet use that fine grained level, its table only access = Integer.valueOf( fsm.getApplication().getFlattenedSolution().getSecurityAccess(Utils.getDotQualitfied(table.getServerName(), table.getName(), cname))); } else { access = Integer.valueOf(-2); } accessMap.put(table, access); } catch (Exception ex) { Debug.error(ex); return false; } } if (access.intValue() == -1 && (flag == IRepository.TRACKING || flag == IRepository.TRACKING_VIEWS)) //deny tracking if security not is specified { return false; } return ((access.intValue() & flag) != 0); } protected boolean fireEditRecordStart(final IRecordInternal record) { Object[] array = prepareForSaveListeners.toArray(); for (Object element : array) { IPrepareForSave listener = (IPrepareForSave)element; if (!listener.recordEditStart(record)) { return false; } } return true; } protected void fireEditChange() { int editRecordsSize = 0; int failedRecordsSize = 0; editRecordsLock.lock(); try { editRecordsSize = editedRecords.size(); failedRecordsSize = failedRecords.size(); } finally { editRecordsLock.unlock(); } GlobalEditEvent e = new GlobalEditEvent(this, editRecordsSize > 0 || failedRecordsSize > 0); Object[] array = editListeners.toArray(); for (Object element : array) { IGlobalEditListener listener = (IGlobalEditListener)element; listener.editChange(e); } if (editRecordsSize == 0) { fsm.performActionIfRequired(); } } public synchronized Map<FoundSet, int[]> getFoundsetEventMap() { if (fsEventMap == null) { fsEventMap = new ConcurrentHashMap<FoundSet, int[]>(); } return fsEventMap; } public void fireEvents() { Map<FoundSet, int[]> map = null; synchronized (this) { if (fsEventMap == null || isSavingAll || savingRecords.size() > 0) return; map = fsEventMap; fsEventMap = null; } Iterator<Entry<FoundSet, int[]>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<FoundSet, int[]> entry = it.next(); int[] indexen = entry.getValue(); FoundSet fs = entry.getKey(); if (indexen[0] < 0 && indexen[1] < 0) { // fire foundset-invalidated fs.fireFoundSetEvent(-1, -1, FoundSetEvent.FOUNDSET_INVALIDATED); } else { fs.fireFoundSetEvent(indexen[0], indexen[1], FoundSetEvent.CHANGE_UPDATE); } } // call until the map is null.. fireEvents(); } public void addEditListener(IGlobalEditListener editListener) { editListeners.add(editListener); } public void removeEditListener(IGlobalEditListener editListener) { editListeners.remove(editListener); } protected boolean firePrepareForSave(boolean looseFocus) { Object[] array = prepareForSaveListeners.toArray(); for (Object element : array) { IPrepareForSave listener = (IPrepareForSave)element; if (!listener.prepareForSave(looseFocus)) { return false; } } return true; } public void addPrepareForSave(IPrepareForSave prepareForSave) { prepareForSaveListeners.add(prepareForSave); } public void removePrepareForSave(IPrepareForSave prepareForSave) { prepareForSaveListeners.remove(prepareForSave); } /** * @param autoSave */ public boolean setAutoSave(boolean autoSave) { if (this.autoSave != autoSave) { if (autoSave) { if (stopEditing(true) != ISaveConstants.STOPPED) { return false; } } this.autoSave = autoSave; } return true; } public boolean getAutoSave() { return autoSave; } public void rollbackRecords() { ArrayList<IRecordInternal> array = new ArrayList<IRecordInternal>(); editRecordsLock.lock(); try { recordTested.clear(); array.addAll(failedRecords); array.addAll(editedRecords); } finally { editRecordsLock.unlock(); } rollbackRecords(array); } public void rollbackRecords(List<IRecordInternal> records) { ArrayList<IRecordInternal> array = new ArrayList<IRecordInternal>(); editRecordsLock.lock(); try { for (IRecordInternal record : records) { recordTested.remove(record); if (failedRecords.remove(record)) array.add(record); if (editedRecords.contains(record)) array.add(record); } } finally { editRecordsLock.unlock(); } if (array.size() > 0) { // The "existsInDB" property sometimes changes while iterating over the array // below (for example when we have several Records that point to the same Row // and the Row is not yet stored in the database). So we memorize the initial // values of "existsInDB" and we use them while iterating over the array. boolean[] existsInDB = new boolean[array.size()]; for (int i = 0; i < array.size(); i++) { existsInDB[i] = array.get(i).existInDataSource(); } for (int i = 0; i < array.size(); i++) { IRecordInternal element = array.get(i); // TODO all fires in rollback should be accumulated and done here at once. element.getRawData().rollbackFromOldValues();//we also rollback !existsInDB records, since they can be held in variables if (!existsInDB[i]) { element.getRawData().remove(); } } editRecordsLock.lock(); try { if (editedRecords.size() > 0) { editedRecords.removeAll(array); } } finally { editRecordsLock.unlock(); } fireEditChange(); } } public IRecordInternal[] getEditedRecords() { removeUnChangedRecords(true, false); editRecordsLock.lock(); try { return editedRecords.toArray(new IRecordInternal[editedRecords.size()]); } finally { editRecordsLock.unlock(); } } /** * @param set * @return */ public IRecordInternal[] getUnmarkedEditedRecords(FoundSet set) { List<IRecordInternal> al = new ArrayList<IRecordInternal>(); editRecordsLock.lock(); try { for (int i = editedRecords.size(); --i >= 0;) { IRecordInternal record = editedRecords.get(i); if (record.getParentFoundSet() == set && !recordTested.contains(record)) { al.add(record); } } } finally { editRecordsLock.unlock(); } return al.toArray(new IRecordInternal[al.size()]); } /** * @param record */ public void markRecordTested(IRecordInternal record) { editRecordsLock.lock(); try { if (!recordTested.contains(record)) { recordTested.add(record); } } finally { editRecordsLock.unlock(); } } public void init() { editRecordsLock.lock(); try { accessMap.clear();//per table (could be per column in future) autoSave = true; preparingForSave = false; isSavingAll = false; savingRecords = new ArrayList<IRecord>(); editedRecords.clear(); failedRecords.clear(); recordTested.clear(); } finally { editRecordsLock.unlock(); } fireEditChange(); } /** * If true then the save/stopedit of records will be ignored for that time. * Do make sure that you turn this boolean back to false when you are done ignoring the possible saves. * (try/finally) * * @param ignore */ public void ignoreSave(boolean ignore) { this.ignoreSave = ignore; } }