OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 151 | 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.model;
15
 
93 ilm 16
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
144 ilm 17
import org.openconcerto.sql.model.SQLRowValues.FillMode;
17 ilm 18
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
19
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeEvent;
20
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeListener;
80 ilm 21
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 22
import org.openconcerto.sql.model.graph.Path;
93 ilm 23
import org.openconcerto.sql.model.graph.Step;
17 ilm 24
import org.openconcerto.sql.utils.SQLUtils;
83 ilm 25
import org.openconcerto.utils.CollectionMap2.Mode;
17 ilm 26
import org.openconcerto.utils.CollectionUtils;
27
import org.openconcerto.utils.CompareUtils;
28
import org.openconcerto.utils.Matrix;
29
import org.openconcerto.utils.RecursionType;
81 ilm 30
import org.openconcerto.utils.SetMap;
132 ilm 31
import org.openconcerto.utils.StringUtils;
32
import org.openconcerto.utils.StringUtils.Side;
17 ilm 33
import org.openconcerto.utils.cc.Closure;
34
import org.openconcerto.utils.cc.IClosure;
35
import org.openconcerto.utils.cc.ITransformer;
36
import org.openconcerto.utils.cc.IdentityHashSet;
37
import org.openconcerto.utils.cc.IdentitySet;
38
 
39
import java.sql.SQLException;
40
import java.util.ArrayList;
93 ilm 41
import java.util.Collection;
17 ilm 42
import java.util.Collections;
43
import java.util.Comparator;
44
import java.util.EventObject;
45
import java.util.HashSet;
46
import java.util.IdentityHashMap;
47
import java.util.Iterator;
83 ilm 48
import java.util.LinkedHashMap;
17 ilm 49
import java.util.List;
50
import java.util.Map;
93 ilm 51
import java.util.Map.Entry;
17 ilm 52
import java.util.Set;
53
import java.util.concurrent.atomic.AtomicInteger;
54
 
132 ilm 55
import net.jcip.annotations.Immutable;
56
 
17 ilm 57
/**
58
 * A set of linked SQLRowValues.
59
 *
60
 * @author Sylvain
61
 */
