OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 177 | 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
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 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.model.ConnectionHandlerNoSetup;
17
import org.openconcerto.sql.model.OrderComparator;
18
import org.openconcerto.sql.model.SQLDataSource;
142 ilm 19
import org.openconcerto.sql.model.SQLField;
17 ilm 20
import org.openconcerto.sql.model.SQLRow;
93 ilm 21
import org.openconcerto.sql.model.SQLRowAccessor;
17 ilm 22
import org.openconcerto.sql.model.SQLRowValues;
142 ilm 23
import org.openconcerto.sql.model.SQLTable.VirtualFields;
17 ilm 24
import org.openconcerto.sql.model.graph.Path;
93 ilm 25
import org.openconcerto.sql.utils.SQLUtils;
142 ilm 26
import org.openconcerto.utils.CompareUtils;
27
import org.openconcerto.utils.SetMap;
93 ilm 28
import org.openconcerto.utils.Value;
142 ilm 29
import org.openconcerto.utils.cc.IClosure;
151 ilm 30
import org.openconcerto.utils.cc.ITransformerExn;
17 ilm 31
 
32
import java.sql.SQLException;
93 ilm 33
import java.util.ArrayList;
182 ilm 34
import java.util.Collection;
17 ilm 35
import java.util.Collections;
36
import java.util.HashMap;
37
import java.util.HashSet;
93 ilm 38
import java.util.LinkedHashMap;
17 ilm 39
import java.util.LinkedList;
40
import java.util.List;
41
import java.util.Map;
142 ilm 42
import java.util.Map.Entry;
17 ilm 43
import java.util.Set;
93 ilm 44
import java.util.concurrent.Callable;
45
import java.util.concurrent.CopyOnWriteArrayList;
46
import java.util.concurrent.Future;
47
import java.util.concurrent.FutureTask;
17 ilm 48
 
93 ilm 49
import net.jcip.annotations.NotThreadSafe;
50
 
17 ilm 51
/**
93 ilm 52
 * Lines are stored in SQLRowValues and committed to the database on demand. Unless otherwise noted,
53
 * public methods are thread-safe, others are required to be called from the {@link UpdateQueue}.
17 ilm 54
 *
55
 * @author Sylvain
56
 */
