/* 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.rmi.RemoteException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.servoy.j2db.persistence.ITransactable; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.util.Debug; /** * Transaction object, keeping track of db transctions (ids) * @author jblok */ public class GlobalTransaction { enum crud { Created, Updated, Deleted } private HashMap<IRecordInternal, crud> records;//all rows to prevent they are gc'ed private Map<String, String> serverTransactions;//serverName->transactionID private final IDataServer dataServer; private final String clientID; private final List<ITransactable> transactionEndListeners = new ArrayList<ITransactable>(); GlobalTransaction(IDataServer ds, String cid) { serverTransactions = new HashMap<String, String>(); records = new HashMap<IRecordInternal, crud>(); dataServer = ds; clientID = cid; } Collection<String> rollback(boolean queryForNewData, boolean revertSavedRecords) { if (serverTransactions != null) { String[] tids = new String[serverTransactions.size()]; serverTransactions.values().toArray(tids); try { dataServer.endTransactions(clientID, tids, false); } catch (Exception e) { Debug.error(e); } } Collection<String> collection; if (queryForNewData && !records.isEmpty()) { collection = new HashSet<String>(); try { for (Entry<IRecordInternal, crud> entry : records.entrySet()) { // TODO this loop can be optimized. // Search for all the rows with the same table and do a in query (per 200 rows..) instead of possibly querying them 1-by-1 IRecordInternal record = entry.getKey(); Row row = record.getRawData(); collection.add(record.getParentFoundSet().getDataSource()); // it is possible that during the transaction the record's foundset did find/search operations or load by query, .... // so the record might no longer be in the foundset; we can only put this record back to editing mode with changes if required by "revertSavedRecords" == false argument // if the foundset still has a reference to the record; currently Servoy checks all over the place to not have orphaned records in editing mode... // if we'd want "revertSavedRecords" == false to work in this case as well we should move the "editing" concept to Rows not just Records which is a bigger change // so only use record if not orphaned if (!revertSavedRecords) { // TODO should we also check somehow that the foundset itself is still in use? IFoundSetInternal pfs = record.getParentFoundSet(); if (pfs != null) { int idx = pfs.getRecordIndex(record); if (idx < 0) record = null; else record = pfs.getRecord(idx); } else record = null; } else record = null; // otherwise following impl. should not need record, just row // call startEdit with false so that start edit on a formcontroller (of the current visible one so only for that record) // if a deleted record was first updated then just overwrite with the database. switch (entry.getValue()) { case Created : if (!revertSavedRecords && record != null && row.getRowManager().getFoundsetManager().getEditRecordList().startEditing(record, false)) { row.clearExistInDB(); } break; case Updated : if (!revertSavedRecords && record != null && row.getRowManager().getFoundsetManager().getEditRecordList().startEditing(record, false)) { // if we shouldn't revert to database values, // quickly create the old values (edit values) array row.createOldValuesIfNeeded(); // then revert by keeping the current column data values. row.rollbackFromDB(Row.ROLLBACK_MODE.KEEP_CHANGES); } else { row.rollbackFromDB(Row.ROLLBACK_MODE.OVERWRITE_CHANGES); } break; case Deleted : break; } } records = null; } catch (Exception e) { Debug.error(e); //TODO: add to application } } else { collection = Collections.<String> emptyList(); } fireTransactionEnded(false); return collection; } private void fireTransactionEnded(boolean committed) { ITransactable[] listeners = null; synchronized (transactionEndListeners) { if (transactionEndListeners.size() > 0) { listeners = transactionEndListeners.toArray(new ITransactable[transactionEndListeners.size()]); transactionEndListeners.clear(); } } if (listeners != null) { for (ITransactable listener : listeners) { if (committed) { listener.processPostCommit(); } else { listener.processPostRollBack(); } } } } Collection<String> commit(boolean revertSavedRecords) { String[] tids = new String[serverTransactions.size()]; serverTransactions.values().toArray(tids); try { dataServer.endTransactions(clientID, tids, true); fireTransactionEnded(true); records = null; } catch (Exception e) { Debug.error(e);//TODO: add to application } if (records != null) { serverTransactions = new HashMap<String, String>();//clear, so they are not processes on IDataService return rollback(true, revertSavedRecords);//all others } return null; } String addRecord(String serverName, IRecordInternal r) throws RepositoryException { if (!records.containsKey(r)) { records.put(r, r.existInDataSource() ? crud.Updated : crud.Created); } return getTransactionID(serverName); } void addDeletedRecord(IRecordInternal r) { records.put(r, crud.Deleted); } public synchronized String getTransactionID(String serverName) throws RepositoryException { try { //check if transaction exist for a server String tid = serverTransactions.get(serverName); if (tid == null) { //create tid = dataServer.startTransaction(clientID, serverName); //store serverTransactions.put(serverName, tid); } return tid; } catch (RemoteException e) { throw new RepositoryException(e); } } public void addTransactionEndListener(ITransactable l) { synchronized (transactionEndListeners) { if (!transactionEndListeners.contains(l)) { transactionEndListeners.add(l); } } } public void removeTransactionEndListener(ITransactable l) { synchronized (transactionEndListeners) { transactionEndListeners.remove(l); } } }