62
public class SQLRowValuesCluster {
63
    private static final Comparator<SQLField> FIELD_COMPARATOR = new Comparator<SQLField>() {
64
        @Override
65
        public int compare(SQLField o1, SQLField o2) {
66
            return o1.getSQLName().quote().compareTo(o2.getSQLName().quote());
67
        }
68
    };
69
 
70
    /**
71
     * The list of links in the order they've been set. This allows deterministic and predictable
72
     * insertion order. E.g. for
73
     *
74
     * <pre>
75
     * r2.put(f, r1);
76
     * r3.put(f2, r2);
77
     * r4.put(f2, r2);
78
     * </pre>
79
     *
80
     * the links will be :
81
     *
82
     * <pre>
83
     * r1
84
     * r2 f r1
85
     * r3 f2 r2
86
     * r4 f2 r2
87
     * </pre>
88
     */
89
    private final List<Link> links;
90
    private final IdentitySet<SQLRowValues> items;
91
    // { vals -> listener on vals' graph }
92
    private Map<SQLRowValues, List<ValueChangeListener>> listeners;
93
 
93 ilm 94
    private boolean frozen = false;
95
 
17 ilm 96
    private SQLRowValuesCluster() {
97
        this.links = new ArrayList<Link>();
98
        // SQLRowValues equals() depends on their values, but we must tell apart each reference
99
        this.items = new IdentityHashSet<SQLRowValues>();
100
        this.listeners = null;
101
    }
102
 
103
    SQLRowValuesCluster(SQLRowValues vals) {
104
        this();
83 ilm 105
        addVals(-1, vals);
106
    }
107
 
108
    // add a lonely node to this
109
    private final void addVals(final int index, final SQLRowValues vals) {
110
        assert vals.getGraph(false) == null;
111
        if (index < 0)
112
            this.links.add(new Link(vals));
113
        else
114
            this.links.add(index, new Link(vals));
17 ilm 115
        this.items.add(vals);
116
    }
117
 
118
    private final SQLRowValues getHead() {
119
        return this.links.get(0).getSrc();
120
    }
121
 
122
    private final DBSystemRoot getSystemRoot() {
123
        return this.getHead().getTable().getDBSystemRoot();
124
    }
125
 
151 ilm 126
    public final int getLinksCount() {
127
        return this.links.size();
128
    }
129
 
17 ilm 130
    /**
131
     * All the rowValues in this cluster.
132
     *
133
     * @return the set of SQLRowValues.
134
     */
135
    public final Set<SQLRowValues> getItems() {
136
        return Collections.unmodifiableSet(this.items);
137
    }
138
 
93 ilm 139
    /**
140
     * The number of items in this instance. NOTE: calling {@link SQLRowValues#getGraphSize()} is
141
     * preferable since it might avoid an allocation.
142
     *
143
     * @return the number of items.
144
     */
17 ilm 145
    public final int size() {
146
        return this.items.size();
147
    }
148
 
149
    public final boolean contains(SQLRowValues start) {
150
        return this.items.contains(start);
151
    }
152
 
153
    private final void containsCheck(SQLRowValues vals) {
154
        if (!this.contains(vals))
155
            throw new IllegalArgumentException(vals + " not in " + this);
156
    }
157
 
93 ilm 158
    // if true a single link path passed to followPath() will never yield more than one row
159
    final boolean hasOneRowPerPath() {
160
        for (final SQLRowValues item : this.getItems()) {
161
            for (final Entry<SQLField, Set<SQLRowValues>> e : item.getReferents().entrySet()) {
162
                if (e.getValue().size() > 1)
163
                    return false;
164
            }
165
        }
166
        return true;
167
    }
168
 
83 ilm 169
    public final Set<SQLTable> getTables() {
170
        final Set<SQLTable> res = new HashSet<SQLTable>();
171
        for (final SQLRowValues v : this.items)
172
            res.add(v.getTable());
173
        return res;
174
    }
175
 
93 ilm 176
    public final boolean isFrozen() {
177
        return this.frozen;
178
    }
179
 
180
    /**
181
     * Freeze this graph so that no modification can be made. Once this method returns, this
182
     * instance can be safely published (e.g. stored into a field that is properly guarded by a
183
     * lock) to other threads without further synchronizations.
184
     *
185
     * @return <code>true</code> if this call changed the frozen status.
186
     */
187
    public final boolean freeze() {
188
        if (this.frozen)
189
            return false;
190
        this.frozen = true;
191
        return true;
192
    }
193
 
17 ilm 194
    void remove(SQLRowValues src, SQLField f, SQLRowValues dest) {
195
        assert dest != null;
196
        assert src.getGraph() == this;
197
        assert src.getTable() == f.getTable();
93 ilm 198
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
17 ilm 199
 
200
        final Link toRm = new Link(src, f, dest);
201
        this.links.remove(toRm);
202
 
203
        // test if the removal of the link split the graph
204
        final IdentitySet<SQLRowValues> reachable = this.getReachable(src);
205
        if (reachable.size() < this.size()) {
206
            final SQLRowValuesCluster newCluster = new SQLRowValuesCluster();
207
 
208
            // moves the links no longer in us into a new cluster
209
            final Iterator<Link> iter = this.links.iterator();
210
            while (iter.hasNext()) {
211
                final Link l = iter.next();
212
                // the graph is split in two, so every link is in one part
213
                assert l.getDest() == null || reachable.contains(l.getSrc()) == reachable.contains(l.getDest());
214
                // hence only test the source
215
                if (!reachable.contains(l.getSrc())) {
216
                    iter.remove();
217
                    newCluster.links.add(l);
218
                }
219
            }
220
            // move unreachable items
221
            assert newCluster.items.isEmpty();
222
            final Iterator<SQLRowValues> itemIter = this.items.iterator();
223
            while (itemIter.hasNext()) {
224
                final SQLRowValues key = itemIter.next();
225
                if (!reachable.contains(key)) {
226
                    itemIter.remove();
227
                    newCluster.items.add(key);
228
                    if (this.listeners != null && this.listeners.containsKey(key))
229
                        newCluster.getListeners().put(key, this.listeners.remove(key));
230
                }
231
            }
90 ilm 232
            // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6612102
233
            // resolved in 7b25 : iterator.remove() might decrement the size twice : e.g. size is 0
234
            // and thus isEmpty() is true, while there's still elements in this.items
235
            assert !this.items.isEmpty() : "Empty items while removing " + f + " -> " + dest + " from " + src;
236
            assert !newCluster.items.isEmpty() : "New graph is empty while removing " + f + " -> " + dest + " from " + src;
237
            assert !CollectionUtils.containsAny(this.items, newCluster.items) : "Shared items while removing " + f + " -> " + dest + " from " + src;
17 ilm 238
 
239
            for (final SQLRowValues vals : newCluster.getItems())
240
                vals.setGraph(newCluster);
241
        }
242
    }
243
 
244
    void add(SQLRowValues src, SQLField f, SQLRowValues dest) {
245
        assert dest != null;
246
        assert src.getTable() == f.getTable();
93 ilm 247
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
248
 
83 ilm 249
        final boolean containsSrc = this.contains(src);
250
        final boolean containsDest = this.contains(dest);
251
        if (!containsSrc && !containsDest)
252
            throw new IllegalArgumentException("Neither source nor destination are contained in this :\n" + src + "\n" + dest);
17 ilm 253
 
254
        final Link toAdd = new Link(src, f, dest);
83 ilm 255
        if (containsSrc && containsDest) {
17 ilm 256
            // both source and dest are in us
257
            this.links.add(toAdd);
258
        } else {
83 ilm 259
            assert src.getGraph(false) != dest.getGraph(false);
260
            final SQLRowValues rowToAdd;
261
            final int index;
262
            if (containsSrc) {
263
                rowToAdd = dest;
264
                // merge the two graphs
93 ilm 265
                // add dest before src since it will be needed to store us (adding at the end would
266
                // work, but src would need to be inserted, then updated)
267
                // add dest just before src, to keep the order of foreigns (needed for deepCopy()
268
                // and insertion order) (adding at the beginning would reverse the order of foreign
269
                // rows to insert)
83 ilm 270
                final int srcIndex = this.links.indexOf(new Link(src));
271
                if (srcIndex < 0)
272
                    throw new IllegalStateException("Source link not found for " + src);
273
                index = srcIndex;
274
            } else {
275
                assert containsDest;
276
                rowToAdd = src;
277
                index = -1;
17 ilm 278
            }
83 ilm 279
            final SQLRowValuesCluster graphToAdd = rowToAdd.getGraph(false);
280
 
281
            // to preserve memory a single node has no graph unless required
282
            // this way rowToAdd never had to create a Cluster, it will use us.
283
            if (graphToAdd == null) {
284
                this.addVals(index, rowToAdd);
285
                rowToAdd.setGraph(this);
286
            } else {
287
                if (index < 0)
288
                    this.links.addAll(graphToAdd.links);
289
                else
290
                    this.links.addAll(index, graphToAdd.links);
291
                graphToAdd.links.clear();
292
                this.items.addAll(graphToAdd.items);
293
                for (final SQLRowValues newlyAdded : graphToAdd.items) {
294
                    newlyAdded.setGraph(this);
295
                }
296
                graphToAdd.items.clear();
297
                if (graphToAdd.listeners != null) {
298
                    this.getListeners().putAll(graphToAdd.listeners);
299
                    graphToAdd.listeners = null;
300
                }
17 ilm 301
            }
93 ilm 302
            // To keep foreign links order, new links should be added after existing ones from src
303
            // (which are after srcIndex). Since they're merged in store(), just add at the end.
304
            this.links.add(toAdd);
17 ilm 305
        }
306
        assert src.getGraph() == dest.getGraph();
307
    }
308
 
309
    private IdentitySet<SQLRowValues> getReachable(final SQLRowValues from) {
310
        final IdentitySet<SQLRowValues> res = new IdentityHashSet<SQLRowValues>();
311
        getReachableRec(from, res);
312
        return res;
313
    }
314
 
315
    private void getReachableRec(final SQLRowValues from, final IdentitySet<SQLRowValues> acc) {
316
        if (!acc.add(from))
317
            return;
318
 
319
        for (final SQLRowValues fVals : from.getForeigns().values()) {
320
            this.getReachableRec(fVals, acc);
321
        }
322
        for (final SQLRowValues fVals : from.getReferentRows()) {
323
            this.getReachableRec(fVals, acc);
324
        }
325
    }
326
 
132 ilm 327
    final SQLRowValues deepCopy(SQLRowValues v, final boolean freeze) {
328
        return deepCopy(freeze).get(v);
93 ilm 329
    }
330
 
132 ilm 331
    public final Map<SQLRowValues, SQLRowValues> deepCopy(final boolean freeze) {
151 ilm 332
        return this.copy(null, false, freeze);
333
    }
334
 
335
    /**
336
     * Copy a subset of this graph. For each link to copy, if the destination was copied then the
337
     * new row will point to it, otherwise the new row will point to the original row. For example,
338
     * if
339
     *
340
     * <pre>
341
     * start is CONTAINER <-- *ITEM [F1, F2]* --> PRIVATE
342
     * and graph is ITEM [F2, ID_PRIVATE]
343
     * then after this method :
344
     *  CONTAINER <-- ITEM [F1, F2] --> PRIVATE
345
     *            \-- ITEM [F2]     --> PRIVATE
346
     * </pre>
347
     *
348
     * @param start where to start copying.
349
     * @param graph which rows and which fields to copy, not <code>null</code>.
350
     * @return the new rows, indexed by the original rows in this instance.
351
     */
352
    public final Map<SQLRowValues, SQLRowValues> copy(final SQLRowValues start, final SQLRowValues graph) {
353
        return this.copy(computeToRetain(start, graph, true), true, false);
354
    }
355
 
356
    /**
357
     * Copy rows of this graph.
358
     *
359
     * @param subset which rows and which fields to copy, <code>null</code> to copy all rows.
360
     * @param allowSameGraph used when copied rows point to rows which weren't copied,
361
     *        <code>true</code> to point to the original rows (and thus linking the new rows into
362
     *        the existing graph), <code>false</code> to flatten the link (like
363
     *        {@link ForeignCopyMode#COPY_ID_OR_RM}).
364
     * @param freeze <code>true</code> if the copied rows should be frozen.
365
     * @return the new rows, indexed by the original rows in this instance.
366
     */
367
    private final Map<SQLRowValues, SQLRowValues> copy(final SetMap<SQLRowValues, String> subset, final boolean allowSameGraph, final boolean freeze) {
368
        assert !allowSameGraph || !freeze;
17 ilm 369
        // copy all rowValues of this graph
370
        final Map<SQLRowValues, SQLRowValues> noLinkCopy = new IdentityHashMap<SQLRowValues, SQLRowValues>();
93 ilm 371
        // don't copy foreigns, but we want to preserve the order of all fields. This works because
372
        // the second put() with the actual foreign row doesn't change the order.
373
        final ForeignCopyMode copyMode = ForeignCopyMode.COPY_NULL;
374
        for (final SQLRowValues n : this.getItems()) {
375
            final SQLRowValues copy;
151 ilm 376
            if (subset == null || subset.containsKey(n)) {
377
                // might as well use the minimum memory if the values won't change
378
                if (freeze) {
379
                    copy = new SQLRowValues(n.getTable(), n.size(), n.getForeignsSize(), n.getReferents().size());
380
                    copy.setAll(n.getAllValues(copyMode));
381
                } else {
382
                    copy = new SQLRowValues(n, copyMode);
383
                    if (subset != null) {
384
                        copy.retainAll(subset.get(n));
385
                    }
386
                }
387
                noLinkCopy.put(n, copy);
93 ilm 388
            }
389
        }
17 ilm 390
 
83 ilm 391
        // and link them together in order
151 ilm 392
        final List<Link> iterableLinks = subset == null ? this.links : new ArrayList<Link>(this.links);
393
        for (final Link l : iterableLinks) {
83 ilm 394
            if (l.getField() != null) {
151 ilm 395
                final SQLRowValues sourceCopy = noLinkCopy.get(l.getSrc());
396
                if (subset == null || (sourceCopy != null && sourceCopy.contains(l.getField().getName()))) {
397
                    assert l.getDest() != null;
398
                    final SQLRowValues destCopy = noLinkCopy.get(l.getDest());
399
                    final Object dest;
400
                    if (destCopy != null)
401
                        dest = destCopy;
402
                    else if (allowSameGraph)
403
                        dest = l.getDest();
404
                    else if (l.getDest().hasID())
405
                        dest = l.getDest().getIDNumber();
406
                    else
407
                        dest = null;
408
                    if (dest != null) {
409
                        sourceCopy.put(l.getField().getName(), dest);
410
                    } else {
411
                        // ForeignCopyMode.COPY_ID_OR_RM like pruneWithoutCopy() (avoids
412
                        // leaving nulls)
413
                        sourceCopy.remove(l.getField().getName());
414
                    }
415
                }
83 ilm 416
            } else {
151 ilm 417
                assert subset != null || noLinkCopy.containsKey(l.getSrc());
83 ilm 418
            }
17 ilm 419
        }
420
 
132 ilm 421
        final SQLRowValues res = noLinkCopy.values().iterator().next();
93 ilm 422
        // only force graph creation if needed
423
        if (freeze)
424
            res.getGraph().freeze();
425
        assert res.isFrozen() == freeze;
151 ilm 426
        assert allowSameGraph || res.getGraph() != this;
93 ilm 427
 
132 ilm 428
        return noLinkCopy;
17 ilm 429
    }
430
 
83 ilm 431
    public final StoreResult insert() throws SQLException {
142 ilm 432
        return this.store(StoreMode.INSERT);
41 ilm 433
    }
434
 
83 ilm 435
    public final StoreResult store(final StoreMode mode) throws SQLException {
142 ilm 436
        return this.store(mode, null);
83 ilm 437
    }
438
 
41 ilm 439
    // checkValidity false useful when we want to avoid loading the graph
142 ilm 440
    public final StoreResult store(final StoreMode mode, final Boolean checkValidity) throws SQLException {
441
        return this.store(mode, null, null, checkValidity, true);
132 ilm 442
    }
443
 
444
    /**
445
     * Store this graph into the DB.
446
     *
447
     * @param mode how to store.
448
     * @param start when storing a subset, the start of <code>pruneGraph</code> in this, can be
449
     *        <code>null</code>.
450
     * @param pruneGraph the maximum graph to store, can be <code>null</code>.
144 ilm 451
     * @param checkValidity whether to {@link SQLRowValues#getInvalid() checking validity} of rows,
452
     *        see {@link SQLRowValues#setValidityChecked(SQLRowValues.ValidityCheck)}.
142 ilm 453
     * @param fireEvent <code>false</code> if stored rows shouldn't be fetched and
454
     *        {@link SQLTableEvent} should not be fired.
132 ilm 455
     * @return the store result.
456
     * @throws SQLException if an exception occurs.
457
     * @see {@link #prune(SQLRowValues, SQLRowValues)}
458
     */
142 ilm 459
    public final StoreResult store(final StoreMode mode, final SQLRowValues start, final SQLRowValues pruneGraph, final Boolean checkValidity, final boolean fireEvent) throws SQLException {
132 ilm 460
        final Map<SQLRowValues, SQLRowValues> prune2orig;
461
        final SQLRowValuesCluster toStore;
462
        final boolean prune = pruneGraph != null;
463
        if (!prune) {
464
            toStore = this;
465
            prune2orig = null;
466
        } else {
467
            final Map<SQLRowValues, SQLRowValues> orig2prune = this.pruneMap(start, pruneGraph, true);
468
            toStore = orig2prune.get(start).getGraph();
469
            prune2orig = CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), orig2prune);
470
        }
471
        final Map<SQLRowValues, Node> nodes = new IdentityHashMap<SQLRowValues, Node>(toStore.size());
472
        final Map<SQLRowValues, Node> res = prune ? new IdentityHashMap<SQLRowValues, Node>(toStore.size()) : nodes;
473
        for (final SQLRowValues vals : toStore.getItems()) {
83 ilm 474
            nodes.put(vals, new Node(vals));
132 ilm 475
            if (prune) {
476
                final SQLRowValues src = prune2orig.get(vals);
477
                assert this.contains(src);
478
                res.put(src, nodes.get(vals));
479
            }
83 ilm 480
        }
17 ilm 481
        // check validity first, avoid beginning a transaction for nothing
482
        // do it after reset otherwise check previous values
142 ilm 483
        if (SQLRowValues.isValidityChecked(checkValidity))
83 ilm 484
            for (final Node n : nodes.values()) {
41 ilm 485
                n.noLink.checkValidity();
486
            }
17 ilm 487
        // this will hold the links and their ID as they are known