57
public class SQLTableModelLinesSourceOffline extends SQLTableModelLinesSource {
58
 
93 ilm 59
    // row container with equals() using reference equality
60
    @NotThreadSafe
151 ilm 61
    static public final class Row {
142 ilm 62
        private final Integer id;
93 ilm 63
        private SQLRowValues vals;
64
 
142 ilm 65
        private Row(Integer id, SQLRowValues vals) {
93 ilm 66
            super();
67
            this.id = id;
68
            this.setRow(vals);
69
        }
70
 
142 ilm 71
        public final Integer getID() {
93 ilm 72
            return this.id;
73
        }
74
 
75
        public final SQLRowValues getRow() {
76
            return this.vals;
77
        }
78
 
151 ilm 79
        final void setRow(SQLRowValues newVals) {
93 ilm 80
            if (!newVals.isFrozen())
81
                throw new IllegalArgumentException("Not frozen : " + newVals);
82
            this.vals = newVals;
83
        }
84
 
85
        @Override
86
        public String toString() {
87
            return this.getClass().getSimpleName() + " of " + this.getID();
88
        }
89
    }
90
 
91
    static private abstract class OfflineCallable<V> implements Callable<V> {
92
 
93
    }
94
 
95
    static private abstract class OfflineRunnable extends OfflineCallable<Object> implements Runnable {
96
        @Override
97
        public final Object call() throws Exception {
98
            this.run();
99
            return null;
100
        }
101
    }
102
 
103
    // all mutable attributes are accessed from the UpdateQueue thread
104
 
17 ilm 105
    private final SQLTableModelSourceOffline parent;
93 ilm 106
    private final List<Row> lines;
17 ilm 107
    // since new lines have no database ID, give them a virtual one
108
    private int freeID;
142 ilm 109
    private final Map<Integer, Row> id2line;
93 ilm 110
    // values that can be modified, read-only
17 ilm 111
    private final SQLRowValues modifiableVals;
112
    // original value for modified lines
93 ilm 113
    private final Map<Row, SQLRowValues> dbVals;
17 ilm 114
    // removed lines
93 ilm 115
    private final Set<Number> deleted;
17 ilm 116
 
93 ilm 117
    private boolean dbOrder;
118
 
17 ilm 119
    {
93 ilm 120
        this.lines = new LinkedList<Row>();
17 ilm 121
        // the firsts are used in other part of the fwk
122
        this.freeID = SQLRow.MIN_VALID_ID - 10;
142 ilm 123
        this.id2line = new HashMap<Integer, Row>();
93 ilm 124
        this.dbOrder = true;
17 ilm 125
    }
126
 
127
    public SQLTableModelLinesSourceOffline(SQLTableModelSourceOffline parent, ITableModel model) {
128
        super(model);
129
        this.parent = parent;
93 ilm 130
        this.modifiableVals = this.getParent().getElem().getPrivateGraph().toImmutable();
131
        this.dbVals = new HashMap<Row, SQLRowValues>();
132
        this.deleted = new HashSet<Number>();
17 ilm 133
    }
134
 
135
    @Override
136
    public final SQLTableModelSourceOffline getParent() {
137
        return this.parent;
138
    }
139
 
151 ilm 140
    private boolean isUpdateThread() {
93 ilm 141
        return this.getModel().getUpdateQ().currentlyInQueue();
17 ilm 142
    }
143
 
151 ilm 144
    private void checkUpdateThread() {
145
        if (!this.isUpdateThread())
146
            throw new IllegalStateException("Not in the update thread");
147
    }
148
 
142 ilm 149
    private final Row getRow(Integer id) {
132 ilm 150
        return this.getRow(id, false);
93 ilm 151
    }
152
 
142 ilm 153
    private final Row getRow(Integer id, final boolean required) {
132 ilm 154
        return this.getRow(id, required, null);
155
    }
156
 
142 ilm 157
    private final Row getRow(Integer id, final boolean required, final Object idSource) {
132 ilm 158
        final Row res = this.id2line.get(id);
159
        if (required && res == null)
160
            throw new IllegalArgumentException("Not in the list : " + (idSource == null ? id : idSource));
161
        return res;
162
    }
163
 
151 ilm 164
    public final Row getRowNow(Integer id, final boolean required) {
165
        checkUpdateThread();
166
        return this.getRow(id, required);
167
    }
168
 
169
    public final List<Row> getRowsNow() {
170
        checkUpdateThread();
171
        return Collections.unmodifiableList(this.lines);
172
    }
173
 
93 ilm 174
    protected final int getSize() {
175
        return this.lines.size();
176
    }
177
 
178
    private final int indexOf(Row r) {
179
        return this.lines.indexOf(r);
180
    }
181
 
182
    protected final List<ListSQLLine> getLines() {
183
        final List<ListSQLLine> res = new ArrayList<ListSQLLine>();
184
        for (final Row r : this.lines) {
185
            final ListSQLLine l = createLine(r);
186
            if (l != null)
187
                res.add(l);
188
        }
189
        return res;
190
    }
191
 
192
    // if the user has moved rows, DB order can no longer be used
193
    private final boolean isDBOrder() {
194
        return this.dbOrder;
195
    }
196
 
197
    private final boolean setDBOrder(final boolean dbOrder) {
151 ilm 198
        assert isUpdateThread();
93 ilm 199
        if (this.dbOrder != dbOrder) {
200
            this.dbOrder = dbOrder;
201
            return true;
202
        } else {
203
            return false;
204
        }
205
    }
206
 
207
    private final Number getOrder(final Row r) {
208
        if (this.isDBOrder())
209
            return null;
210
        else
211
            return this.indexOf(r);
212
    }
213
 
214
    private final ListSQLLine createLine(final Row r) {
215
        if (r == null)
216
            return null;
217
        final ListSQLLine res = this.createLine(r.vals, r.id);
218
        if (res != null)
219
            res.setOrder(getOrder(r));
220
        return res;
221
    }
222
 
142 ilm 223
    protected final List<SQLRowValues> fetch() {
224
        return this.getUpdateQueueReq().getValues();
17 ilm 225
    }
226
 
93 ilm 227
    /**
228
     * Fetch all rows and update our lines. Must be called by the {@link UpdateQueue}. Deleted rows
229
     * will be removed, inserted rows added, virtual rows unchanged, and updated rows will only be
230
     * updated if unchanged.
231
     *
232
     * @return the new lines.
233
     */
234
    @Override
182 ilm 235
    public List<ListSQLLine> get(Collection<? extends Number> ids) {
151 ilm 236
        assert isUpdateThread();
182 ilm 237
        if (ids != null)
238
            throw new UnsupportedOperationException("Refreshing a subset of rows is not yet supported");
142 ilm 239
        final List<SQLRowValues> dbRows = this.fetch();
93 ilm 240
 
241
        if (this.lines.isEmpty()) {
242
            // optimization of the else block
243
            for (final SQLRowValues dbRow : dbRows) {
244
                this._add(dbRow, false, false);
245
            }
246
        } else {
247
            // delete
142 ilm 248
            final Set<Integer> dbIDs = new HashSet<Integer>();
93 ilm 249
            for (final SQLRowValues dbRow : dbRows) {
142 ilm 250
                dbIDs.add(dbRow.getIDNumber(true).intValue());
93 ilm 251
            }
142 ilm 252
            final Set<Integer> deletedIDs = new HashSet<Integer>(this.id2line.keySet());
93 ilm 253
            deletedIDs.removeAll(dbIDs);
142 ilm 254
            for (final Integer id : deletedIDs) {
93 ilm 255
                // don't delete virtual rows
256
                if (id.intValue() >= SQLRow.MIN_VALID_ID) {
257
                    final Value<ListSQLLine> val = this.updateRow(id.intValue(), null);
258
                    assert val.getValue() == null;
259
                }
260
            }
261
            // update/insert
262
            for (final SQLRowValues dbRow : dbRows) {
263
                this.updateRow(dbRow.getID(), dbRow);
264
            }
17 ilm 265
        }
93 ilm 266
        return this.getLines();
17 ilm 267
    }
268
 
93 ilm 269
    // row is null, if it was deleted
270
    private final Value<ListSQLLine> updateRow(final int id, final SQLRowValues row) {
271
        final Row existingLine = this.getRow(id);
272
 
273
        // if the row wasn't removed and we updated it, ignore new values
274
        if (row != null && existingLine != null && this.dbVals.containsKey(existingLine)) {
275
            // MAYBE warn if ignoring changes
276
            return Value.getNone();
277
        } else {
278
            final Row newRow;
279
            if (row == null) {
280
                // if this id is not part of us, rm from our list
281
                this._rm(existingLine);
282
                newRow = null;
283
            } else if (existingLine != null) {
284
                existingLine.setRow(row);
285
                newRow = existingLine;
286
            } else {
287
                // don't fire as the new line is returned from this method
288
                newRow = this._add(row, false, false);
289
            }
290
            return Value.getSome(this.createLine(newRow));
17 ilm 291
        }
292
    }
293
 
93 ilm 294
    /**
295
     * {@inheritDoc} Must be called by the {@link UpdateQueue}.
296
     */
297
    @Override
298
    public Value<ListSQLLine> get(final int id) {
151 ilm 299
        assert isUpdateThread();
142 ilm 300
        return updateRow(id, this.getUpdateQueueReq().getValues(id));
17 ilm 301
    }
302
 
93 ilm 303
    // *** Modify virtual rows ***
304
 
151 ilm 305
    private final <T> Future<T> execInUpdateQ(final OfflineCallable<T> call) {
177 ilm 306
        return this.getModel().getUpdateQ().add(new FutureTask<T>(call));
151 ilm 307
    }
308
 
93 ilm 309
    public final Future<Number> add(final SQLRowValues vals) {
151 ilm 310
        // copy to avoid back-door and allow grow()
311
        final SQLRowValues copy = vals.deepCopy();
312
        return this.execInUpdateQ(new OfflineCallable<Number>() {
17 ilm 313
            @Override
93 ilm 314
            public Number call() throws Exception {
151 ilm 315
                return addNow(copy, true);
17 ilm 316
            }
151 ilm 317
        });
17 ilm 318
    }
319
 
151 ilm 320
    /**
321
     * Add a new row. This method can only be called from the update thread, e.g. from
322
     * {@link #useRow(Number, ITransformerExn)}.
323
     *
324
     * @param vals the values, its {@link SQLRowValues#getID() ID} will be ignored.
325
     * @return the ID for the new row.
326
     * @see #add(SQLRowValues)
327
     */
328
    public final Integer addNow(final SQLRowValues vals) {
329
        return addNow(vals, false);
330
    }
331
 
332
    private final Integer addNow(final SQLRowValues vals, final boolean safe) {
333
        if (!safe)
334
            checkUpdateThread();
335
        final SQLRowValues copy = safe ? vals : vals.deepCopy();
336
        // otherwise could replace an existing row (use replaceRow() for that)
337
        // existing DB rows can only be added by fetch()
338
        copy.clearPrimaryKeys();
339
        return this._add(copy, true, true).getID();
340
    }
341
 
93 ilm 342
    protected Row _add(final SQLRowValues vals, final boolean grow, final boolean fireAdd) {
151 ilm 343
        assert isUpdateThread();
93 ilm 344
        // make sure every needed path is there
345
        if (grow)
142 ilm 346
            vals.grow(getUpdateQueueReq().getGraphToFetch(), false);
93 ilm 347
        // ATTN only works because vals was just fetched or just copied
348
        vals.getGraph().freeze();
151 ilm 349
        final boolean fromDB = vals.hasID() && vals.getID() >= SQLRow.MIN_VALID_ID;
142 ilm 350
        final List<Integer> order;
93 ilm 351
        final Row r;
142 ilm 352
        r = new Row(fromDB ? vals.getID() : this.freeID--, vals);
93 ilm 353
        this.id2line.put(r.getID(), r);
354
        this.lines.add(r);
355
 
356
        if (!fromDB && this.setDBOrder(false)) {
357
            order = this.getIDsOrder();
358
            assert order != null;
359
        } else {
360
            order = null;
361
        }
362
        if (order != null) {
363
            this.getModel().getUpdateQ().reorder(order);
364
            // put a setList() in searchQ
365
        }
366
        if (fireAdd) {
367
            // even if the row is filtered (i.e. line is null), don't remove as the filter might
368
            // change afterwards
369
            final ListSQLLine line = createLine(r);
370
            this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), line);
371
            // add the line in fullList
372
            // put a addList() in searchQ
373
        }
