OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 93 | Rev 156 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.view.list;
15
 
93 ilm 16
import org.openconcerto.sql.Log;
73 ilm 17
import org.openconcerto.sql.model.SQLRow;
18
import org.openconcerto.sql.model.SQLRowValues;
93 ilm 19
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
73 ilm 20
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
17 ilm 21
import org.openconcerto.sql.model.SQLTable;
22
import org.openconcerto.sql.model.SQLTableEvent;
83 ilm 23
import org.openconcerto.sql.model.SQLTableModifiedListener;
80 ilm 24
import org.openconcerto.sql.model.graph.Link.Direction;
93 ilm 25
import org.openconcerto.sql.model.graph.Path;
17 ilm 26
import org.openconcerto.sql.view.list.UpdateRunnable.RmAllRunnable;
93 ilm 27
import org.openconcerto.sql.view.list.search.SearchOne;
28
import org.openconcerto.sql.view.list.search.SearchOne.Mode;
29
import org.openconcerto.sql.view.list.search.SearchQueue;
30
import org.openconcerto.sql.view.list.search.SearchQueue.SetStateRunnable;
142 ilm 31
import org.openconcerto.utils.CollectionUtils;
93 ilm 32
import org.openconcerto.utils.ListMap;
73 ilm 33
import org.openconcerto.utils.RecursionType;
17 ilm 34
import org.openconcerto.utils.SleepingQueue;
93 ilm 35
import org.openconcerto.utils.Tuple2;
17 ilm 36
import org.openconcerto.utils.cc.IClosure;
93 ilm 37
import org.openconcerto.utils.cc.IPredicate;
73 ilm 38
import org.openconcerto.utils.cc.ITransformer;
17 ilm 39
 
40
import java.beans.PropertyChangeEvent;
41
import java.beans.PropertyChangeListener;
93 ilm 42
import java.util.ArrayList;
43
import java.util.Collections;
61 ilm 44
import java.util.Deque;
73 ilm 45
import java.util.HashSet;
93 ilm 46
import java.util.Iterator;
47
import java.util.List;
73 ilm 48
import java.util.Set;
17 ilm 49
import java.util.concurrent.FutureTask;
93 ilm 50
import java.util.logging.Level;
17 ilm 51
 
93 ilm 52
import net.jcip.annotations.GuardedBy;
53
 