488
        /**
489
         * A cycle example :
490
         *
491
         * <pre>
492
         * s null
493
         * c ID_SITE s
494
         * c null
495
         * s ID_CONTACT_RAPPORT c
496
         * s ID_CONTACT_UTILE c
497
         * </pre>
498
         *
499
         * First s will be inserted :
500
         *
501
         * <pre>
502
         * c ID_SITE sID
503
         * c null
504
         * s ID_CONTACT_RAPPORT c
505
         * s ID_CONTACT_UTILE c
506
         * </pre>
507
         *
508
         * Then c :
509
         *
510
         * <pre>
511
         * s ID_CONTACT_RAPPORT cID
512
         * s ID_CONTACT_UTILE cID
513
         * </pre>
514
         *
515
         * And finally, s will be updated.
516
         */
132 ilm 517
        final List<StoringLink> storingLinks = new ArrayList<StoringLink>(toStore.links.size());
518
        for (final Link l : toStore.links)
17 ilm 519
            storingLinks.add(new StoringLink(l));
520
 
521
        // store the whole graph atomically
522
        final List<SQLTableEvent> events = SQLUtils.executeAtomic(getSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<List<SQLTableEvent>, SQLException>() {
523
            @Override
524
            public List<SQLTableEvent> handle(SQLDataSource ds) throws SQLException {
525
                final List<SQLTableEvent> res = new ArrayList<SQLTableEvent>();
526
                while (storingLinks.size() > 0) {
527
                    final StoringLink toStore = storingLinks.remove(0);
528
                    if (!toStore.canStore())
529
                        throw new IllegalStateException();
83 ilm 530
                    final Node n = nodes.get(toStore.getSrc());
17 ilm 531
 
532
                    // merge the maximum of links starting from the row to be stored
533
                    boolean lastDBAccess = true;
534
                    final Iterator<StoringLink> iter = storingLinks.iterator();
535
                    while (iter.hasNext()) {
536
                        final StoringLink sl = iter.next();
537
                        if (sl.getSrc() == toStore.getSrc()) {
538
                            if (sl.canStore()) {
539
                                iter.remove();
83 ilm 540
                                // sl can either be the main row or one the link from the row
541
                                // (bear in mind that toStore can be not the main row if the link
542
                                // destination has already been inserted)
17 ilm 543
                                if (sl.destID != null)
544
                                    n.noLink.put(sl.getField().getName(), sl.destID);
545
                            } else {
546
                                lastDBAccess = false;
547
                            }
548
                        }
549
                    }
550
 
551
                    if (n.isStored()) {
552
                        // if there's a cycle, we have to update an already inserted row
142 ilm 553
                        res.add(n.update(fireEvent));
17 ilm 554
                    } else {
142 ilm 555
                        res.add(n.store(fireEvent, mode));
17 ilm 556
                        final SQLRow r = n.getStoredRow();
557
 
558
                        // fill the noLink of referent nodes with the new ID
559
                        for (final StoringLink sl : storingLinks) {
560
                            if (sl.getDest() == toStore.getSrc()) {
561
                                sl.destID = r.getIDNumber();
83 ilm 562
                                nodes.get(sl.getSrc()).noLink.put(sl.getField().getName(), r.getIDNumber());
17 ilm 563
                            }
564
                        }
565
                    }
566
 
567
                    // link together the new values
568
                    // if there is a cycle not all foreign keys can be stored at the same time, so
569
                    // wait for the last DB access
142 ilm 570
                    if (lastDBAccess) {
17 ilm 571
                        for (final Map.Entry<String, SQLRowValues> e : toStore.getSrc().getForeigns().entrySet()) {
83 ilm 572
                            final SQLRowValues foreign = nodes.get(e.getValue()).getStoredValues();
17 ilm 573
                            assert foreign != null : "since this the last db access for this row, all foreigns should have been inserted";
574
                            // check coherence
575
                            if (n.getStoredValues().getLong(e.getKey()) != foreign.getIDNumber().longValue())
576
                                throw new IllegalStateException("stored " + n.getStoredValues().getObject(e.getKey()) + " but foreign is " + SQLRowValues.trim(foreign));
577
                            n.getStoredValues().put(e.getKey(), foreign);
578
                        }
142 ilm 579
                    }
17 ilm 580
                }
142 ilm 581
                // all nodes share the same graph, so pick any and freeze the graph
582
                // null if !fireEvent or if non-rowable table
583
                final SQLRowValues graphFetched = nodes.values().iterator().next().getStoredValues();
584
                if (graphFetched != null)
585
                    graphFetched.getGraph().freeze();
17 ilm 586
                return res;
587
            }
132 ilm 588
        });
17 ilm 589
        // fire events
142 ilm 590
        if (fireEvent) {
591
            for (final SQLTableEvent n : events) {
592
                // MAYBE put a Map<SQLRowValues, SQLTableEvent> to know how our fellow values have
593
                // been affected
594
                n.getTable().fire(n);
595
            }
17 ilm 596
        }
83 ilm 597
 
132 ilm 598
        return new StoreResult(res);
17 ilm 599
    }
600
 
83 ilm 601
    static public final class WalkOptions {
602
        private final Direction direction;
603
        private RecursionType recType;
604
        private boolean allowCycle;
605
        private boolean includeStart;
606
        private boolean ignoreForeignsOrder;
607
 
608
        public WalkOptions(final Direction dir) {
609
            if (dir == null)
610
                throw new NullPointerException("No direction");
611
            this.direction = dir;
612
            this.recType = RecursionType.BREADTH_FIRST;
613
            this.allowCycle = false;
614
            this.includeStart = true;
615
            this.ignoreForeignsOrder = true;
17 ilm 616
        }
83 ilm 617
 
618
        public Direction getDirection() {
619
            return this.direction;
620
        }
621
 
622
        public RecursionType getRecursionType() {
623
            return this.recType;
624
        }
625
 
626
        public WalkOptions setRecursionType(RecursionType recType) {
627
            if (recType == null)
628
                throw new NullPointerException("No type");
629
            this.recType = recType;
630
            return this;
631
        }
632
 
633
        public boolean isCycleAllowed() {
634
            return this.allowCycle;
635
        }
636
 
93 ilm 637
        /**
638
         * Set whether cycles (encountering the same row twice) are allowed. If <code>false</code>
639
         * is passed, then steps that go back to already visited rows will never be crossed :
640
         *
641
         * <pre>
642
         *       /- ID_CONTACT_RAPPORT \
643
         *  SITE -- ID_CONTACT_CHEF ---> CONTACT
644
         *       \-     ID_SITE        /
645
         * </pre>
646
         *
647
         * The SITE will only be passed once and ID_SITE would never be crossed from CONTACT. To go
648
         * through all paths, pass <code>true</code> and SITE will be visited 3 times if
649
         * {@link Direction#FOREIGN} and 7 times if {@link Direction#ANY}. Note that in both cases,
650
         * CONTACT will be visited 3 times if {@link Direction#ANY} and twice if
651
         * {@link Direction#FOREIGN}.
652
         *
653
         * @param allowCycle <code>true</code> if {@link State#getValsPath()} can contains the same
654
         *        row twice, <code>false</code> to stop before that happens.
655
         * @return this.
656
         */
83 ilm 657
        public WalkOptions setCycleAllowed(boolean allowCycle) {
658
            this.allowCycle = allowCycle;
659
            return this;
660
        }
661
 
662
        public boolean isStartIncluded() {
663
            return this.includeStart;
664
        }
665
 
666
        public WalkOptions setStartIncluded(boolean includeStart) {
667
            this.includeStart = includeStart;
668
            return this;
669
        }
670
 
671
        public boolean isForeignsOrderIgnored() {
672
            return this.ignoreForeignsOrder;
673
        }
674
 
675
        public WalkOptions setForeignsOrderIgnored(boolean ignoreForeignsOrder) {
676
            this.ignoreForeignsOrder = ignoreForeignsOrder;
677
            return this;
678
        }
17 ilm 679
    }
680
 
681
    /**
682
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
683
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
684
     * <code>start</code> is not the root, some nodes will not be traversed.
685
     *
686
     * @param <T> type of acc
687
     * @param start where to start the walk.
688
     * @param acc the initial value.
689
     * @param closure what to do on each node.
690
     */
691
    public final <T> void walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure) {
692
        this.walk(start, acc, closure, RecursionType.BREADTH_FIRST);
693
    }
694
 
695
    /**
696
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
697
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
698
     * <code>start</code> is not the root, some nodes will not be traversed. Also you can stop the
699
     * recursion by throwing {@link StopRecurseException} in <code>closure</code>.
700
     *
701
     * @param <T> type of acc
702
     * @param start where to start the walk.
703
     * @param acc the initial value.
704
     * @param closure what to do on each node.
705
     * @param recType how to recurse.
706
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
707
     */
708
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType) {
80 ilm 709
        return this.walk(start, acc, closure, recType, Direction.FOREIGN);
17 ilm 710
    }
711
 
80 ilm 712
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType, final Direction foreign) {
83 ilm 713
        return this.walk(start, acc, closure, new WalkOptions(foreign).setRecursionType(recType));
17 ilm 714
    }
715
 
83 ilm 716
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, final WalkOptions options) {
17 ilm 717
        this.containsCheck(start);
83 ilm 718
        return this.walk(new State<T>(Collections.singletonList(start), Path.get(start.getTable()), acc, closure), options, options.isStartIncluded());
17 ilm 719
    }
720
 
721
    /**
93 ilm 722
     * Walk through the graph from the passed state. NOTE: this method will call the {@link State}
723
     * with each path a row can be reached (except if this would cause a cycle, see
724
     * {@link WalkOptions#setCycleAllowed(boolean)}). E.g. for :
17 ilm 725
     *
93 ilm 726
     * <pre>
727
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT -- ID_TITLE ---> TITLE
728
     *       \- ID_CONTACT_RAPPORT /
729
     * </pre>
730
     *
731
     * The CONTACT and TITLE will be passed twice, once for each link.
732
     *
17 ilm 733
     * @param <T> type of acc.
734
     * @param state the current position in the graph.
83 ilm 735
     * @param options how to walk the graph.
17 ilm 736
     * @param computeThisState <code>false</code> if the <code>state</code> should not be
737
     *        {@link State#compute() computed}.
738
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
739
     */