374
        return r;
17 ilm 375
    }
376
 
93 ilm 377
    public final Future<SQLRowValues> remove(final Number id) {
151 ilm 378
        return this.execInUpdateQ(new OfflineCallable<SQLRowValues>() {
93 ilm 379
            @Override
380
            public SQLRowValues call() throws Exception {
142 ilm 381
                final Row r = getRow(id.intValue());
151 ilm 382
                return rm(r);
93 ilm 383
            }
151 ilm 384
        });
17 ilm 385
    }
386
 
151 ilm 387
    public final SQLRowValues removeNow(final Row r) {
388
        this.checkUpdateThread();
389
        return rm(r);
390
    }
391
 
392
    private SQLRowValues rm(final Row r) {
93 ilm 393
        if (r != null) {
394
            this._rm(r);
395
            // add to a list of id to archive if it's in the DB
396
            if (r.vals.hasID())
397
                this.deleted.add(r.vals.getIDNumber());
398
            this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), null);
151 ilm 399
            return r.vals;
400
        } else {
401
            return null;
17 ilm 402
        }
403
    }
404
 
93 ilm 405
    private void _rm(final Row l) {
151 ilm 406
        assert isUpdateThread();
93 ilm 407
        if (l != null) {
408
            this.lines.remove(l);
409
            this.id2line.remove(l.id);
410
            this.dbVals.remove(l);
411
        }
412
    }