54
public final class UpdateQueue extends SleepingQueue {
17 ilm 55
 
56
    /**
57
     * Whether the passed future performs an update.
58
     *
59
     * @param f a task in this queue, can be <code>null</code>.
60
     * @return <code>true</code> if <code>f</code> loads from the db.
61
     */
62
    static boolean isUpdate(FutureTask<?> f) {
93 ilm 63
        return isUpdate(SearchQueue.getRunnable(f));
17 ilm 64
    }
65
 
93 ilm 66
    static boolean isUpdate(Runnable r) {
67
        return r instanceof UpdateRunnable;
68
    }
69
 
70
    private static boolean isCancelableUpdate(Runnable r) {
17 ilm 71
        // don't cancel RmAll so we can put an UpdateAll right after it (the UpdateAll won't be
72
        // executed since RmAll put the queue to sleep)
93 ilm 73
        return isUpdate(r) && !(r instanceof RmAllRunnable);
17 ilm 74
    }
75
 
76
    private final class TableListener implements SQLTableModifiedListener, PropertyChangeListener {
93 ilm 77
        @Override
17 ilm 78
        public void tableModified(SQLTableEvent evt) {
79
            if (UpdateQueue.this.alwaysUpdateAll)
80
                putUpdateAll();
93 ilm 81
            else if (evt.getMode() == SQLTableEvent.Mode.ROW_UPDATED) {
17 ilm 82
                rowModified(evt);
73 ilm 83
            } else {
84
                rowAddedOrDeleted(evt);
17 ilm 85
            }
86
        }
87
 
88
        @Override
89
        public void propertyChange(PropertyChangeEvent evt) {
90
            // where changed
93 ilm 91
            stateChanged(null, getModel().getReq().createState());
17 ilm 92
        }
93
    }
94
 
95
    private final ITableModel tableModel;
93 ilm 96
    // thread-confined
97
    private SQLTableModelSourceState state;
98
    @GuardedBy("itself")
99
    private final List<ListSQLLine> fullList;
100
    @GuardedBy("fullList")
101
    private SQLTableModelColumns columns;
17 ilm 102
    private final TableListener tableListener;
73 ilm 103
    // TODO rm : needed for now since our optimizations are false if there's a where not on the
104
    // primary table, see http://192.168.1.10:3000/issues/show/22
17 ilm 105
    private boolean alwaysUpdateAll = false;
93 ilm 106
    private final IClosure<Deque<FutureTask<?>>> cancelClosure;
17 ilm 107
 
108
    public UpdateQueue(ITableModel model) {
109
        super(UpdateQueue.class.getSimpleName() + " on " + model);
110
        this.tableModel = model;
93 ilm 111
        this.fullList = new ArrayList<ListSQLLine>();
112
        this.cancelClosure = createCancelClosure(this, new ITransformer<FutureTask<?>, TaskType>() {
113
            @Override
114
            public TaskType transformChecked(FutureTask<?> input) {
115
                final Runnable r = SearchQueue.getRunnable(input);
116
                if (isCancelableUpdate(r))
117
                    return TaskType.COMPUTE;
118
                else if (r instanceof SetStateRunnable)
119
                    return TaskType.SET_STATE;
120
                else
121
                    return TaskType.USER;
122
            }
123
        });
17 ilm 124
        this.tableListener = new TableListener();
93 ilm 125
    }
126
 
127
    private final ITableModel getModel() {
128
        return this.tableModel;
129
    }
130
 
131
    @Override
132
    protected void started() {
17 ilm 133
        // savoir quand les tables qu'on affiche changent
142 ilm 134
        addSourceListener();
93 ilm 135
        stateChanged(null, this.getModel().getReq().createState());
136
        // Only starts once there's something to search, that way the runnable passed to
137
        // ITableModel.search() will be meaningful
138
        // SetStateRunnable since this must not be cancelled by an updateAll, but it shouldn't
139
        // prevent earlier updates to be cancelled
140
        this.put(new SetStateRunnable() {
141
            @Override
142
            public void run() {
143
                getModel().getSearchQueue().start();
144
            }
145
        });
17 ilm 146
    }
147
 
93 ilm 148
    @Override
149
    protected void dying() throws Exception {
150
        super.dying();
151
        assert currentlyInQueue();
152
 
153
        // only kill searchQ once updateQ is really dead, otherwise the currently executing
154
        // update might finish once the searchQ is already dead.
155
 
156
        final SearchQueue searchQueue = getModel().getSearchQueue();
157
        // state cannot change since we only change it in this thread (except for an Error being
158
        // thrown and killing the queue)
159
        final RunningState state = searchQueue.getRunningState();
160
        // not started or there was an Error
161
        if (state == RunningState.NEW || state == RunningState.DEAD)
162
            return;
163
        if (state == RunningState.WILL_DIE || state == RunningState.DYING)
164
            throw new IllegalStateException("Someone else already called die()");
165
        try {
166
            searchQueue.die().get();
167
        } catch (Exception e) {
168
            if (searchQueue.getRunningState() != RunningState.DEAD)
169
                throw e;
170
            // there was an Error in the last run task or while in die(), but it's OK we wanted the
171
            // queue dead
172
            Log.get().log(Level.CONFIG, "Exception while killing search queue", e);
173
        }
174
        assert searchQueue.getRunningState().compareTo(RunningState.DYING) >= 0;
175
        searchQueue.join();
176
        assert searchQueue.getRunningState() == RunningState.DEAD;
177
    }
178
 
179
    final List<ListSQLLine> getFullList() {
180
        return this.fullList;
181
    }
182
 
183
    public final Tuple2<List<ListSQLLine>, SQLTableModelColumns> copyFullList() {
184
        final Tuple2<List<ListSQLLine>, SQLTableModelColumns> res;
185
        final List<ListSQLLine> fullList = this.getFullList();
186
        synchronized (fullList) {
187
            res = Tuple2.<List<ListSQLLine>, SQLTableModelColumns> create(new ArrayList<ListSQLLine>(fullList), this.columns);
188
        }
189
        return res;
190
    }
191
 
192
    public final ListSQLLine getLine(final Number id) {
193
        final ListSQLLine res;
194
        final List<ListSQLLine> fullList = this.getFullList();
195
        synchronized (fullList) {
196
            res = ListSQLLine.fromID(fullList, id.intValue());
197
        }
198
        return res;
199
    }
200
 
201
    /**
202
     * The lines and their path affected by a change of the passed row.
203
     *
204
     * @param r the row that has changed.
205
     * @return the refreshed lines and their changed paths.
206
     */
207
    protected final ListMap<ListSQLLine, Path> getAffectedLines(final SQLRow r) {
208
        return this.getAffected(r, new ListMap<ListSQLLine, Path>(), true);
209
    }
210
 
211
    protected final ListMap<Path, ListSQLLine> getAffectedPaths(final SQLRow r) {
212
        return this.getAffected(r, new ListMap<Path, ListSQLLine>(), false);
213
    }
214
 
215
    // must be called from within this queue, as this method use fullList
216
    private <K, V> ListMap<K, V> getAffected(final SQLRow r, final ListMap<K, V> res, final boolean byLine) {
217
        final List<ListSQLLine> fullList = this.getFullList();
218
        synchronized (fullList) {
219
            final SQLTable t = r.getTable();
220
            final int id = r.getID();
221
            if (id < SQLRow.MIN_VALID_ID)
222
                throw new IllegalArgumentException("invalid ID: " + id);
223
            if (!fullList.isEmpty()) {
142 ilm 224
                final SQLRowValues proto = this.getState().getReq().getGraphToFetch();
93 ilm 225
                final List<Path> pathsToT = new ArrayList<Path>();
226
                proto.getGraph().walk(proto, pathsToT, new ITransformer<State<List<Path>>, List<Path>>() {
227
                    @Override
228
                    public List<Path> transformChecked(State<List<Path>> input) {
229
                        if (input.getCurrent().getTable() == t) {
230
                            input.getAcc().add(input.getPath());
231
                        }
232
                        return input.getAcc();
233
                    }
234
                }, RecursionType.BREADTH_FIRST, Direction.ANY);
235
                for (final Path p : pathsToT) {
236
                    final String lastReferentField = SearchQueue.getLastReferentField(p);
237
                    for (final ListSQLLine line : fullList) {
238
                        boolean put = false;
239
                        for (final SQLRowValues current : line.getRow().followPath(p, CreateMode.CREATE_NONE, false)) {
240
                            // works for rowValues w/o any ID
241
                            if (current != null && current.getID() == id) {
242
                                put = true;
243
                            }
244
                        }
245
                        // if the modified row isn't in the existing line, it might still affect it
246
                        // if it's a referent row insertion
247
                        if (!put && lastReferentField != null && r.exists()) {
248
                            final int foreignID = r.getInt(lastReferentField);
249
                            for (final SQLRowValues current : line.getRow().followPath(p.minusLast(), CreateMode.CREATE_NONE, false)) {
250
                                if (current.getID() == foreignID) {
251
                                    put = true;
252
                                }
253
                            }
254
                        }
255
                        if (put) {
256
                            // add to the list of paths that have been refreshed
257
                            add(byLine, res, p, line);
258
                        }
259
                    }
260
                }
261
            }
262
        }
263
        return res;
264
    }
265
 
266
    @SuppressWarnings("unchecked")
267
    <V, K> void add(boolean byLine, ListMap<K, V> res, final Path p, final ListSQLLine line) {
268
        if (byLine)
269
            res.add((K) line, (V) p);
270
        else
271
            res.add((K) p, (V) line);
272
    }
273
 
274
    final void setFullList(final List<ListSQLLine> tmp, final SQLTableModelColumns cols) {
275
        final List<ListSQLLine> fullList = this.getFullList();
276
        synchronized (fullList) {
277
            fullList.clear();
278
            fullList.addAll(tmp);
279
            // MAYBE only sort() if it can't be done by the SELECT
280
            // but comparing ints (field ORDRE) is quite fast : 170ms for 100,000 items
281
            Collections.sort(fullList);
282
            if (cols != null)
283
                this.columns = cols;
284
        }
285
        this.tableModel.getSearchQueue().fullListChanged();
286
    }
287
 
142 ilm 288
    final void reorder(final List<Integer> idsOrder) {
93 ilm 289
        final List<ListSQLLine> fullList = this.getFullList();
290
        synchronized (fullList) {
291
            for (final ListSQLLine l : fullList) {
292
                final Number newOrder;
293
                if (idsOrder == null) {
294
                    newOrder = null;
295
                } else {
296
                    final int index = idsOrder.indexOf(l.getID());
297
                    if (index < 0)
298
                        throw new IllegalArgumentException("Missing id " + l.getID() + " in " + idsOrder);
299
                    newOrder = index;
300
                }
301
                l.setOrder(newOrder);
302
            }
303
            Collections.sort(fullList);
304
        }
305
        this.tableModel.getSearchQueue().orderChanged();
306
    }
307
 
308
    // vals can be null if we're removing a referent row
309
    final void updateLine(ListSQLLine line, Path p, int valsID, SQLRowValues vals) {
310
        final Set<Integer> modifiedCols = line.loadAt(valsID, vals, p);
311
        this.tableModel.getSearchQueue().changeFullList(line.getID(), line, modifiedCols, SearchOne.Mode.CHANGE);
312
    }
313
 
314
    final ListSQLLine replaceLine(final int id, final ListSQLLine newLine) {
315
        final Mode mode;
316
        final List<ListSQLLine> fullList = this.getFullList();
317
        final ListSQLLine oldLine;
318
        synchronized (fullList) {
319
            final int modifiedIndex = ListSQLLine.indexFromID(fullList, id);
320
            oldLine = modifiedIndex < 0 ? null : fullList.get(modifiedIndex);
321
 
322
            if (modifiedIndex < 0) {
323
                // la ligne n'était dans notre liste
324
                if (newLine != null) {
325
                    // mais elle existe : ajout
326
                    // ATTN on ajoute à la fin, sans se soucier de l'ordre
327
                    fullList.add(newLine);
328
                    Collections.sort(fullList);
329
                    mode = Mode.ADD;
330
                } else {
331
                    // et elle n'y est toujours pas
332
                    mode = Mode.NO_CHANGE;
333
                }
334
            } else {
335
                // la ligne était dans notre liste
336
                if (newLine != null) {
337
                    // mettre à jour
338
                    fullList.set(modifiedIndex, newLine);
339
                    Collections.sort(fullList);
340
                    mode = Mode.CHANGE;
341
                } else {
342
                    // elle est effacée ou filtrée
343
                    fullList.remove(modifiedIndex);
344
                    mode = Mode.REMOVE;
345
                }
346
            }
347
        }
348
 
349
        // notify search queue
350
        this.tableModel.getSearchQueue().changeFullList(id, newLine, null, mode);
351
        return oldLine;
352
    }
353
 
354
    public final int getFullListSize() {
355
        final List<ListSQLLine> fullList = this.getFullList();
356
        synchronized (fullList) {
357
            return fullList.size();
358
        }
359
    }
360
 
17 ilm 361
    void setAlwaysUpdateAll(boolean b) {
362
        this.alwaysUpdateAll = b;
363
    }
364
 
365
    // *** listeners
366
 
93 ilm 367
    void stateChanged(final SQLTableModelSourceState beforeState, final SQLTableModelSourceState afterState) {
368
        if (afterState == null)
369
            throw new NullPointerException("Null state");
370
 
371
        // As in SearchQueue :
372
        // needs to be 2 different runnables, that way if the source is changed and then the table
373
        // is updated : the queue would naively contain setState, updateAll, updateAll and thus we
374
        // can cancel one updateAll. Whereas if the setState was contained in updateAll, we
375
        // couldn't cancel it.
376
        // use tasksDo() so that no other runnable can come between setState and updateAll.
377
        // Otherwise an updateOne might use new columns and add a line with different columns than
378
        // the full list.
379
        this.tasksDo(new IClosure<Deque<FutureTask<?>>>() {
380
            @Override
381
            public void executeChecked(Deque<FutureTask<?>> input) {
382
                put(new SetStateRunnable() {
383
                    @Override
384
                    public void run() {
142 ilm 385
                        setState(afterState);
93 ilm 386
                    }
387
                });
388
                // TODO if request didn't change and the new graph is smaller, copy and prune the
389
                // rows
390
                putUpdateAll();
391
            }
392
        });
393
    }
394
 
142 ilm 395
    protected final void setState(final SQLTableModelSourceState newState) {
396
        if (this.state != null)
397
            this.rmTableListener();
398
        this.state = newState;
399
        if (this.state != null)
400
            this.addTableListener();
401
    }
402
 
93 ilm 403
    protected final SQLTableModelSourceState getState() {
404
        assert this.currentlyInQueue();
405
        if (this.state == null)
406
            throw new IllegalStateException("Not yet started");
407
        return this.state;
408
    }
409
 
17 ilm 410
    @Override
83 ilm 411
    protected void willDie() {
142 ilm 412
        this.rmTableListener();
413
        this.removeSourceListener();
83 ilm 414
        super.willDie();
17 ilm 415
    }
416
 
93 ilm 417
    protected final void addTableListener() {
142 ilm 418
        this.getState().getReq().addTableListener(this.tableListener);
93 ilm 419
    }
420
 
142 ilm 421
    private void addSourceListener() {
17 ilm 422
        this.tableModel.getLinesSource().addListener(this.tableListener);
423
    }
424
 
93 ilm 425
    protected final void rmTableListener() {
142 ilm 426
        this.getState().getReq().removeTableListener(this.tableListener);
93 ilm 427
    }
428
 
142 ilm 429
    private void removeSourceListener() {
17 ilm 430
        this.tableModel.getLinesSource().rmListener(this.tableListener);
431
    }
432
 
433
    // *** une des tables que l'on affiche a changé
434
 
435
    void rowModified(final SQLTableEvent evt) {
436
        final int id = evt.getId();
73 ilm 437
        if (id < SQLRow.MIN_VALID_ID) {
17 ilm 438
            this.putUpdateAll();
439
        } else if (CollectionUtils.containsAny(this.tableModel.getReq().getLineFields(), evt.getFields())) {
440
            this.put(evt);
441
        }
442
        // si on n'affiche pas le champ ignorer
443
    }
444
 
73 ilm 445
    // takes 1-2ms, perhaps cache
446
    final Set<SQLTable> getNotForeignTables() {
447
        final Set<SQLTable> res = new HashSet<SQLTable>();
448
        final SQLRowValues maxGraph = this.tableModel.getReq().getMaxGraph();
449
        maxGraph.getGraph().walk(maxGraph, res, new ITransformer<State<Set<SQLTable>>, Set<SQLTable>>() {
450
            @Override
451
            public Set<SQLTable> transformChecked(State<Set<SQLTable>> input) {
452
                if (input.getPath().length() == 0 || input.isBackwards())
453
                    input.getAcc().add(input.getCurrent().getTable());
454
                return input.getAcc();
455
            }
80 ilm 456
        }, RecursionType.BREADTH_FIRST, Direction.ANY);
73 ilm 457
        return res;
17 ilm 458
    }
459
 
73 ilm 460
    void rowAddedOrDeleted(final SQLTableEvent evt) {
461
        if (evt.getId() < SQLRow.MIN_VALID_ID)
462
            this.putUpdateAll();
463
        // if a row of a table that we point to is added, we will care when the referent table will
464
        // point to it
465
        else if (this.getNotForeignTables().contains(evt.getTable()))
466
            this.put(evt);
17 ilm 467
    }
468
 
469
    // *** puts
470
 
93 ilm 471
    public final void putExternalUpdated(final String externalID, final IPredicate<ListSQLLine> affectedPredicate) {
472
        this.put(new Runnable() {
473
            @Override
474
            public void run() {
475
                externalUpdated(externalID, affectedPredicate);
476
            }
477
        });
478
    }
479
 
480
    protected final void externalUpdated(final String externalID, final IPredicate<ListSQLLine> affectedPredicate) {
481
        final List<ListSQLLine> fullList = this.getFullList();
482
        synchronized (fullList) {
483
            final Set<Integer> indexes = new HashSet<Integer>();
484
            int i = 0;
485
            for (final SQLTableModelColumn col : this.columns.getAllColumns()) {
486
                if (col.getUsedExternals().contains(externalID)) {
487
                    indexes.add(i);
488
                }
489
                i++;
490
            }
491
            if (indexes.isEmpty()) {
492
                Log.get().log(Level.INFO, "No columns use " + externalID + " in " + this);
493
                return;
494
            }
495
 
496
            for (final ListSQLLine line : fullList) {
497
                if (affectedPredicate.evaluateChecked(line)) {
498
                    this.tableModel.getSearchQueue().changeFullList(line.getID(), line, indexes, SearchOne.Mode.CHANGE);
499
                }
500
            }
501
        }
502
    }
503
 
17 ilm 504
    private void put(SQLTableEvent evt) {
505
        this.put(UpdateRunnable.create(this.tableModel, evt));
506
    }
507
 
508
    public void putUpdateAll() {
509
        this.put(UpdateRunnable.create(this.tableModel));
510
    }
511
 
512
    /**
513
     * If this is sleeping, empty the list and call {@link #putUpdateAll()} so that the list reload
514
     * itself when this wakes up.
515
     *
516
     * @throws IllegalStateException if not sleeping.
517
     */
518
    void putRemoveAll() {
519
        if (!this.isSleeping())
520
            throw new IllegalStateException("not sleeping");
521
        // no user runnables can come between the RmAll and the UpdateAll since runnableAdded()
522
        // is blocked by our lock, so there won't be any incoherence for them
523
        this.put(UpdateRunnable.createRmAll(this, this.tableModel));
524
        this.setSleeping(false);
525
        // reload the empty list when waking up
526
        this.putUpdateAll();
527
    }
528
 
93 ilm 529
    @Override
530
    protected void willPut(final FutureTask<?> qr) throws InterruptedException {
531
        if (SearchQueue.getRunnable(qr) instanceof ChangeAllRunnable) {
17 ilm 532
            // si on met tout à jour, ne sert à rien de garder les maj précédentes.
93 ilm 533
            this.tasksDo(this.cancelClosure);
534
        }
535
    }
536
 
537
    static public enum TaskType {
538
        USER(false, true), COMPUTE(true, false), SET_STATE(false, false);
539
 
540
        private final boolean cancelable, dependsOnPrevious;
541
 
542
        private TaskType(boolean cancelable, boolean dependsOnPrevious) {
543
            this.cancelable = cancelable;
544
            this.dependsOnPrevious = dependsOnPrevious;
545
        }
546
    }
547
 
548
    static public final IClosure<Deque<FutureTask<?>>> createCancelClosure(final SleepingQueue q, final ITransformer<? super FutureTask<?>, TaskType> cancelablePred) {
549
        return new IClosure<Deque<FutureTask<?>>>() {
550
            @Override
551
            public void executeChecked(final Deque<FutureTask<?>> tasks) {
552
                // on part de la fin et on supprime toutes les maj jusqu'a ce qu'on trouve
553
                // un runnable qui n'est pas annulable
554
                final Iterator<FutureTask<?>> iter = tasks.descendingIterator();
555
                boolean needsPrevious = false;
556
                while (iter.hasNext() && !needsPrevious) {
557
                    final FutureTask<?> current = iter.next();
558
                    final TaskType type = cancelablePred.transformChecked(current);
559
                    needsPrevious = type.dependsOnPrevious;
560
                    if (type.cancelable)
561
                        iter.remove();
562
                }
563
                // if we stop only because we ran out of items, continue with beingRun
564
                if (!needsPrevious) {
565
                    // before trying to cancel being run we should have been through all the backlog
566
                    assert !iter.hasNext();
567
                    final FutureTask<?> br = q.getBeingRun();
568
                    if (br != null && cancelablePred.transformChecked(br).cancelable) {
569
                        // might already be done by now, but it's OK cancel() will just return false
570
                        br.cancel(true);
17 ilm 571
                    }
572
                }
93 ilm 573
            }
574
        };
17 ilm 575
    }
576
 
577
}