83 ilm 740
    private final <T> StopRecurseException walk(final State<T> state, final WalkOptions options, final boolean computeThisState) {
741
        if (computeThisState && options.getRecursionType() == RecursionType.BREADTH_FIRST) {
17 ilm 742
            final StopRecurseException e = state.compute();
743
            if (e != null)
744
                return e;
745
        }
93 ilm 746
 
747
        if (!options.isCycleAllowed() || !state.hasCycle()) {
748
            // get the foreign or referents rowValues
749
            StopRecurseException res = null;
750
            if (options.getDirection() != Direction.REFERENT) {
751
                res = rec(state, options, Direction.FOREIGN);
752
            }
753
            if (res != null)
754
                return res;
755
            if (options.getDirection() != Direction.FOREIGN) {
756
                res = rec(state, options, Direction.REFERENT);
757
            }
758
            if (res != null)
759
                return res;
17 ilm 760
        }
761
 
83 ilm 762
        if (computeThisState && options.getRecursionType() == RecursionType.DEPTH_FIRST) {
17 ilm 763
            final StopRecurseException e = state.compute();
764
            if (e != null)
765
                return e;
766
        }
767
        return null;
768
    }
769
 
83 ilm 770
    private <T> StopRecurseException rec(final State<T> state, final WalkOptions options, final Direction actualDirection) {
17 ilm 771
        final SQLRowValues current = state.getCurrent();
772
        final List<SQLRowValues> currentValsPath = state.getValsPath();
81 ilm 773
        final SetMap<SQLField, SQLRowValues> nextVals;
80 ilm 774
        if (actualDirection == Direction.FOREIGN) {
17 ilm 775
            final Map<SQLField, SQLRowValues> foreigns = current.getForeignsBySQLField();
83 ilm 776
            nextVals = new SetMap<SQLField, SQLRowValues>(new LinkedHashMap<SQLField, Set<SQLRowValues>>(foreigns.size()), Mode.NULL_FORBIDDEN);
81 ilm 777
            nextVals.mergeScalarMap(foreigns);
80 ilm 778
        } else {
779
            assert actualDirection == Direction.REFERENT;
17 ilm 780
            nextVals = current.getReferents();
80 ilm 781
        }
83 ilm 782
        // predictable and repeatable order (SQLRowValues.referents has no order, but .foreigns has)
17 ilm 783
        final List<SQLField> keys = new ArrayList<SQLField>(nextVals.keySet());
83 ilm 784
        if (actualDirection == Direction.REFERENT || options.isForeignsOrderIgnored())
785
            Collections.sort(keys, FIELD_COMPARATOR);
17 ilm 786
        for (final SQLField f : keys) {
93 ilm 787
            final Step step = Step.create(f, actualDirection);
788
            // we must never go back from where we came
789
            final boolean backtrack = state.getPath().length() > 0 && state.getPath().getStep(-1).equals(step.reverse());
17 ilm 790
            for (final SQLRowValues v : nextVals.getNonNull(f)) {
93 ilm 791
                // backtrack is not enough, there can be other referents than previous
17 ilm 792
                // avoid infinite loop (don't use equals so that we can go over several equals rows)
93 ilm 793
                if (!(backtrack && v == state.getPrevious()) && (options.isCycleAllowed() || !state.identityContains(v))) {
794
                    final Path path = state.getPath().add(step);
17 ilm 795
                    final List<SQLRowValues> valsPath = new ArrayList<SQLRowValues>(currentValsPath);
796
                    valsPath.add(v);
93 ilm 797
                    final StopRecurseException e = this.walk(new State<T>(Collections.unmodifiableList(valsPath), path, state.getAcc(), state.closure), options, true);
17 ilm 798
                    if (e != null && e.isCompletely())
799
                        return e;
800
                }
801
            }
802
        }
803
        return null;
804
    }
805
 
132 ilm 806
    @Immutable
93 ilm 807
    static public final class IndexedRows {
808
        private final List<State<?>> flatList;
809
        private final Map<SQLRowValues, Integer> indexes;
810
 
132 ilm 811
        // optimization
812
        private IndexedRows(final SQLRowValues sole) {
813
            this(Collections.<State<?>> singletonList(new State<Object>(Collections.singletonList(sole), Path.get(sole.getTable()), null, null)), Collections.singletonMap(sole, 0));
814
            if (sole.getGraphSize() != 1)
815
                throw new IllegalArgumentException("Row is not alone : " + sole.printGraph());
816
        }
817
 
818
        // private to make sure the arguments are immutable
819
        private IndexedRows(List<State<?>> flatList, Map<SQLRowValues, Integer> indexes) {
93 ilm 820
            super();
821
            this.flatList = flatList;
822
            this.indexes = indexes;
823
            assert flatList.size() == indexes.size();
824
        }
825
 
132 ilm 826
        List<State<?>> getStates() {
93 ilm 827
            return this.flatList;
828
        }
829
 
830
        public int getSize() {
831
            return this.flatList.size();
832
        }
833
 
132 ilm 834
        State<?> getFirstState(int i) {
93 ilm 835
            return this.flatList.get(i);
836
        }
837
 
838
        public SQLRowValues getRow(int i) {
839
            return this.getFirstState(i).getCurrent();
840
        }
841
 
842
        public Path getFirstPath(int i) {
843
            return this.getFirstState(i).getPath();
844
        }
845
 
846
        public int getIndex(final SQLRowValues v) {
847
            return this.indexes.get(v);
848
        }
849
    }
850
 
851
    /**
852
     * Return all rows of this graph. This method will
853
     * {@link #walk(SQLRowValues, Object, ITransformer, WalkOptions) walk} the graph with the passed
854
     * parameters and collect the {@link State state} when it first reaches each row.
855
     *
856
     * @param vals where to start.
857
     * @param recType how to recurse.
858
     * @param useForeignsOrder <code>true</code> to use the order of foreign keys.
859
     * @return all rows.
860
     */
861
    public final IndexedRows getIndexedRows(final SQLRowValues vals, final RecursionType recType, final boolean useForeignsOrder) {
862
        final int size = this.size();
863
        final List<State<?>> flatList = new ArrayList<State<?>>(size);
864
        final IdentityHashMap<SQLRowValues, Integer> thisIndexes = new IdentityHashMap<SQLRowValues, Integer>(size);
865
 
866
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(recType).setForeignsOrderIgnored(!useForeignsOrder).setStartIncluded(true);
867
        this.walk(vals, null, new ITransformer<State<Object>, Object>() {
868
            @Override
869
            public Object transformChecked(State<Object> input) {
870
                final SQLRowValues r = input.getCurrent();
871
                if (thisIndexes.containsKey(r)) {
872
                    // happens when there's multiple paths between 2 rows
873
                    throw new StopRecurseException("already added").setCompletely(false);
874
                } else {
875
                    thisIndexes.put(r, flatList.size());
876
                    flatList.add(input);
877
                }
878
                return null;
879
            }
880
        }, walkOptions);
881
 
882
        assert flatList.size() == size : "missing rows, should have been " + size + " but was " + flatList.size() + " : " + flatList;
883
        return new IndexedRows(Collections.unmodifiableList(flatList), Collections.unmodifiableMap(thisIndexes));
884
    }
885
 
17 ilm 886
    final void walkFields(final SQLRowValues start, IClosure<FieldPath> closure, final boolean includeFK) {
80 ilm 887
        walkFields(start, Path.get(start.getTable()), Collections.singletonList(start), closure, includeFK);
17 ilm 888
    }
889
 
890
    private void walkFields(final SQLRowValues current, final Path p, final List<SQLRowValues> currentValsPath, IClosure<FieldPath> closure, final boolean includeFK) {
891
        final Map<String, SQLRowValues> foreigns = current.getForeigns();
892
        for (final String field : current.getFields()) {
893
            final boolean isFK = foreigns.containsKey(field);
894
            if (!isFK || includeFK)
895
                closure.executeChecked(new FieldPath(p, field));
896
            if (isFK) {
897
                final SQLRowValues newVals = foreigns.get(field);
898
                // avoid infinite loop
899
                if (!currentValsPath.contains(newVals)) {
80 ilm 900
                    final Path newP = p.add(current.getTable().getField(field), Direction.FOREIGN);
17 ilm 901
                    final List<SQLRowValues> newValsPath = new ArrayList<SQLRowValues>(currentValsPath);
902
                    newValsPath.add(newVals);
903
                    this.walkFields(newVals, newP, newValsPath, closure, includeFK);
904
                }
905
            }
906
        }
907
    }
908
 
909
    /**
93 ilm 910
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
911
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
912
     * contain all the paths.
17 ilm 913
     *
914
     * @param start where to start pruning, eg RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
915
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
93 ilm 916
     * @param graph the maximum structure wanted, eg RECEPTEUR {DESIGNATION=null}.
917
     * @return a copy of this no larger than <code>graph</code>, eg RECEPTEUR {DESIGNATION="rec"}.
17 ilm 918
     */
93 ilm 919
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph) {
920
        return this.prune(start, graph, true);
921
    }
922
 
923
    /**
924
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
925
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
926
     * contain all the paths. NOTE : only fields are considered, i.e. referents are not used. E.g.
927
     * with the rows below, no links would be removed, even though one LOCAL in the graph has no
928
     * CPI.
929
     *
930
     * <pre>
931
     * Graph :
932
     *        LOCAL LOCAL
933
     *      /        |
934
     *   SOURCE --> CPI
935
     *
936
     * This :
937
     *        LOCAL
938
     *      /       \
939
     *   SOURCE --> CPI
940
     * </pre>
941
     *
942
     *
943
     * @param start where to start pruning, e.g. RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
944
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
945
     * @param graph the maximum structure wanted, e.g. RECEPTEUR {DESIGNATION=null}.
946
     * @param keepUnionOfFields only relevant when the same row from this can be reached by more
947
     *        than one path in <code>graph</code>.
948
     *
949
     *        <pre>
950
     * Graph :
951
     *       /- ID_CONTACT_RAPPORT --> CONTACT1
952
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT2
953
     *
954
     * This :
955
     *       /- ID_CONTACT_RAPPORT \
956
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT
132 ilm 957
     *        </pre>
93 ilm 958
     *
959
     *        If <code>true</code> keep the union of all fields, if <code>false</code> the
960
     *        intersection.
961
     * @return a copy of this no larger than <code>graph</code>, e.g. RECEPTEUR {DESIGNATION="rec"}.
962
     */
963
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
132 ilm 964
        return pruneMap(start, graph, keepUnionOfFields).get(start);
965
    }
966
 
