OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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