413
 
17 ilm 414
    @Override
93 ilm 415
    public void commit(final ListSQLLine l, final Path path, final SQLRowValues vals) {
416
        checkCanModif(path);
417
        if (!vals.isFrozen())
418
            throw new IllegalArgumentException("Not frozen");
151 ilm 419
        this.execInUpdateQ(new OfflineRunnable() {
93 ilm 420
            @Override
421
            public void run() {
151 ilm 422
                getModel().getUpdateQ().updateLine(l, path, vals.getID(), vals);
142 ilm 423
                recordOriginal(l);
93 ilm 424
            }
425
        });
17 ilm 426
    }
427
 
142 ilm 428
    public Future<?> replaceRow(final Number id, final SQLRowValues vals) {
429
        checkCanModif(Path.get(vals.getTable()));
430
        final SQLRowValues copy = vals.deepCopy();
431
        return this.updateRow(id, new IClosure<SQLRowValues>() {
432
            @Override
433
            public void executeChecked(SQLRowValues newVals) {
434
                final Set<String> contentFields = newVals.getTable().getFieldsNames(VirtualFields.CONTENT);
435
                newVals.clearReferents().removeAll(contentFields);
436
                newVals.load(copy, contentFields);
437
                if (copy.hasReferents()) {
438
                    for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(copy.getReferentsMap()).entrySet()) {
439
                        for (final SQLRowValues ref : e.getValue()) {
440
                            ref.put(e.getKey().getName(), newVals);
441
                        }
442
                    }
443
                }
444
                assert copy.getGraphSize() == 1;
445
            }
446
        }, false);