151 ilm 967
    public final Map<SQLRowValues, SQLRowValues> pruneMap(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
17 ilm 968
        this.containsCheck(start);
151 ilm 969
        return this.copy(computeToRetain(start, graph, keepUnionOfFields), false, false);
970
    }
971
 
972
    static SetMap<SQLRowValues, String> computeToRetain(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
17 ilm 973
        if (!start.getTable().equals(graph.getTable()))
974
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
93 ilm 975
        // there's no way to tell apart 2 referents
976
        if (!graph.getGraph().hasOneRowPerPath())
977
            throw new IllegalArgumentException("More than one row for " + graph.printGraph());
978
 
979
        final SetMap<SQLRowValues, String> toRetain = new SetMap<SQLRowValues, String>(new IdentityHashMap<SQLRowValues, Set<String>>(), Mode.NULL_FORBIDDEN);
980
        // BREADTH_FIRST to stop as soon as this no longer have rows in the graph
981
        // CycleAllowed since we need to go through every path (e.g. what is a cycle in graph might
982
        // not be in this :
983
        // SITE1 -> CONTACT1 -> SITE1 for graph and
984
        // SITE1 -> CONTACT1 -> SITE2 for this)
985
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setCycleAllowed(true);
986
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
17 ilm 987
            @Override
93 ilm 988
            public Object transformChecked(State<Object> input) {
989
                final SQLRowValues r = input.getCurrent();
990
                // since we allowed cycles in graph, allow it here
151 ilm 991
                final Collection<SQLRowValues> rows = start.followPath(input.getPath(), CreateMode.CREATE_NONE, false, true);
93 ilm 992
                // since we're using BREADTH_FIRST, the next path will be longer so no need to
993
                // continue if there's already no row
994
                if (rows.isEmpty())
995
                    throw new StopRecurseException().setCompletely(false);
996
                for (final SQLRowValues row : rows) {
997
                    if (keepUnionOfFields || !toRetain.containsKey(row))
998
                        toRetain.addAll(row, r.getFields());
999
                    else
1000
                        toRetain.getCollection(row).retainAll(r.getFields());
17 ilm 1001
                }
93 ilm 1002
                return null;
17 ilm 1003
            }
93 ilm 1004
        }, walkOptions);
151 ilm 1005
        return toRetain;
1006
    }
93 ilm 1007
 
151 ilm 1008
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph) {
1009
        return this.pruneWithoutCopy(start, graph, true);
1010
    }
1011
 
1012
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
1013
        this.containsCheck(start);
1014
        final SetMap<SQLRowValues, String> toRetain = computeToRetain(start, graph, keepUnionOfFields);
1015
        assert toRetain.containsKey(start);
1016
 
93 ilm 1017
        // remove extra fields and flatten if necessary
1018
        for (final Entry<SQLRowValues, Set<String>> e : toRetain.entrySet()) {
1019
            final SQLRowValues r = e.getKey();
1020
            r.retainAll(e.getValue());
1021
            final Set<String> toFlatten = new HashSet<String>();
1022
            for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1023
                final SQLRowValues foreign = e2.getValue();
1024
                if (!toRetain.containsKey(foreign)) {
1025
                    toFlatten.add(e2.getKey());
1026
                }
1027
            }
1028
            for (final String fieldToFlatten : toFlatten) {
1029
                r.flatten(fieldToFlatten, ForeignCopyMode.COPY_ID_OR_RM);
1030
            }
1031
        }
1032
        // now, remove referents that aren't in the graph
151 ilm 1033
        for (final SQLRowValues r : new ArrayList<SQLRowValues>(start.getGraph().getItems())) {
93 ilm 1034
            if (!toRetain.containsKey(r)) {
1035
                // only remove links at the border and even then, only remove links to the result :
1036
                // avoid creating a myriad of graphs
1037
                final Set<String> toFlatten = new HashSet<String>();
1038
                for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1039
                    final SQLRowValues foreign = e2.getValue();
1040
                    if (toRetain.containsKey(foreign)) {
1041
                        toFlatten.add(e2.getKey());
1042
                    }
1043
                }
1044
                r.removeAll(toFlatten);
1045
            }
1046
        }
151 ilm 1047
        assert start.getGraph().getItems().equals(toRetain.keySet());
1048
        // NOTE this instance no longer the graph of start if referents were removed
1049
        return start;
17 ilm 1050
    }
1051
 
142 ilm 1052
    // TODO handle referents (and decide how to handle multiple paths to the same node)
151 ilm 1053
    final void grow(final SQLRowValues start, final SQLRowValues toGrow, final boolean checkFields, final boolean growUndefined) {
17 ilm 1054
        this.containsCheck(start);
1055
        if (!start.getTable().equals(toGrow.getTable()))
1056
            throw new IllegalArgumentException(start + " is not from the same table as " + toGrow);
151 ilm 1057
        this.walk(start, toGrow, new ITransformer<State<SQLRowValues>, SQLRowValues>() {
17 ilm 1058
            @Override
151 ilm 1059
            public SQLRowValues transformChecked(State<SQLRowValues> input) {
17 ilm 1060
                final SQLRowValues existing = toGrow.followPath(input.getPath());
151 ilm 1061
                // don't add undefined row if there's none or if don't want
1062
                if (existing == null && input.getAcc().isForeignEmpty(input.getFrom().getName()) && (input.getPath().getLast().getUndefinedIDNumber() == null || !growUndefined))
1063
                    throw new StopRecurseException().setCompletely(false);
17 ilm 1064
                if (existing == null || (checkFields && !existing.getFields().containsAll(input.getCurrent().getFields()))) {
1065
                    final SQLRowValues leaf = toGrow.assurePath(input.getPath());
1066
                    if (leaf.hasID()) {
1067
                        final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(input.getCurrent());
151 ilm 1068
                        // don't exclude undef otherwise cannot grow eg
1069
                        // LOCAL.ID_FAMILLE_2 = 1
1070
                        if (growUndefined) {
1071
                            fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
1072
                                @Override
1073
                                public SQLSelect transformChecked(SQLSelect input) {
1074
                                    input.setExcludeUndefined(false);
1075
                                    return input;
1076
                                }
1077
                            });
1078
                        }
142 ilm 1079
                        final SQLRowValues fetched = fetcher.fetchOne(leaf.getIDNumber());
17 ilm 1080
                        if (fetched == null)
1081
                            throw new IllegalArgumentException("no row for " + fetcher);
1082
                        leaf.load(fetched, null);
151 ilm 1083
                        // we already loaded all further rows
1084
                        throw new StopRecurseException().setCompletely(false);
1085
                    } else {
17 ilm 1086
                        throw new IllegalArgumentException("cannot expand, missing ID in " + leaf + " at " + input.getPath());
151 ilm 1087
                    }
1088
                } else {
1089
                    return existing;
17 ilm 1090
                }
1091
            }
151 ilm 1092
        }, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
17 ilm 1093
    }
1094
 
1095
    public final String contains(final SQLRowValues start, SQLRowValues graph) {
1096
        return this.contains(start, graph, true);
1097
    }
1098
 
1099
    /**
1100
     * Whether the tree begining at <code>start</code> contains the tree begining at
1101
     * <code>graph</code>. Ie each path of <code>graph</code> must exist from <code>start</code>.
1102
     *
1103
     * @param start a SQLRowValues of this.
1104
     * @param graph another SQLRowValues.
1105
     * @param checkFields <code>true</code> to check that each rowValues of this containsAll the
1106
     *        fields of the other.
1107
     * @return a String explaining the first problem, <code>null</code> if <code>graph</code> is
1108
     *         contained in this.
1109
     */
1110
    public final String contains(final SQLRowValues start, SQLRowValues graph, final boolean checkFields) {
1111
        this.containsCheck(start);
1112
        if (!start.getTable().equals(graph.getTable()))
1113
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1114
        final StopRecurseException res = graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1115
            @Override
1116
            public Object transformChecked(State<Object> input) {
1117
                final SQLRowValues v = start.followPath(input.getPath());
1118
                if (v == null)
1119
                    throw new StopRecurseException("no " + input.getPath() + " in " + start);
1120
                if (checkFields && !v.getFields().containsAll(input.getCurrent().getFields()))
1121
                    throw new StopRecurseException("at " + input.getPath() + " " + v.getFields() + " does not contain " + input.getCurrent().getFields());
1122
 
1123
                return null;
1124
            }
1125
        }, RecursionType.BREADTH_FIRST);
1126
        return res == null ? null : res.getMessage();
1127
    }
1128
 
1129
    /**
144 ilm 1130
     * Merge one graph (including its fields) into another.
1131
     *
1132
     * Merge
1133
     *
1134
     * <pre>
1135
     *  LOCAL(DESIGNATION, ORDRE) <-- CPI(ORDRE)
1136
     *                             \--- SRC --/
1137
     * </pre>
1138
     *
1139
     * Into
1140
     *
1141
     * <pre>
1142
     *  LOCAL(DESIGNATION, ARCHIVE) <-- CPI(ARCHIVE)
1143
     * </pre>
1144
     *
1145
     * Result in this :
1146
     *
1147
     * <pre>
1148
     *  LOCAL(DESIGNATION, ORDRE, ARCHIVE) <-- CPI(ORDRE, ARCHIVE)
1149
     *                                      \--- SRC --/
1150
     * </pre>
1151
     *
1152
     * @param start the graph that will be modified.
1153
     * @param graph the graph that will be merged (not modified).
1154
     */
1155
    public final void merge(final SQLRowValues start, final SQLRowValues graph) {
1156
        this.containsCheck(start);
1157
        if (start == graph)
1158
            return;
1159
        if (!start.getTable().equals(graph.getTable()))
1160
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1161
        final Map<SQLRowValues, SQLRowValues> from = new IdentityHashMap<>();
1162
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1163
            @Override
1164
            public Object transformChecked(State<Object> input) {
1165
                final SQLRowValues previousReceiver = from.get(input.getPrevious());
1166
                final SQLRowValues alreadyMergedReceiver = from.get(input.getCurrent());
1167
                if (alreadyMergedReceiver != null) {
1168
                    // fields already merged just add a link
1169
                    previousReceiver.put(input.getPath().getStep(-1), alreadyMergedReceiver);
1170
                } else {
1171
                    final SQLRowValues currentReceiver;
1172
                    if (input.getPath().length() == 0) {
1173
                        assert input.getCurrent() == graph;
1174
                        assert previousReceiver == null;
1175
                        currentReceiver = start;
1176
                    } else {
1177
                        currentReceiver = previousReceiver.followPath(input.getPath().subPath(-1));
1178
                    }
1179
                    final SQLRowValues actualReceiver;
1180
                    if (currentReceiver != null) {
1181
                        actualReceiver = currentReceiver;
1182
                        // merge fields
1183
                        actualReceiver.putAll(input.getCurrent().getAllValues(ForeignCopyMode.COPY_NULL), null, FillMode.DONT_OVERWRITE);
1184
                    } else {
1185
                        // node didn't exist in the receiving graph, copy the single node with all
1186
                        // its fields
1187
                        actualReceiver = new SQLRowValues(input.getCurrent(), ForeignCopyMode.COPY_NULL);
1188
                        previousReceiver.put(input.getPath().getStep(-1), actualReceiver);
1189
                    }
1190
                    assert actualReceiver != null;
1191
                    from.put(input.getCurrent(), actualReceiver);
1192
                }
1193
                return null;
1194
            }
1195
            // cycle allowed to go through all links
1196
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setForeignsOrderIgnored(false).setCycleAllowed(true));
1197
    }
