1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
|
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;
import ca.odell.glazedlists.event.ListEvent;
import java.util.List;
import java.util.ArrayList;
/**
* A list transformation that presents traditional transaction semantics.
* Typical usage resembles one of two methods:
*
* <pre>
* EventList source = ...
* TransactionList txList = new TransactionList(source);
*
* // begin a transaction in which all ListEvents are collected by txList
* // into a single "super ListEvent", which is fired on commit
* txList.beginEvent(true);
*
* // fill in the details of the transaction
* // (these operations actually physically change ONLY txList and its source)
* txList.add("A new element");
* txList.set(0, "A changed element");
* txList.remove(6);
*
* // commit the transaction, which will broadcast a single ListEvent from
* // txList describing the aggregate of all changes made during the transaction
* // (this returns the entire list pipeline to a consistent state)
* txList.commitEvent();
* </pre>
*
* In this usage, all ListEventListeners "downstream" of TransactionList remain
* clueless about changes made during a transaction. As a result, the
* "list pipeline" is allowed to temporarily fall into an inconsistent state
* because only a portion of the pipeline (TransactionList and lower) has seen
* changes made during the transaction. Users must ensure that they do not
* read or write through any "downstream" EventList that depends on the
* TransactionList during a transaction. Typically this is done using the
* built-in {@link #getReadWriteLock() locks}.
*
* <p>If the transaction was rolled back instead of committed, the txList would
* not produce a ListEvent, since none of its listeners would be aware of any
* changes made during the transaction.
*
* The second popular usage resembles this:
*
* <pre>
* EventList source = ...
* TransactionList txList = new TransactionList(source);
*
* // begin a transaction in which we change the ListEvent
* txList.beginEvent(); // this is the same as txList.beginEvent(false);
*
* // fill in the details of the transaction
* // (these operations actually physically change the ENTIRE PIPELINE)
* txList.add("A new element");
* txList.set(0, "A changed element");
* txList.remove(6);
*
* // commit the transaction, which will NOT broadcast a ListEvent from
* // txList because all of its listeners are already aware of the changes
* // made during the transaction
* txList.commitEvent();
* </pre>
*
* In this case, the "list pipeline" always remains consistent and reads/writes
* may occur through any part EventList in the pipeline without error.
*
* <p>If the transaction is rolled back instead of committed, the txList
* produces a ListEvent describing the rollback, since its listeners are fully
* aware of the changes made during the transaction and must also be given a
* chance to undo their changes.
*
* <p>Transactions may be nested arbitrarily deep using code that resembles:
* <pre>
* txList.beginEvent();
* txList.add("A");
*
* txList.beginEvent();
* txList.set(0, "B");
* txList.commitEvent();
*
* txList.beginEvent();
* txList.add("C");
* txList.commitEvent();
* txList.commitEvent();
* </pre>
*
* @author James Lemieux
*/
public class TransactionList<E> extends TransformedList<E, E> {
/** produces {@link UndoRedoSupport.Edit}s which are collected during a transaction to support rollback */
private UndoRedoSupport rollbackSupport;
/** A stack of transactions contexts; one for each layer of nested transaction */
private final List<Context> txContextStack = new ArrayList<Context>();
/**
* Constructs a <code>TransactionList</code> that provides traditional
* transaction semantics over the given <code>source</code>.
*
* @param source the EventList over which to provide a transactional view
*/
public TransactionList(EventList<E> source) {
this(source, true);
}
/**
* Constructs a <code>TransactionList</code> that provides traditional
* transaction semantics over the given <code>source</code>.
*
* <p>If <code>rollbackSupport</code> is <tt>true</tt> then this
* TransactionList supports calling {@link #rollbackEvent()} during a
* transaction. This constructor exists solely to break the constructor
* cycle between UndoRedoSupport and TransactionList and should only be
* used internally by Glazed Lists.
*
* @param source the EventList over which to provide a transactional view
* @param rollbackSupport <tt>true</tt> indicates this TransactionList must
* support the rollback ability; <tt>false</tt> indicates it is not
* necessary
*/
TransactionList(EventList<E> source, boolean rollbackSupport) {
super(source);
// if rollback support is requested, build the necessary infrastructure
if (rollbackSupport) {
this.rollbackSupport = UndoRedoSupport.install(source);
this.rollbackSupport.addUndoSupportListener(new RollbackSupportListener());
}
source.addListEventListener(this);
}
/**
* Demarks the beginning of a transaction which accumulates all ListEvents
* received during the transaction and fires a single aggregate ListEvent
* on {@link #commitEvent()}.
*/
public void beginEvent() {
beginEvent(true);
}
/**
* Demarks the beginning of a transaction. If <code>buffered</code> is
* <tt>true</tt> then all ListEvents received during the transaction are
* accumulated and fired as a single aggregate ListEvent on
* {@link #commitEvent()}. If <code>buffered</code> is <tt>false</tt> then
* all ListEvents received during the transaction are forwarded immediately
* and {@link #commitEvent()} produces no ListEvent of its own.
*
* @param buffered <tt>true</tt> indicates ListEvents should be buffered and
* sent on {@link #commitEvent()}; <tt>false</tt> indicates they should
* be sent on immediately
*/
public void beginEvent(boolean buffered) {
// start a nestable ListEvent if we're supposed to buffer them
if (buffered)
updates.beginEvent(true);
// push a new context onto the stack describing this new transaction
txContextStack.add(new Context(buffered));
}
/**
* Demarks the successful completion of a transaction. If changes were
* buffered during the transaction by calling {@link #beginEvent(boolean) beginEvent(true)}
* then a single ListEvent will be fired from this TransactionList
* describing the changes accumulated during the transaction.
*/
public void commitEvent() {
// verify that there is a transaction to roll back
if (rollbackSupport != null && txContextStack.isEmpty())
throw new IllegalStateException("No ListEvent exists to commit");
// pop the last context off the stack and ask it to commit
txContextStack.remove(txContextStack.size()-1).commit();
}
/**
* Demarks the unsuccessful completion of a transaction. If changes were
* NOT buffered during the transaction by calling {@link #beginEvent(boolean) beginEvent(false)}
* then a single ListEvent will be fired from this TransactionList
* describing the rollback of the changes accumulated during the transaction.
*/
public void rollbackEvent() {
// check if this TransactionList was created with rollback abilities
if (rollbackSupport == null)
throw new IllegalStateException("This TransactionList does not support rollback");
// check if a transaction exists to rollback
if (txContextStack.isEmpty())
throw new IllegalStateException("No ListEvent exists to roll back");
// pop the last context off the stack and ask it to rollback
txContextStack.remove(txContextStack.size()-1).rollback();
}
/** {@inheritDoc} */
@Override
protected boolean isWritable() {
return true;
}
/** @inheritDoc */
@Override
public void dispose() {
if (rollbackSupport != null)
rollbackSupport.uninstall();
rollbackSupport = null;
txContextStack.clear();
super.dispose();
}
/**
* Simply forwards all of the <code>listChanges</code> since TransactionList
* doesn't transform the source data in any way.
*/
@Override
public void listChanged(ListEvent<E> listChanges) {
updates.forwardEvent(listChanges);
}
/**
* Accumulates all of the small Edits that occur during a transaction
* within a CompositeEdit that can be undone to support rollback, if
* necessary.
*/
private class RollbackSupportListener implements UndoRedoSupport.Listener {
public void undoableEditHappened(UndoRedoSupport.Edit edit) {
// if a tx context exists we are in the middle of a transaction
if (!txContextStack.isEmpty())
txContextStack.get(txContextStack.size()-1).add(edit);
}
}
/**
* A small object describing the details about the transaction that was
* started so that it can be properly committed or rolled back at a later
* time. Specifically it tracks:
*
* <ul>
* <li>a CompositeEdit which can be used to undo the transaction's changes
* in the case of a rollback</li>
* <li>a flag indicating wether a ListEvent was started when the
* transaction began (and thus must be committed or discarded later)
* </ul>
*/
private final class Context {
/** collects the smaller intermediate Edits that occur during a transaction; <code>null</code> if no transaction exists */
private UndoRedoSupport.CompositeEdit rollbackEdit = rollbackSupport == null ? null : rollbackSupport.new CompositeEdit();
/**
* <tt>true</tt> indicates a ListEvent was started when this Context
* was created and must be committed or rolled back later.
*/
private boolean eventStarted = false;
public Context(boolean eventStarted) {
this.eventStarted = eventStarted;
}
/**
* Add the given edit into this Context to support its possible rollback.
*/
public void add(UndoRedoSupport.Edit edit) {
if (rollbackEdit != null)
rollbackEdit.add(edit);
}
/**
* Commit the changes associated with this transaction.
*/
public void commit() {
rollbackEdit = null;
if (eventStarted)
updates.commitEvent();
}
/**
* Rollback the changes associated with this transaction.
*/
public void rollback() {
if (rollbackEdit != null) {
// rollback all changes from the transaction as a single ListEvent
updates.beginEvent(true);
try {
rollbackEdit.undo();
} finally {
updates.commitEvent();
}
rollbackEdit = null;
}
// throw away the ListEvent if we started one
if (eventStarted)
updates.discardEvent();
}
}
}
|