447
    }
448
 
449
    public Future<?> updateRow(final Number id, final SQLRowValues vals) {
450
        checkCanModif(Path.get(vals.getTable()));
451
        if (vals.getGraphSize() > 1)
452
            throw new IllegalArgumentException("This method doesn't merge graphs");
453
        final SQLRowValues copy = vals.deepCopy();
454
        return this.updateRow(id, new IClosure<SQLRowValues>() {
455
            @Override
456
            public void executeChecked(SQLRowValues newVals) {
457
                final Set<String> contentFields = newVals.getTable().getFieldsNames(VirtualFields.CONTENT);
458
                newVals.load(copy, contentFields);
459
            }
460
        }, false);
461
    }
462
 
151 ilm 463
    public Future<SQLRowValues> updateRow(final Number id, final IClosure<SQLRowValues> valsClosure) {
142 ilm 464
        return this.updateRow(id, valsClosure, true);
465
    }
466
 
151 ilm 467
    private Future<SQLRowValues> updateRow(final Number id, final IClosure<SQLRowValues> valsClosure, final boolean mdCanChange) {
468
        return this.execInUpdateQ(new OfflineCallable<SQLRowValues>() {
93 ilm 469
            @Override
151 ilm 470
            public SQLRowValues call() throws Exception {
471
                return _updateRow(getRow(id.intValue(), true), valsClosure, mdCanChange).vals;
93 ilm 472
            }
473
        });
474
    }
475
 
151 ilm 476
    public final SQLRowValues updateRowNow(final Row r, final IClosure<SQLRowValues> valsClosure) {
477
        checkUpdateThread();
478
        return _updateRow(r, valsClosure, true).vals;
479
    }
480
 
481
    protected Row _updateRow(final Row r, final IClosure<SQLRowValues> valsClosure, final boolean mdCanChange) {
482
        assert isUpdateThread();
142 ilm 483
        final SQLRowValues newVals = r.getRow().deepCopy();
484
        valsClosure.executeChecked(newVals);
485
        // make sure PK and metadata don't change
486
        if (mdCanChange) {
487
            final Set<String> notContent = newVals.getTable().getFieldsNames(VirtualFields.CONTENT.complement());
488
            newVals.removeAll(notContent);
489
            newVals.putAll(r.getRow().getValues(notContent, false));
490
        }
491
        assert CompareUtils.equals(r.getRow().getIDNumber(), newVals.getIDNumber());
492
        // make sure every needed path is there
493
        newVals.grow(getUpdateQueueReq().getGraphToFetch(), false);
494
        newVals.getGraph().freeze();
495
        setRow(r, newVals);
496
 
497
        // call createLine() to apply filter
498
        this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), createLine(r));