1198
 
1199
    /**
17 ilm 1200
     * Return a graphical representation of the tree rooted at <code>root</code>. The returned
1201
     * string is akin to the result of a query :
1202
     *
1203
     * <pre>
1204
     * BATIMENT[2]      LOCAL[5]         CPI_BT[5]
1205
     *                  LOCAL[3]
1206
     *                  LOCAL[2]         CPI_BT[3]
1207
     *                                   CPI_BT[2]
1208
     * </pre>
1209
     *
1210
     * In the above example, the BATIMENT has 3 LOCAL. LOCAL[3] is empty and LOCAL[2] has 2 CPI.
1211
     *
1212
     * @param root the root of the tree to print, eg BATIMENT[2].
1213
     * @param cellLength the length of each cell.
1214
     * @return a string representing the tree.
1215
     */
1216
    public final String printTree(final SQLRowValues root, int cellLength) {
1217
        this.containsCheck(root);
1218
        final Map<SQLRowValues, Integer> ys = new IdentityHashMap<SQLRowValues, Integer>();
1219
        final AtomicInteger currentY = new AtomicInteger(0);
1220
        final Matrix<SQLRowValues> matrix = new Matrix<SQLRowValues>();
1221
        this.walk(root, null, new Closure<State<Object>>() {
1222
            @Override
1223
            public void executeChecked(State<Object> input) {
1224
                // x is easy : it's the length of the path
1225
                // y : for each rowValues we go up the tree and set the y (if not already set)
1226
                // then y is either this value or the current line.
1227
                final SQLRowValues r = input.getCurrent();
1228
                final int y;
1229
                if (ys.containsKey(r))
1230
                    y = ys.get(r);
1231
                else
1232
                    y = currentY.getAndIncrement();
1233
                matrix.put(input.getPath().length(), y, input.getCurrent());
1234
 
1235
                final SQLRowValues ancestor = input.getPrevious();
1236
                if (ancestor != null) {
1237
                    ancestor.walkGraph(null, new Closure<State<Object>>() {
1238
                        @Override
1239
                        public void executeChecked(State<Object> input) {
1240
                            final SQLRowValues ancestorRow = input.getCurrent();
1241
                            if (!ys.containsKey(ancestorRow))
1242
                                ys.put(ancestorRow, y);
1243
                            else
1244
                                throw new StopRecurseException();
1245
                        }
1246
                    });
1247
                }
1248
            }
80 ilm 1249
        }, RecursionType.DEPTH_FIRST, Direction.REFERENT);
17 ilm 1250
 
1251
        return matrix.print(cellLength, new ITransformer<SQLRowValues, String>() {
1252
            @Override
1253
            public String transformChecked(SQLRowValues input) {
1254
                if (input == null)
1255
                    return "";
1256
                else if (input.hasID())
1257
                    // avoid requests
1258
                    return input.asRow().simpleToString();
1259
                else
1260
                    return input.getTable().toString();
1261
            }
1262
        });
1263
    }
1264
 
1265
    public final String printNodes() {
1266
        final StringBuilder sb = new StringBuilder(this.getClass().getSimpleName() + " of " + size() + " nodes:\n");
1267
        for (final SQLRowValues n : getItems()) {
132 ilm 1268
            StringUtils.appendFixedWidthString(sb, String.valueOf(System.identityHashCode(n)), 12, Side.LEFT, ' ', true);
1269
            sb.append(' ');
17 ilm 1270
            sb.append(n.getTable());
132 ilm 1271
            sb.append('\t');
17 ilm 1272
            for (final Map.Entry<String, SQLRowValues> e : n.getForeigns().entrySet()) {
1273
                sb.append(e.getKey());
1274
                sb.append(" -> ");
1275
                sb.append(System.identityHashCode(e.getValue()));
1276
                sb.append(" ; ");
1277
            }
1278
            sb.append(new SQLRowValues(n, ForeignCopyMode.NO_COPY));
1279
            sb.append("\n");
1280
        }
1281
        return sb.toString();
1282
    }
1283
 
132 ilm 1284
    @Immutable
1285
    public static final class DiffResult {
1286
 
1287
        private final String firstDiff;
1288
        private final SQLRowValues vals, otherVals;
1289
        // null if difference is trivial
1290
        private final IndexedRows thisRows, otherRows;
1291
 
1292
        private DiffResult(final String firstDiff, final SQLRowValues vals, final SQLRowValues otherVals) {
1293
            this.firstDiff = firstDiff;
1294
            this.thisRows = null;
1295
            this.otherRows = null;
1296
            this.vals = vals;
1297
            this.otherVals = otherVals;
1298
        }
1299
 
1300
        private DiffResult(final String firstDiff, final IndexedRows thisRows, final IndexedRows otherRows) {
1301
            super();
1302
            this.firstDiff = firstDiff;
1303
            this.thisRows = thisRows;
1304
            this.otherRows = otherRows;
1305
            assert !this.isEqual() || this.getRows1().getSize() == this.getRows2().getSize();
1306
            this.vals = thisRows.getRow(0);
1307
            this.otherVals = otherRows.getRow(0);
1308
        }
1309
 
1310
        public String getFirstDifference() {
1311
            return this.firstDiff;
1312
        }
1313
 
1314
        public final boolean isEqual() {
1315
            return this.getFirstDifference() == null;
1316
        }
1317
 
1318
        public SQLRowValues getRow1() {
1319
            return this.vals;
1320
        }
1321
 
1322
        public IndexedRows getRows1() {
1323
            return this.thisRows;
1324
        }
1325
 
1326
        public SQLRowValues getRow2() {
1327
            return this.otherVals;
1328
        }
1329
 
1330
        public IndexedRows getRows2() {
1331
            return this.otherRows;
1332
        }
1333
 
1334
        public final void fillRowMap(final Map<SQLRow, SQLRow> m, final boolean fromFirst) {
1335
            if (!this.isEqual())
1336
                throw new IllegalStateException("Rows are not equal : " + this.getFirstDifference());
1337
            final int stop = this.getRows1().getSize();
1338
            final IndexedRows i1, i2;
1339
            if (fromFirst) {
1340
                i1 = this.getRows1();
1341
                i2 = this.getRows2();
1342
            } else {
1343
                i1 = this.getRows2();
1344
                i2 = this.getRows1();
1345
            }
1346
            for (int i = 0; i < stop; i++) {
1347
                final SQLRow key = i1.getRow(i).asRow();
1348
                final SQLRow value = i2.getRow(i).asRow();
1349
                final SQLRow prev = m.put(key, value);
1350
                if (prev != null)
1351
                    throw new IllegalStateException(key + " already encountered in this : " + this.getRow1());
1352
            }
1353
        }
1354
    }
1355
 
1356
    private static final class DiffResultBuilder {
1357
        private final SQLRowValues vals, other;
1358
        private IndexedRows valsRows, otherRows;
1359
 
1360
        private DiffResultBuilder(SQLRowValues vals, SQLRowValues other) {
1361
            super();
1362
            this.vals = vals;
1363
            this.other = other;
1364
        }
1365
 
1366
        private void setRows(IndexedRows thisRows, IndexedRows otherRows) {
1367
            assert thisRows.getRow(0) == this.vals;
1368
            assert otherRows.getRow(0) == this.other;
1369
            this.valsRows = thisRows;
1370
            this.otherRows = otherRows;
1371
        }
1372
 
1373
        private DiffResult build(final String firstDiff) {
1374
            if (this.valsRows != null && this.otherRows != null) {
1375
                return new DiffResult(firstDiff, this.valsRows, this.otherRows);
1376
            } else if (firstDiff == null) {
1377
                // if rows are equal but there's no IndexedRows, then the graphs have only one row
1378
                return new DiffResult(firstDiff, new IndexedRows(this.vals), new IndexedRows(this.other));
1379
            } else {
1380
                return new DiffResult(firstDiff, this.vals, this.other);
1381
            }
1382
        }
1383
    }
1384
 
