I removed old unused debugging code, applied some coding conventions and shortened some routines.
I also removed the exception you would normally get when you add or remove a node twice from a transaction.
I think this should work, but thusfar I only tested it using the Editwizards/Dove
I'm still not totally satisfied with how the code works, but a better cleanup requires adaptations in MMObjectNode and either removal or change of the TransactionManagerInterface.
Anyway, perhaps people who have working transaction examples (and who ran into trouble earlier) can test these classes?
They work on the 1.7 code.
-- Pierre van Rooden Mediapark, C 107 tel. +31 (0)35 6772815 "Never summon anything bigger than your head."
/*
This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. The license (Mozilla version 1.0) can be read at the MMBase site. See http://www.MMBase.org/license */ package org.mmbase.module.core; import java.util.*; import org.mmbase.module.corebuilders.*; import org.mmbase.util.logging.*; /** * @author Rico Jansen * @version $Id: TransactionResolver.java,v 1.13 2002/04/17 13:17:43 pierre Exp $ */ public class TransactionResolver { private static Logger log = Logging.getLoggerInstance(TransactionResolver.class.getName()); private MMBase mmbase; public TransactionResolver(MMBase mmbase) { this.mmbase = mmbase; } public boolean resolve(Collection nodes) throws TransactionManagerException { Map numbers = new HashMap(); Map nnodes = new HashMap(); boolean success = true; // Find all unique keys and store them in a map to remap them later // Also store the nodes with which fields uses them. for (Iterator i = nodes.iterator(); i.hasNext();) { MMObjectNode node = (MMObjectNode)i.next(); MMObjectBuilder bul = mmbase.getMMObject(node.getName()); log.debug("TransactionResolver - builder " + node.getName() + " builder " + bul); for (Enumeration f=bul.getFields().elements();f.hasMoreElements();) { FieldDefs fd = (FieldDefs)f.nextElement(); int dbtype = fd.getDBType(); log.debug("TransactionResolver - type " + dbtype + "," + fd.getDBName() + "," + fd.getDBState()); if ((dbtype == FieldDefs.TYPE_INTEGER)|| (dbtype == FieldDefs.TYPE_NODE)) { int state = fd.getDBState(); if (state == FieldDefs.DBSTATE_PERSISTENT || state == FieldDefs.DBSTATE_SYSTEM) { // Database field of type integer String field = fd.getDBName(); String tmpfield = "_" + field; if (node.getDBState(tmpfield) == FieldDefs.DBSTATE_VIRTUAL) { int ikey = node.getIntValue(field); if (ikey < 0) { // Key is not set String key = node.getStringValue(tmpfield); if (key!=null) { log.debug("TransactionResolver - key,field " + field + " - " + key); // keep fieldnumber key if (!numbers.containsKey(key)) numbers.put(key,new Integer(-1)); // keep node + field to change Collection changedFields = (Collection)nnodes.get(node); if (changedFields!=null) { changedFields.add(field); } else { changedFields=new ArrayList(); changedFields.add(field); nnodes.put(node,changedFields); } } else { log.debug("TransactionResolver - Can't find key for field " + tmpfield + " node "+node+" (warning)"); } if (field.equals("number")) node.setValue("_exists",TransactionManager.EXISTS_NO); } else { // Key is already set log.debug("TransactionResolver - Key for value " + field + " is already set "+ikey); // Mark it as existing if (field.equals("number")) { // test for remove here String exists=node.getStringValue("_exists"); if (exists == null || !exists.equals(TransactionManager.EXISTS_NOLONGER)) { node.setValue("_exists",TransactionManager.EXISTS_YES); } String key=node.getStringValue(tmpfield); if (key!=null) { numbers.put( key, new Integer(ikey)); } else { log.debug("TransactionResolver - Can't find key for field "+tmpfield+" node "+node); } } } } else { log.debug("TransctionResolver - DBstate for "+tmpfield+" is not set to 0 but is "+node.getDBState(field)); } } } } } log.debug("TransactionResolver - nnodes "+nnodes); // Get the numbers for (Iterator i = numbers.entrySet().iterator(); i.hasNext();) { Map.Entry numberEntry = (Map.Entry)i.next(); Object key = numberEntry.getKey(); Integer num = (Integer)numberEntry.getValue(); if (num.intValue() == -1) { numbers.put(key, new Integer(mmbase.getDBKey())); } } // put numbers in the right place for (Iterator i = nnodes.entrySet().iterator(); i.hasNext();) { Map.Entry nnodeEntry = (Map.Entry)i.next(); MMObjectNode node = (MMObjectNode)nnodeEntry.getKey(); Collection changedFields = (Collection)nnodeEntry.getValue(); for (Iterator j = changedFields.iterator(); j.hasNext();) { String field = (String)j.next(); String tmpfield = "_"+field; String key = node.getStringValue(tmpfield); int number = ((Integer)numbers.get(key)).intValue(); node.setValue(field, number); } } for (Iterator i = nodes.iterator(); i.hasNext();) { MMObjectNode node = (MMObjectNode)i.next(); MMObjectBuilder bul=mmbase.getMMObject(node.getName()); for (Iterator j = bul.getFields().iterator();j.hasNext();) { FieldDefs fd = (FieldDefs)j.next(); int dbtype = fd.getDBType(); if ((dbtype == FieldDefs.TYPE_INTEGER)|| (dbtype == FieldDefs.TYPE_NODE)) { String field = fd.getDBName(); int number = node.getIntValue(field); if (number == -1) { String tmpfield = "_"+field; if (node.getDBState(tmpfield) == 0) { String key = node.getStringValue(tmpfield); if (key != null && key.length() > 0) { success = false; } } } } } } return success; } }
/* This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. The license (Mozilla version 1.0) can be read at the MMBase site. See http://www.MMBase.org/license */ package org.mmbase.bridge.implementation; import java.util.*; import org.mmbase.bridge.*; import org.mmbase.module.core.*; import org.mmbase.util.logging.*; /** * The absic implementation for a Transaction cLoud. * A Transaction cloud is a cloud which buffers allc hanegs made to nodes - * which means that chanegs are committed only if you commit the transaction itself. * This mechanism allows you to rollback changes if something goes wrong. * @author Pierre van Rooden * @version $Id: BasicTransaction.java,v 1.15 2003/12/17 09:16:44 michiel Exp $ */ public class BasicTransaction extends BasicCloud implements Transaction { private static final Logger log = Logging.getLoggerInstance(BasicTransaction.class); /** * The id of the transaction for use with the transaction manager. */ protected String transactionContext; /** * The name of the transaction as used by the user. */ protected String transactionName = null; /* * Constructor to call from the CloudContext class. * Package only, so cannot be reached from a script. * @param transactionName name of the transaction (assigned by the user) * @param cloud The cloud this transaction is working on */ BasicTransaction(String transactionName, BasicCloud cloud) { super(transactionName, cloud); this.transactionName=transactionName; // if the parent cloud is itself a transaction, // do not create a new one, just use that context instead! // this allows for nesting of transactions without loosing performance // due to additional administration if (parentCloud instanceof BasicTransaction) { transactionContext= ((BasicTransaction)parentCloud).transactionContext; } else { try { // XXX: the current transaction manager does not allow multiple transactions with the // same name for different users // We solved this here, but this should really be handled in the Transactionmanager. transactionContext = BasicCloudContext.transactionManager.create(account, account+"_"+transactionName); } catch (TransactionManagerException e) { String message = e.getMessage(); log.error(message); throw new BridgeException(message, e); } } } public boolean commit() { if (transactionContext==null) { throw new BridgeException("No valid transaction : " + name); } // if this is a transaction within a transaction (theoretically possible) // leave the committing to the 'parent' transaction if (parentCloud instanceof Transaction) { // do nothing } else { try { // BasicCloudContext.transactionManager.commit(account, transactionContext); BasicCloudContext.transactionManager.commit(userContext.getUserContext(), transactionContext); } catch (TransactionManagerException e) { // do we drop the transaction here or delete the trans context? // return false; String message = e.getMessage(); log.error(message); throw new BridgeException(message, e); } } // remove the transaction from the parent cloud parentCloud.transactions.remove(transactionName); // clear the transactioncontext transactionContext=null; return true; } public void cancel() { if (transactionContext==null) { throw new BridgeException("No valid transaction : " + name); } // if this is a transaction within a transaction (theoretically possible) // call the 'parent' transaction to cancel everything if (parentCloud instanceof Transaction) { ((Transaction)parentCloud).cancel(); } else { try { // BasicCloudContext.transactionManager.cancel(account, transactionContext); BasicCloudContext.transactionManager.cancel(userContext.getUserContext(), transactionContext); } catch (TransactionManagerException e) { // do we drop the transaction here or delete the trans context? String message = e.getMessage(); log.error(message); throw new BridgeException(message,e); } } // remove the transaction from the parent cloud parentCloud.transactions.remove(transactionName); // clear the transactioncontext transactionContext=null; } /* * Transaction-notification: add a new temporary node to a transaction. * @param currentObjectContext the context of the object to add */ void add(String currentObjectContext) { try { BasicCloudContext.transactionManager.addNode(transactionContext, account,currentObjectContext); } catch (TransactionManagerException e) { String message = e.getMessage(); log.error(message); throw new BridgeException(message,e); } } /* * Transaction-notification: remove a temporary (not yet committed) node in a transaction. * @param currentObjectContext the context of the object to remove */ void remove(String currentObjectContext) { try { BasicCloudContext.transactionManager.removeNode(transactionContext,account,currentObjectContext); } catch (TransactionManagerException e) { String message = e.getMessage(); log.error(message); throw new BridgeException(message,e); } } /* * Transaction-notification: remove an existing node in a transaction. * @param currentObjectContext the context of the object to remove */ void delete(String currentObjectContext) { try { BasicCloudContext.transactionManager.deleteObject(transactionContext,account,currentObjectContext); } catch (TransactionManagerException e) { String message = e.getMessage(); log.error(message); throw new BridgeException(message,e); } } boolean contains(MMObjectNode node) { // additional check, so transaction can still get nodes after it has committed. if (transactionContext==null) { return false; } try { Collection transaction = BasicCloudContext.transactionManager.get(account,transactionContext); return transaction.contains(node); } catch (TransactionManagerException tme) { String message = tme.getMessage(); log.error(message); throw new BridgeException(message,tme); } } /** * If this Transaction is scheduled to be garbage collected, the transaction is canceled and cleaned up. * Unless it has already been committed/canceled, ofcourse, and * unless the parentcloud of a transaction is a transaction itself. * In that case, the parent transaction should cancel! * This means that a transaction is always cleared - if it 'times out', or is not properly removed, it will * eventually be removed from the MMBase cache. */ protected void finalize() { if ((transactionContext!=null) && !(parentCloud instanceof Transaction)) { cancel(); } } }
/* This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. The license (Mozilla version 1.0) can be read at the MMBase site. See http://www.MMBase.org/license */ package org.mmbase.module.core; import java.util.*; import org.mmbase.module.corebuilders.*; import org.mmbase.util.logging.Logger; import org.mmbase.util.logging.Logging; import org.mmbase.security.*; /** * @javadoc * @author Rico Jansen * @version $Id: TransactionManager.java,v 1.26 2003/11/26 17:55:07 michiel Exp $ */ public class TransactionManager implements TransactionManagerInterface { private static final Logger log = Logging.getLoggerInstance(TransactionManager.class); public static final String EXISTS_NO = "no"; public static final int I_EXISTS_NO = 0; public static final String EXISTS_YES = "yes"; public static final int I_EXISTS_YES = 1; public static final String EXISTS_NOLONGER = "nolonger"; public static final int I_EXISTS_NOLONGER = 2; private TemporaryNodeManagerInterface tmpNodeManager; private MMBaseCop mmbaseCop = null; private MMBase mmbase; protected Map transactions = new HashMap(); protected TransactionResolver transactionResolver; public TransactionManager(MMBase mmbase,TemporaryNodeManagerInterface tmpn) { this.mmbase = mmbase; this.tmpNodeManager = tmpn; transactionResolver = new TransactionResolver(mmbase); mmbaseCop = mmbase.getMMBaseCop(); } synchronized protected Collection getTransaction(String transactionName) throws TransactionManagerException { Collection transaction = (Collection)transactions.get(transactionName); if (transaction == null) { throw new TransactionManagerException("Transaction "+transactionName+" does not exist"); } else { return transaction; } } synchronized protected Collection createTransaction(String transactionName) throws TransactionManagerException { if (!transactions.containsKey(transactionName)) { Collection transactionNodes = new Vector(); transactions.put(transactionName,transactionNodes); return transactionNodes; } else { throw new TransactionManagerException("Transaction "+transactionName+" already exists"); } } synchronized protected Collection deleteTransaction(String transactionName) { return (Collection) transactions.remove(transactionName); } /** * @deprecated use get() */ public Vector getNodes(Object user, String transactionName) { try { return (Vector)getTransaction(transactionName); } catch (TransactionManagerException tme) { return null; } } public String create(Object user, String transactionName) throws TransactionManagerException { createTransaction(transactionName); if (log.isDebugEnabled()) { log.debug("Create transaction for " + transactionName); } return transactionName; } public Collection get(Object user, String transactionName) throws TransactionManagerException { return getTransaction(transactionName); } public String addNode(String transactionName, String owner, String tmpnumber) throws TransactionManagerException { Collection transaction = getTransaction(transactionName); MMObjectNode node = tmpNodeManager.getNode(owner,tmpnumber); if (node != null) { if (!transaction.contains(node)) { transaction.add(node); // } else { // throw new TransactionManagerException( // "Node " + tmpnumber + " not added as it was already in transaction " + transactionName); } } else { throw new TransactionManagerException("Node " + tmpnumber + " doesn't exist."); } return tmpnumber; } public String removeNode(String transactionName, String owner, String tmpnumber) throws TransactionManagerException { Collection transaction = getTransaction(transactionName); MMObjectNode node = tmpNodeManager.getNode(owner, tmpnumber); if (node!=null) { if (transaction.contains(node)) { transaction.remove(node); // } else { // throw new TransactionManagerException("Node " + tmpnumber + " is not in transaction " + transactionName); } } else { throw new TransactionManagerException("Node " + tmpnumber + " doesn't exist."); } return tmpnumber; } public String deleteObject(String transactionName, String owner, String tmpnumber) throws TransactionManagerException { Collection transaction = getTransaction(transactionName); MMObjectNode node = tmpNodeManager.getNode(owner, tmpnumber); if (node!=null) { if (transaction.contains(node)) { // Mark it as deleted node.setValue("_exists",EXISTS_NOLONGER); } else { throw new TransactionManagerException("Node " + tmpnumber + " is not in transaction " + transactionName); } } else { throw new TransactionManagerException("Node " + tmpnumber + " doesn't exist."); } return tmpnumber; } public String cancel(Object user, String transactionName) throws TransactionManagerException { Collection transaction = getTransaction(transactionName); // remove nodes from the temporary node cache MMObjectBuilder builder = mmbase.getTypeDef(); for (Iterator i = transaction.iterator(); i.hasNext(); ) { MMObjectNode node=(MMObjectNode)i.next(); builder.removeTmpNode(node.getStringValue("_number")); } deleteTransaction(transactionName); if (log.isDebugEnabled()) { log.debug("Removed transaction " + transactionName + "\n" + transaction); } return transactionName; } public String commit(Object user, String transactionName) throws TransactionManagerException { Collection transaction = getTransaction(transactionName); try { boolean resolved = transactionResolver.resolve(transaction); if (!resolved) { log.error("Can't resolve transaction " + transactionName); log.error("Nodes \n" + transaction); throw new TransactionManagerException("Can't resolve transaction " + transactionName); } else { resolved = performCommits(user, transaction); if (!resolved) { log.error("Can't commit transaction " + transactionName); log.error("Nodes \n" + transaction); throw new TransactionManagerException("Can't commit transaction " + transactionName); } } } finally { // remove nodes from the temporary node cache MMObjectBuilder builder = mmbase.getTypeDef(); for (Iterator i = transaction.iterator(); i.hasNext(); ) { MMObjectNode node=(MMObjectNode)i.next(); builder.removeTmpNode(node.getStringValue("_number")); } deleteTransaction(transactionName); } return transactionName; } private final static int UNCOMMITED = 0; private final static int COMMITED = 1; private final static int FAILED = 2; private final static int NODE = 3; private final static int RELATION = 4; boolean performCommits(Object user, Collection nodes) { if (nodes == null || nodes.size() == 0) { log.warn("Empty list of nodes"); return true; } MMObjectBuilder bul = mmbase.getMMObject("typedef"); boolean okay = false; int[] nodestate = new int[nodes.size()]; int[] nodeexist = new int[nodes.size()]; String username = findUserName(user),exists; log.debug("Checking types and existence"); int i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); // Nodes are uncommited by default nodestate[i] = UNCOMMITED; exists = node.getStringValue("_exists"); if (exists == null) { throw new IllegalStateException("The _exists field does not exist on node "+node); } else if (exists.equals(EXISTS_NO)) { nodeexist[i]=I_EXISTS_NO; } else if (exists.equals(EXISTS_YES)) { nodeexist[i]=I_EXISTS_YES; } else if (exists.equals(EXISTS_NOLONGER)) { nodeexist[i]=I_EXISTS_NOLONGER; } else { throw new IllegalStateException("Invalid value for _exists on node "+node); } } log.debug("Commiting nodes"); // First commit all the NODES i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); if (!(node.getBuilder() instanceof InsRel)) { if (nodeexist[i] == I_EXISTS_YES ) { // use safe commit, which locks the node cache boolean commitOK; if (user instanceof UserContext) { commitOK = node.commit((UserContext)user); } else { commitOK = node.parent.safeCommit(node); } if (commitOK) { nodestate[i]=COMMITED; } else { nodestate[i]=FAILED; } } else if (nodeexist[i] == I_EXISTS_NO ) { int insertOK; if (user instanceof UserContext) { insertOK = node.insert((UserContext)user); } else { insertOK = node.parent.safeInsert(node, username); } if (insertOK > 0) { nodestate[i] = COMMITED; } else { nodestate[i] = FAILED; String message = "When this failed, it is possible that the creation of an insrel went right, which leads to a database inconsistency.. stop now.. (transaction 2.0: [rollback?])"; log.error(message); throw new RuntimeException(message); } } } } log.debug("Commiting relations"); // Then commit all the RELATIONS i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); if (node.getBuilder() instanceof InsRel) { if (nodeexist[i] == I_EXISTS_YES ) { boolean commitOK; if (user instanceof UserContext) { commitOK = node.commit((UserContext)user); } else { commitOK = node.parent.safeCommit(node); } if (commitOK) { nodestate[i]=COMMITED; } else { nodestate[i]=FAILED; } } else if (nodeexist[i] == I_EXISTS_NO ) { int insertOK; if (user instanceof UserContext) { insertOK = node.insert((UserContext)user); } else { insertOK = node.parent.safeInsert(node, username); } if (insertOK > 0) { nodestate[i] = COMMITED; } else { nodestate[i] = FAILED; String message = "relation failed(transaction 2.0: [rollback?])"; log.error(message); } } } } log.debug("Deleting relations"); // Then commit all the RELATIONS that must be deleted i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); if (node.getBuilder() instanceof InsRel && nodeexist[i] == I_EXISTS_NOLONGER) { // no return information if (user instanceof UserContext) { node.remove((UserContext)user); } else { node.parent.removeNode(node); } nodestate[i]=COMMITED; } } log.debug("Deleting nodes"); // Then commit all the NODES that must be deleted i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); if (!(node.getBuilder() instanceof InsRel) && (nodeexist[i] == I_EXISTS_NOLONGER)) { // no return information if (user instanceof UserContext) { node.remove((UserContext)user); } else { node.parent.removeNode(node); } nodestate[i]=COMMITED; } } // check for failures okay=true; i = 0; for (Iterator nodeIterator = nodes.iterator(); nodeIterator.hasNext(); i++) { MMObjectNode node = (MMObjectNode)nodeIterator.next(); if (nodestate[i] == FAILED) { okay=false; log.error("Failed node "+node.toString()); } } return okay; } public String findUserName(Object user) { if (user instanceof UserContext) { return ((UserContext)user).getIdentifier(); } else { return ""; } } }