499
        return r;
500
    }
501
 
93 ilm 502
    private void checkCanModif(Path path) {
17 ilm 503
        if (this.modifiableVals.followPath(path) == null)
504
            throw new IllegalArgumentException("can only modify " + this.modifiableVals);
93 ilm 505
    }
506
 
142 ilm 507
    private void recordOriginal(ListSQLLine l) {
508
        setRow(getRow(l.getID(), true), l.getRow());
509
    }
510
 
511
    private void setRow(Row r, SQLRowValues newVals) {
512
        // if the existing row isn't in the DB, no need to update, the new values will be inserted
93 ilm 513
        if (r.getRow().hasID() && !this.dbVals.containsKey(r)) {
17 ilm 514
            // copy the initial state
93 ilm 515
            this.dbVals.put(r, r.getRow());
17 ilm 516
        }
142 ilm 517
        r.setRow(newVals);
17 ilm 518
    }
519
 
151 ilm 520
    public final <T> Future<T> useRow(final Number id, final ITransformerExn<SQLRowValues, T, ?> valsTransf) {
521
        return this.execInUpdateQ(new OfflineCallable<T>() {
522
            @Override
523
            public T call() throws Exception {
524
                final SQLRowValues vals = getRow(id.intValue(), true).vals;
525
                assert vals.isFrozen();
526
                return valsTransf.transformChecked(vals);
527
            }
528
        });
529
    }
530
 
531
    /**
532
     * Execute the passed transformer in the update queue, allow to call any method ending in "Now".
533
     *
534
     * @param rowsTransf what to do.
535
     * @return a future with the value returned by <code>rowsTransf</code>.
536
     */
537
    public final <T> Future<T> useRows(final ITransformerExn<SQLTableModelLinesSourceOffline, T, ?> rowsTransf) {
538
        return this.execInUpdateQ(new OfflineCallable<T>() {
539
            @Override
540
            public T call() throws Exception {
541
                return rowsTransf.transformChecked(SQLTableModelLinesSourceOffline.this);
542
            }
543
        });
544
    }
545
 
93 ilm 546
    // *** Order ***
547
 
548
    @Override
549
    public Future<?> moveBy(final List<? extends SQLRowAccessor> list, final int inc) {
550
        if (inc == 0 || list.size() == 0)
551
            return null;
552
 
553
        // since SQLRowValues isn't thread-safe, use Concurrent Collection to safely pass it to
554
        // another thread
555
        final List<SQLRowAccessor> copy = new CopyOnWriteArrayList<SQLRowAccessor>(list);
151 ilm 556
        return this.execInUpdateQ(new OfflineRunnable() {
93 ilm 557
            @Override
558
            public void run() {
559
                _moveBy(copy, inc);
560
            }
561
        });
17 ilm 562
    }
563
 
93 ilm 564
    protected void _moveBy(final List<? extends SQLRowAccessor> list, final int inc) {
151 ilm 565
        assert isUpdateThread();
132 ilm 566
        final int count = this.lines.size();
93 ilm 567
        final boolean after = inc > 0;
568
 
142 ilm 569
        final List<Integer> order;
93 ilm 570
        // same algorithm as MoveQueue
571
        int outerIndex = -1;
572
        final List<Row> ourLines = new ArrayList<Row>(list.size());
573
        for (final SQLRowAccessor r : list) {
142 ilm 574
            final Row ourLine = this.getRow(r.getID(), true, r);
93 ilm 575
            final int index = this.indexOf(ourLine);
576
            ourLines.add(ourLine);
577
            if (outerIndex < 0 || after && index > outerIndex || !after && index < outerIndex) {
578
                outerIndex = index;
579
            }
580
        }
132 ilm 581
        assert outerIndex >= 0 && ourLines.size() == list.size();
93 ilm 582
 
132 ilm 583
        this.lines.removeAll(ourLines);
584
        assert this.lines.size() == count - ourLines.size();
93 ilm 585
        final int newIndex = after ? outerIndex + inc - list.size() + 1 : outerIndex + inc;
586
        this.lines.addAll(newIndex, ourLines);
132 ilm 587
        assert this.lines.size() == count : "Move has changed the number of rows from " + count + " to " + this.lines.size();
93 ilm 588
        this.setDBOrder(false);
589
        order = this.getIDsOrder();
590
        this.getModel().getUpdateQ().reorder(order);
591
    }