1385
    public final DiffResult getFirstDifference(final SQLRowValues vals, final SQLRowValues other, final boolean useForeignsOrder, final boolean useFieldsOrder, final boolean usePK) {
17 ilm 1386
        this.containsCheck(vals);
132 ilm 1387
        final DiffResultBuilder b = new DiffResultBuilder(vals, other);
93 ilm 1388
        if (vals == other)
132 ilm 1389
            return b.build(null);
93 ilm 1390
        final int size = this.size();
17 ilm 1391
        // don't call walk() if we can avoid as it is quite costly
93 ilm 1392
        if (size != other.getGraph().size())
132 ilm 1393
            return b.build("different size : " + size + " != " + other.getGraph().size());
1394
        else if (!vals.equalsJustThis(other, useFieldsOrder, false, usePK))
1395
            return b.build("unequal :\n" + vals + " !=\n" + other);
93 ilm 1396
        if (size == 1)
132 ilm 1397
            return b.build(null);
83 ilm 1398
 
17 ilm 1399
        // BREADTH_FIRST no need to go deep if the first values are not equals
93 ilm 1400
        final IndexedRows thisRows = this.getIndexedRows(vals, RecursionType.BREADTH_FIRST, useForeignsOrder);
1401
        final IndexedRows otherRows = other.getGraph().getIndexedRows(other, RecursionType.BREADTH_FIRST, useForeignsOrder);
132 ilm 1402
        b.setRows(thisRows, otherRows);
83 ilm 1403
 
93 ilm 1404
        // now check that each row is equal
83 ilm 1405
        // (this works because walk() always goes with the same order, see #FIELD_COMPARATOR and
1406
        // WalkOptions.setForeignsOrderIgnored())
93 ilm 1407
        for (int i = 0; i < size; i++) {
1408
            final SQLRowValues thisVals = thisRows.getRow(i);
1409
            final SQLRowValues oVals = otherRows.getRow(i);
1410
            final Path thisPath = thisRows.getFirstPath(i);
1411
            final Path oPath = otherRows.getFirstPath(i);
83 ilm 1412
 
93 ilm 1413
            if (!thisPath.equals(oPath))
132 ilm 1414
                return b.build("unequal graph at index " + i + " " + thisPath + " != " + oPath);
93 ilm 1415
 
1416
            // no need to check again
1417
            assert i != 0 || (thisVals == vals && oVals == other);
1418
            // don't use foreign ID, foreign rows are checked below
132 ilm 1419
            if (i != 0 && !thisVals.equalsJustThis(oVals, useFieldsOrder, false, usePK)) {
1420
                return b.build("unequal local values at " + thisPath + " :\n" + thisVals + " !=\n" + oVals);
17 ilm 1421
            }
93 ilm 1422
            final Map<String, SQLRowValues> thisForeigns = thisVals.getForeigns();
1423
            final Map<String, SQLRowValues> otherForeigns = oVals.getForeigns();
1424
            if (!thisForeigns.keySet().equals(otherForeigns.keySet()))
132 ilm 1425
                return b.build("unequal foreigns at " + thisPath + " :\n" + thisForeigns.keySet() + " !=\n" + otherForeigns.keySet());
93 ilm 1426
 
1427
            for (final Entry<String, SQLRowValues> e : thisForeigns.entrySet()) {
1428
                final String ff = e.getKey();
1429
                final SQLRowValues thisForeign = e.getValue();
1430
                final SQLRowValues otherForeign = otherForeigns.get(ff);
1431
                if (thisRows.getIndex(thisForeign) != otherRows.getIndex(otherForeign))
132 ilm 1432
                    return b.build("unequal foreign " + ff + " at " + thisPath + " for " + thisVals + " and " + oVals);
93 ilm 1433
                // since they point to the same index, the equality will be checked by the time this
1434
                // loop ends
1435
            }
1436
        }
1437
 
132 ilm 1438
        return b.build(null);
17 ilm 1439
    }
1440
 
1441
    static public final class StopRecurseException extends RuntimeException {
1442
 
1443
        private boolean completely = true;
1444
 
1445
        public StopRecurseException() {
1446
            super();
1447
        }
1448
 
1449
        public StopRecurseException(String message, Throwable cause) {
1450
            super(message, cause);
1451
        }
1452
 
1453
        public StopRecurseException(String message) {
1454
            super(message);
1455
        }
1456
 
1457
        public StopRecurseException(Throwable cause) {
1458
            super(cause);
1459
        }
1460
 
1461
        public final StopRecurseException setCompletely(boolean completely) {
1462
            this.completely = completely;
1463
            return this;
1464
        }
1465
 
1466
        public final boolean isCompletely() {
1467
            return this.completely;
1468
        }
1469
    }
1470
 
1471
    static public final class State<T> {
1472
        private final List<SQLRowValues> valsPath;
1473
        private final Path path;
1474
        private T acc;
1475
        private final ITransformer<State<T>, T> closure;
1476
 
1477
        State(List<SQLRowValues> valsPath, Path path, T acc, ITransformer<State<T>, T> closure) {
1478
            super();
1479
            this.valsPath = valsPath;
1480
            this.path = path;
1481
            this.acc = acc;
1482
            this.closure = closure;
1483
        }
1484
 
1485
        public SQLField getFrom() {
93 ilm 1486
            return this.path.length() == 0 ? null : this.path.getSingleField(this.path.length() - 1);
17 ilm 1487
        }
1488
 
1489
        /**
1490
         * Whether the last step of the path was taken backwards through a foreign field. Eg
1491
         * <code>true</code> if the path is BATIMENT,LOCAL.ID_BATIMENT, and <code>false</code> if
1492
         * LOCAL,LOCAL.ID_BATIMENT.
1493
         *
1494
         * @return <code>true</code> if the last step was backwards.
1495
         */
1496
        public final boolean isBackwards() {
1497
            if (this.path.length() == 0)
1498
                throw new IllegalStateException("empty path");
1499
            return this.path.isBackwards(this.path.length() - 1);
1500
        }
1501
 
1502
        /**
1503
         * Compute the new acc.
1504
         *
1505
         * @return whether this recursion should continue.
1506
         */
1507
        StopRecurseException compute() {
1508
            try {
1509
                this.acc = this.closure.transformChecked(this);
1510
                return null;
1511
            } catch (StopRecurseException e) {
1512
                return e;
1513
            }
1514
        }
1515
 
1516
        @Override
1517
        public String toString() {
1518
            return this.getClass().getSimpleName() + " path: " + this.path + " current node: " + this.getCurrent() + " current acc: " + this.getAcc();
1519
        }
1520
 
1521
        public final SQLRowValues getCurrent() {
1522
            return CollectionUtils.getLast(this.valsPath);
1523
        }
1524
 
1525
        public final SQLRowValues getPrevious() {
1526
            return CollectionUtils.getNoExn(this.valsPath, this.valsPath.size() - 2);
1527
        }
1528
 
1529
        public final List<SQLRowValues> getValsPath() {
1530
            return this.valsPath;
1531
        }
1532
 
1533
        final boolean identityContains(final SQLRowValues vals) {
93 ilm 1534
            return CollectionUtils.identityContains(this.valsPath, vals);
17 ilm 1535
        }
1536
 
93 ilm 1537
        boolean hasCycle() {
1538
            final int size = this.valsPath.size();
1539
            if (size < 2)
1540
                return false;
1541
            return CollectionUtils.identityContains(this.valsPath.subList(0, size - 1), this.valsPath.get(size - 1));
1542
        }
1543
 
17 ilm 1544
        public Path getPath() {
1545
            return this.path;
1546
        }
1547
 
1548
        public T getAcc() {
1549
            return this.acc;
1550
        }
1551
    }
1552
 
1553
    @Override
1554
    public String toString() {
1555
        return this.getClass().getSimpleName() + " " + this.links;
1556
    }
1557
 
1558
    private static class Link {
1559
        private final SQLRowValues src;
1560
        private final SQLField f;
1561
        private final SQLRowValues dest;
1562
 
1563
        public Link(final SQLRowValues src) {
1564
            this(src, null, null);
1565
        }
1566
 
1567
        public Link(final SQLRowValues src, final SQLField f, final SQLRowValues dest) {
1568
            if (src == null)
1569
                throw new NullPointerException("src is null");
25 ilm 1570
            assert (f == null && dest == null) || (dest != null && f.getTable() == src.getTable());
17 ilm 1571
            this.src = src;
1572
            this.f = f;
1573
            this.dest = dest;
1574
        }
1575
 
1576
        public final SQLRowValues getSrc() {
1577
            return this.src;
1578
        }
1579
 
1580
        public final SQLRowValues getDest() {
1581
            return this.dest;
1582
        }
1583
 
1584
        public final SQLField getField() {
1585
            return this.f;
1586
        }
1587
 
1588
        @Override
1589
        public int hashCode() {
1590
            final int prime = 31;
1591
            int result = 1;
25 ilm 1592
            result = prime * result + System.identityHashCode(this.src);
1593
            result = prime * result + System.identityHashCode(this.dest);
17 ilm 1594
            result = prime * result + ((this.f == null) ? 0 : this.f.hashCode());
1595
            return result;
1596
        }
1597
 
1598
        @Override
1599
        public boolean equals(Object obj) {
1600
            if (this == obj)
1601
                return true;
1602
            if (obj == null)
1603
                return false;
1604
            if (getClass() != obj.getClass())
1605
                return false;
1606
 
1607
            final Link other = (Link) obj;
1608
            return this.src == other.src && this.dest == other.dest && CompareUtils.equals(this.f, other.f);
1609
        }
1610
 
1611
        @Override
1612
        public String toString() {
1613
            return this.getClass().getSimpleName() + " " + System.identityHashCode(this.src) + (this.f == null ? "" : " " + this.f.getName() + " " + System.identityHashCode(this.dest));
1614
        }
1615
    }
1616
 
1617
    private static final class StoringLink extends Link {
1618
 
1619
        private Number destID;
1620
 
1621
        private StoringLink(Link l) {
1622
            super(l.getSrc(), l.getField(), l.getDest());
1623
            this.destID = null;
1624
        }
1625
 
1626
        public final boolean canStore() {
1627
            return this.getDest() == null || this.destID != null;
1628
        }
1629
 
1630
        @Override
1631
        public String toString() {
1632
            return super.toString() + " destID: " + this.destID;
1633
        }
1634
    }
1635
 
83 ilm 1636
    public static final class StoreResult {
1637
        private final Map<SQLRowValues, Node> nodes;
1638
 
1639
        public StoreResult(final Map<SQLRowValues, Node> nodes) {
1640
            this.nodes = nodes;
1641
        }
1642
 
93 ilm 1643
        final SQLRowValues getRowValuesFor(final SQLRow stored) {
1644
            for (final Entry<SQLRowValues, Node> e : this.nodes.entrySet()) {
1645
                if (e.getValue().getStoredRow().equals(stored))
1646
                    return e.getKey();
1647
            }
1648
            return null;
1649
        }
1650
 
1651
        /**
1652
         * Get the set of rows at the time the {@link SQLRowValuesCluster#store(StoreMode)} was
1653
         * performed. NOTE : the field values of the returned rows might have changed since the
1654
         * store but the other methods of this class use the instance identity.
1655
         *
1656
         * @return the rows that were stored.
1657
         */
1658
        final Set<SQLRowValues> getRowValues() {
1659
            return Collections.unmodifiableSet(this.nodes.keySet());
1660
        }
1661
 
83 ilm 1662
        public final int getStoredCount() {
1663
            return this.nodes.size();
1664
        }
1665
 
1666
        public final SQLRow getStoredRow(SQLRowValues vals) {
1667
            return this.nodes.get(vals).getStoredRow();
1668
        }
1669
 
1670
        public final SQLRowValues getStoredValues(SQLRowValues vals) {
1671
            return this.nodes.get(vals).getStoredValues();
1672
        }
93 ilm 1673
 
1674
        final List<SQLTableEvent> getEvents(SQLRowValues vals) {
1675
            return this.nodes.get(vals).getEvents();
1676
        }
83 ilm 1677
    }
