File: TransactionList.java

package info (click to toggle)
libglazedlists-java 1.9.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,024 kB
  • ctags: 4,252
  • sloc: java: 22,561; xml: 818; sh: 51; makefile: 5
file content (307 lines) | stat: -rw-r--r-- 11,730 bytes parent folder | download | duplicates (3)
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();
        }
    }
}