592
 
142 ilm 593
    private List<Integer> getIDsOrder() {
594
        final List<Integer> ids = new ArrayList<Integer>();
93 ilm 595
        for (final Row r : this.lines)
596
            ids.add(r.getID());
597
        return ids;
598
    }
599
 
600
    @Override
601
    public Future<?> moveTo(final List<? extends Number> ids, final int index) {
151 ilm 602
        return this.execInUpdateQ(new OfflineRunnable() {
93 ilm 603
            @Override
604
            public void run() {
605
                _moveTo(ids, index);
606
            }
607
        });
608
    }
609
 
610
    protected void _moveTo(final List<?> ids, final int index) {
151 ilm 611
        assert isUpdateThread();
132 ilm 612
        final int count = this.lines.size();
93 ilm 613
 
142 ilm 614
        final List<Integer> order;
93 ilm 615
        final List<Row> list = new ArrayList<Row>(ids.size());
616
        for (final Object o : ids) {
142 ilm 617
            final Integer id = o instanceof SQLRowAccessor ? ((SQLRowAccessor) o).getID() : ((Number) o).intValue();
132 ilm 618
            list.add(this.getRow(id, true, o));
93 ilm 619
        }
620
        if (index <= 0) {
621
            this.lines.removeAll(list);
622
            this.lines.addAll(0, list);
623
        } else if (index >= this.lines.size()) {
624
            this.lines.removeAll(list);
625
            this.lines.addAll(list);
626
        } else {
627
            Row destLine = null;
628
            int i = index;
629
            boolean contains = true;
630
            while (i < this.lines.size() && contains) {
631
                destLine = this.lines.get(i);
632
                contains = list.contains(destLine);
633
                if (contains)
634
                    i++;
635
            }
636
            if (contains) {
637
                this.lines.removeAll(list);
638
                this.lines.addAll(list);
639
            } else {
640
                this.lines.removeAll(list);
641
                final int newIndex = this.indexOf(destLine);
642
                this.lines.addAll(newIndex, list);
643
            }
644
        }
132 ilm 645
        assert this.lines.size() == count : "Move has changed the number of rows from " + count + " to " + this.lines.size();
93 ilm 646
        this.setDBOrder(false);
647
        order = this.getIDsOrder();
648
        this.getModel().getUpdateQ().reorder(order);
649
    }
650
 
651
    // *** Roll back or Commit ***
652
 
151 ilm 653
    public final Future<Boolean> hasModifications() {
654
        return this.execInUpdateQ(new OfflineCallable<Boolean>() {
655
            @Override
656
            public Boolean call() throws Exception {
657
                return hasModificationsNow();
658
            }
659
        });
660
    }
661
 
662
    public final boolean hasModificationsNow() {
663
        checkUpdateThread();
664
        // ATTN this.dbOrder doesn't check just for order but also for added rows
665
        return !this.dbVals.isEmpty() || !this.deleted.isEmpty() || !this.dbOrder;
666
    }
667
 
17 ilm 668
    /**
93 ilm 669
     * Lose any changes and refetch from the database.
670
     *
671
     * @return the future.
672
     */
673
    public final Future<?> reset() {
151 ilm 674
        return this.execInUpdateQ(new OfflineRunnable() {
93 ilm 675
            @Override
676
            public void run() {
677
                _reset();
678
            }
679
        });
680
    }