1678
 
17 ilm 1679
    private static final class Node {
1680
 
1681
        // don't use noLink since it might contains foreigns if store() was just called
1682
        // or it might be out of sync with vals since the graph is only recreated on foreign change
1683
        /** vals without any links */
83 ilm 1684
        private final SQLRowValues noLink;
17 ilm 1685
        private final List<SQLTableEvent> modif;
1686
 
83 ilm 1687
        private Node(final SQLRowValues vals) {
17 ilm 1688
            this.modif = new ArrayList<SQLTableEvent>();
1689
            this.noLink = new SQLRowValues(vals, ForeignCopyMode.NO_COPY);
1690
        }
1691
 
142 ilm 1692
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode) throws SQLException {
1693
            return this.store(fetchStoredRow, mode, true);
1694
        }
1695
 
1696
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode, final boolean setRowValues) throws SQLException {
17 ilm 1697
            assert !this.isStored();
142 ilm 1698
            final SQLTableEvent evt = this.addEvent(mode.execOn(this.noLink, fetchStoredRow));
1699
            if (fetchStoredRow && evt.getRow() != null && setRowValues)
1700
                evt.setRowValues(evt.getRow().asRowValues());
1701
            return evt;
17 ilm 1702
        }
1703
 
142 ilm 1704
        private SQLTableEvent update(final boolean fetchStoredRow) throws SQLException {
17 ilm 1705
            assert this.isStored();
1706
 
1707
            // fields that have been updated since last store
1708
            final Set<String> fieldsToUpdate = new HashSet<String>(this.noLink.getFields());
1709
            fieldsToUpdate.removeAll(this.getEvent().getFieldNames());
1710
            assert fieldsToUpdate.size() > 0;
1711
 
1712
            final SQLRowValues updatingVals = this.getStoredRow().createEmptyUpdateRow();
1713
            updatingVals.load(this.noLink, fieldsToUpdate);
1714
 
142 ilm 1715
            final SQLTableEvent evt = new Node(updatingVals).store(fetchStoredRow, StoreMode.COMMIT, false);
17 ilm 1716
            // Update previous rowValues, and use it for the new event
1717
            // that way there's only one graph of rowValues (with the final values) for all events.
1718
            // Load all fields since updating 1 field might change the value of another (e.g.
1719
            // with a trigger).
142 ilm 1720
            if (fetchStoredRow && evt.getRow() != null) {
1721
                this.getStoredValues().load(evt.getRow(), null);
1722
                evt.setRowValues(this.getStoredValues());
1723
            }
17 ilm 1724
            return this.addEvent(evt);
1725
        }
1726
 
1727
        public final boolean isStored() {
1728
            return this.modif.size() > 0;
1729
        }
1730
 
1731
        // the last stored row, can be null for non-rowable tables
1732
        public final SQLRow getStoredRow() {
1733
            return this.getEvent() == null ? null : this.getEvent().getRow();
1734
        }
1735
 
1736
        public final SQLRowValues getStoredValues() {
1737
            // all events have the same values
1738
            return this.getEvent() == null ? null : this.getEvent().getRowValues();
1739
        }
1740
 
1741
        // the last event
1742
        private final SQLTableEvent getEvent() {
1743
            return CollectionUtils.getLast(this.modif);
1744
        }
1745
 
93 ilm 1746
        final List<SQLTableEvent> getEvents() {
1747
            return Collections.unmodifiableList(this.modif);
1748
        }
1749
 
17 ilm 1750
        private final SQLTableEvent addEvent(SQLTableEvent evt) {
142 ilm 1751
            if (evt == null)
1752
                throw new IllegalStateException("Couldn't update missing row  " + this.noLink);
17 ilm 1753
            this.modif.add(evt);
1754
            return evt;
1755
        }
1756
 
1757
        @Override
1758
        public String toString() {
1759
            return this.getClass().getSimpleName() + " " + this.noLink;
1760
        }
1761
    }
1762
 
1763
    /**
1764
     * What to do on each rowVals.
1765
     *
1766
     * @author Sylvain
1767
     */
1768
    public static abstract class StoreMode {
142 ilm 1769
        abstract SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException;
17 ilm 1770
 
1771
        public static final StoreMode COMMIT = new Commit();
142 ilm 1772
        public static final StoreMode INSERT = new Insert(false, false);
1773
        public static final StoreMode INSERT_VERBATIM = new Insert(true, true);
17 ilm 1774
    }
1775
 
1776
    public static class Insert extends StoreMode {
1777
 
1778
        private final boolean insertPK;
1779
        private final boolean insertOrder;
1780
 
1781
        public Insert(boolean insertPK, boolean insertOrder) {
1782
            super();
1783
            this.insertPK = insertPK;
1784
            this.insertOrder = insertOrder;
1785
        }
1786
 
1787
        @Override
142 ilm 1788
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
17 ilm 1789
            final Set<SQLField> autoFields = new HashSet<SQLField>();
1790
            if (!this.insertPK)
1791
                autoFields.addAll(vals.getTable().getPrimaryKeys());
1792
            if (!this.insertOrder)
1793
                autoFields.add(vals.getTable().getOrderField());
142 ilm 1794
            return vals.insertJustThis(fetchStoredRow, autoFields);
17 ilm 1795
        }
1796
    }
1797
 
1798
    public static class Commit extends StoreMode {
142 ilm 1799
 
17 ilm 1800
        @Override
142 ilm 1801
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
1802
            return vals.commitJustThis(fetchStoredRow);
17 ilm 1803
        }
1804
    }
1805
 
1806
    // * listeners
1807
 
1808
    // create if necessary
1809
    private final Map<SQLRowValues, List<ValueChangeListener>> getListeners() {
1810
        if (this.listeners == null)
1811
            this.listeners = new IdentityHashMap<SQLRowValues, List<ValueChangeListener>>(4);
1812
        return this.listeners;
1813
    }
1814
 
1815
    final void addValueListener(final SQLRowValues vals, ValueChangeListener l) {
1816
        this.containsCheck(vals);
1817
        List<ValueChangeListener> list = this.getListeners().get(vals);
1818
        if (list == null) {
1819
            list = new ArrayList<ValueChangeListener>();
1820
            this.getListeners().put(vals, list);
1821
        }
1822
        list.add(l);
1823
    }
1824
 
1825
    final void removeValueListener(final SQLRowValues vals, ValueChangeListener l) {
1826
        if (this.listeners != null && this.listeners.containsKey(vals)) {
1827
            final List<ValueChangeListener> list = this.listeners.get(vals);
1828
            list.remove(l);
1829
            // never leave an empty list so that hasListeners() works
1830
            if (list.size() == 0)
1831
                this.listeners.remove(vals);
1832
        }
1833
    }
1834
 
1835
    final void fireModification(SQLRowValues vals, String fieldName, Object newValue) {
1836
        if (hasListeners())
1837
            fireModification(new ValueChangeEvent(vals, fieldName, newValue));
1838
    }
1839
 
1840
    final void fireModification(SQLRowValues vals, Map<String, ?> m) {
1841
        if (hasListeners())
1842
            fireModification(new ValueChangeEvent(vals, m));
1843
    }
1844
 
1845
    final void fireModification(SQLRowValues vals, Set<String> removed) {
1846
        if (hasListeners())
1847
            fireModification(new ValueChangeEvent(vals, removed));
1848
    }
1849
 
1850
    private final void fireModification(final ValueChangeEvent evt) {
1851
        for (final List<ValueChangeListener> list : this.listeners.values())
1852
            for (ValueChangeListener l : list)
1853
                l.valueChange(evt);
1854
    }
1855
 
1856
    final void fireModification(final ReferentChangeEvent evt) {
1857
        if (referentFireNeeded(evt.isAddition())) {
1858
            for (final List<ValueChangeListener> list : this.listeners.values())
1859
                for (ValueChangeListener l : list)
1860
                    l.referentChange(evt);
1861
        }
1862
    }
1863
 
1864
    final boolean referentFireNeeded(final boolean put) {
1865
        // if isAddition there will be a valueChange() for the same put()
1866
        return this.hasListeners() && !put;
1867
    }
1868
 
1869
    final boolean hasListeners() {
1870
        return this.listeners != null && this.listeners.size() > 0;
1871
    }
1872
 
1873
    public static class ValueChangeEvent extends EventObject {
1874
 
1875
        private final Map<String, ?> added;
1876
        private final Set<String> removed;
1877
 
1878
        private ValueChangeEvent(final SQLRowValues vals, final Map<String, ?> m) {
1879
            super(vals);
1880
            this.added = Collections.unmodifiableMap(m);
1881
            this.removed = Collections.emptySet();
1882
        }
1883
 
1884
        public ValueChangeEvent(SQLRowValues vals, String fieldName, Object newValue) {
1885
            super(vals);
1886
            this.added = Collections.singletonMap(fieldName, newValue);
1887
            this.removed = Collections.emptySet();
1888
        }
1889
 
1890
        public ValueChangeEvent(SQLRowValues vals, Set<String> removed) {
1891
            super(vals);
1892
            this.added = Collections.emptyMap();
1893
            this.removed = Collections.unmodifiableSet(removed);
1894
        }
1895
 
1896
        @Override
1897
        public SQLRowValues getSource() {
1898
            return (SQLRowValues) super.getSource();
1899
        }
1900
 
1901
        public final Set<String> getAddedFields() {
1902
            return this.added.keySet();
1903
        }
1904
 
1905
        public final Set<String> getRemovedFields() {
1906
            return this.removed;
1907
        }
1908
 
1909
        public final Map<String, ?> getAddedValues() {
1910
            return this.added;
1911
        }
1912
 
1913
        @Override
1914
        public String toString() {
1915
            return super.toString() + " added : " + getAddedFields() + " removed: " + getRemovedFields();
1916
        }
1917
    }
1918
 
1919
    /**
1920
     * This listener is notified whenever a rowValues changes. Note that
1921
     * {@link ValueChangeListener#referentChange(ReferentChangeEvent)} is only called for
1922
     * {@link ReferentChangeEvent#isRemoval() removals} since for addition there will be a
1923
     * {@link ValueChangeListener#valueChange(ValueChangeEvent)}.
1924
     *
1925
     * @author Sylvain CUAZ
1926
     */
1927
    public static interface ValueChangeListener extends ReferentChangeListener {
1928
 
1929
        void valueChange(ValueChangeEvent evt);
1930
 
1931
    }
1932
}