681
 
682
    protected void _reset() {
151 ilm 683
        assert isUpdateThread();
93 ilm 684
 
685
        this.lines.clear();
686
        this.id2line.clear();
687
        this.dbVals.clear();
688
        this.deleted.clear();
689
        this.setDBOrder(true);
690
 
142 ilm 691
        for (final SQLRowValues r : this.fetch())
93 ilm 692
            this._add(r, false, false);
693
        this.getModel().getUpdateQ().setFullList(getLines(), null);
694
    }
695
 
696
    /**
17 ilm 697
     * Make all changes applied to this persistent.
698
     *
93 ilm 699
     * @return the future.
17 ilm 700
     */
93 ilm 701
    public final Future<?> commit() {
151 ilm 702
        return this.execInUpdateQ(new OfflineCallable<Object>() {
93 ilm 703
            @Override
704
            public Object call() throws Exception {
705
                _commit();
706
                return null;
707
            }
151 ilm 708
        });
93 ilm 709
    }
710
 
151 ilm 711
    public final void commitNow() throws SQLException {
712
        checkUpdateThread();
713
        this._commit();
714
    }
715
 
93 ilm 716
    protected final void _commit() throws SQLException {
151 ilm 717
        assert isUpdateThread();
93 ilm 718
 
719
        // don't listen to every commit and then to re-order, just updateAll() at the end
720
        this.getModel().getUpdateQ().rmTableListener();
721
 
722
        try {
723
            SQLUtils.executeAtomic(this.getParent().getPrimaryTable().getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
724
                @Override
725
                public Object handle(SQLDataSource ds) throws SQLException {
726
                    coreCommit();
727
                    return null;
728
                }
729
            });
730
        } finally {
731
            this.getModel().getUpdateQ().addTableListener();
732
        }
733
        this._reset();
734
    }
735
 
736
    protected void coreCommit() throws SQLException {
142 ilm 737
        // delete. Must be done first (e.g. there's a unique constraint and a deleted row conflicts
738
        // with a new row)
739
        getParent().getElem().archiveIDs(this.deleted);
740
        this.deleted.clear();
741
 
742
        // ordered rows
93 ilm 743
        final Map<Row, SQLRow> newRows = new LinkedHashMap<Row, SQLRow>();
17 ilm 744
        // insert, copy since we will remove some of the lines
93 ilm 745
        for (final Row l : this.lines) {
746
            final SQLRow newRow;
17 ilm 747
            if (!l.getRow().hasID()) {
748
                // only commit modified values, avoid updating each local, batiment, etc
93 ilm 749
                newRow = l.getRow().prune(this.modifiableVals).commit();
750
            } else {
751
                newRow = l.getRow().asRow();
17 ilm 752
            }
93 ilm 753
            // if the line is to be updated, this will get replaced below but it won't
142 ilm 754
            // change the ordering of the map
93 ilm 755
            newRows.put(l, newRow);
756
        }
17 ilm 757
 
758
        // update
93 ilm 759
        for (final Map.Entry<Row, SQLRowValues> e : this.dbVals.entrySet()) {
760
            final Row l = e.getKey();
761
            assert newRows.containsKey(l);
762
            newRows.put(l, this.getParent().getElem().update(e.getValue(), l.getRow()).exec());
763
        }
17 ilm 764
        this.dbVals.clear();
765
 
151 ilm 766
        if (getParent().getPrimaryTable().isOrdered()) {
767
            final List<SQLRow> wantedOrder = new ArrayList<SQLRow>(newRows.values());
768
            final List<SQLRow> dbOrder = new ArrayList<SQLRow>(newRows.values());
769
            Collections.sort(dbOrder, OrderComparator.INSTANCE);
770
            if (!wantedOrder.equals(dbOrder)) {
771
                MoveQueue.moveAtOnce(wantedOrder.subList(1, wantedOrder.size()), true, wantedOrder.get(0));
772
            }
93 ilm 773
        }
17 ilm 774
    }
775
}