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
 
80 ilm 16
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
17 ilm 17
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
83 ilm 18
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
67 ilm 19
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 20
import org.openconcerto.sql.model.graph.Path;
21
import org.openconcerto.sql.model.graph.Step;
83 ilm 22
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
132 ilm 23
import org.openconcerto.utils.CollectionUtils;
17 ilm 24
import org.openconcerto.utils.CompareUtils;
93 ilm 25
import org.openconcerto.utils.CopyUtils;
80 ilm 26
import org.openconcerto.utils.ListMap;
17 ilm 27
import org.openconcerto.utils.RTInterruptedException;
28
import org.openconcerto.utils.RecursionType;
151 ilm 29
import org.openconcerto.utils.SetMap;
17 ilm 30
import org.openconcerto.utils.Tuple2;
31
import org.openconcerto.utils.cc.ITransformer;
144 ilm 32
import org.openconcerto.utils.cc.LinkedIdentitySet;
142 ilm 33
import org.openconcerto.utils.cc.Transformer;
17 ilm 34
 
35
import java.sql.ResultSet;
36
import java.sql.SQLException;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.Collections;
144 ilm 40
import java.util.Deque;
17 ilm 41
import java.util.HashMap;
42
import java.util.HashSet;
80 ilm 43
import java.util.LinkedHashMap;
73 ilm 44
import java.util.LinkedHashSet;
144 ilm 45
import java.util.LinkedList;
17 ilm 46
import java.util.List;
47
import java.util.Map;
48
import java.util.Map.Entry;
49
import java.util.Set;
50
import java.util.concurrent.Callable;
51
import java.util.concurrent.ExecutorService;
52
import java.util.concurrent.Future;
53
import java.util.concurrent.LinkedBlockingQueue;
54
import java.util.concurrent.ThreadPoolExecutor;
55
import java.util.concurrent.TimeUnit;
83 ilm 56
import java.util.concurrent.atomic.AtomicInteger;
80 ilm 57
import java.util.concurrent.atomic.AtomicReference;
17 ilm 58
 
132 ilm 59
import org.apache.commons.dbutils.ResultSetHandler;
60
 
93 ilm 61
import net.jcip.annotations.GuardedBy;
62
import net.jcip.annotations.ThreadSafe;
63
 
17 ilm 64
/**
65
 * Construct a list of linked SQLRowValues from one request.
66
 *
67
 * @author Sylvain
68
 */
93 ilm 69
@ThreadSafe
17 ilm 70
public class SQLRowValuesListFetcher {
71
 
61 ilm 72
    /**
65 ilm 73
     * Create an ordered fetcher with the necessary grafts to fetch the passed graph.
61 ilm 74
     *
75
     * @param graph what to fetch, can be any tree.
76
     * @return the fetcher.
77
     */
78
    public static SQLRowValuesListFetcher create(final SQLRowValues graph) {
65 ilm 79
        // ORDER shouldn't slow down the query and it makes the result predictable and repeatable
80
        return create(graph, true);
81
    }
82
 
83
    public static SQLRowValuesListFetcher create(final SQLRowValues graph, final boolean ordered) {
61 ilm 84
        // path -> longest referent only path
85
        // i.e. map each path to the main fetcher or a referent graft
86
        final Map<Path, Path> handledPaths = new HashMap<Path, Path>();
80 ilm 87
        final Path emptyPath = Path.get(graph.getTable());
61 ilm 88
        handledPaths.put(emptyPath, emptyPath);
89
        // find out referent only paths (yellow in the diagram)
90
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
91
            @Override
92
            public Path transformChecked(State<Object> input) {
93
                final Path p = input.getPath();
94
                for (int i = p.length(); i > 0; i--) {
95
                    final Path subPath = p.subPath(0, i);
96
                    if (handledPaths.containsKey(subPath))
97
                        break;
98
                    handledPaths.put(subPath, p);
99
                }
100
                return null;
101
            }
80 ilm 102
        }, RecursionType.DEPTH_FIRST, Direction.REFERENT);
61 ilm 103
 
104
        // find out needed grafts
83 ilm 105
        final ListMap<Path, SQLRowValuesListFetcher> grafts = new ListMap<Path, SQLRowValuesListFetcher>();
61 ilm 106
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
107
            @Override
108
            public Path transformChecked(State<Object> input) {
109
                final Path p = input.getPath();
110
                if (!handledPaths.containsKey(p)) {
111
                    final Path pMinusLast = p.minusLast();
112
                    if (!input.isBackwards()) {
113
                        // Forwards can be fetched by existing fetcher (blue in the diagram)
114
                        final Path existingRefPath = handledPaths.get(pMinusLast);
115
                        assert existingRefPath != null;
116
                        handledPaths.put(p, existingRefPath);
117
                    } else {
118
                        // Backwards needs another fetcher
119
                        if (!grafts.containsKey(pMinusLast)) {
120
                            final SQLRowValues copy = graph.deepCopy();
121
                            final SQLRowValues graftNode = copy.followPath(pMinusLast);
122
                            graftNode.clear();
123
                            final SQLRowValues previous = copy.followPath(pMinusLast.minusLast());
124
                            assert p.getStep(-2).isForeign();
125
                            previous.remove(p.getStep(-2).getSingleField().getName());
126
                            // don't recurse forever
127
                            if (previous.getGraph() == graftNode.getGraph())
128
                                throw new IllegalArgumentException("Graph is not a tree");
65 ilm 129
                            // ATTN pMinusLast might not be on the main fetcher so don't graft now
130
                            // also we can only graft non empty descendant path fetchers (plus
131
                            // removing a fetcher saves one request)
132
                            final SQLRowValuesListFetcher rec = create(graftNode, ordered);
133
                            final Collection<SQLRowValuesListFetcher> ungrafted = rec.ungraft();
134
                            if (ungrafted == null || ungrafted.size() == 0) {
135
                                // i.e. only one referent and thus graft not necessary
136
                                assert rec.descendantPath.length() > 0;
83 ilm 137
                                grafts.add(pMinusLast, rec);
65 ilm 138
                            } else {
83 ilm 139
                                grafts.addAll(pMinusLast, ungrafted);
65 ilm 140
                            }
61 ilm 141
                        }
142
                        throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
143
                    }
144
                }
145
                return null;
146
            }
83 ilm 147
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
61 ilm 148
 
149
        final Set<Path> refPaths = new HashSet<Path>(handledPaths.values());
150
        // remove the main fetcher
151
        refPaths.remove(emptyPath);
65 ilm 152
        // fetchers for the referent paths (yellow part)
61 ilm 153
        final Map<Path, SQLRowValuesListFetcher> graftedFetchers;
154
        // create the main fetcher and grafts
155
        final SQLRowValuesListFetcher res;
156
        if (refPaths.size() == 1) {
157
            res = new SQLRowValuesListFetcher(graph, refPaths.iterator().next());
158
            graftedFetchers = Collections.emptyMap();
159
        } else {
160
            res = new SQLRowValuesListFetcher(graph, false);
161
            graftedFetchers = new HashMap<Path, SQLRowValuesListFetcher>();
162
            if (refPaths.size() > 0) {
163
                final Path graftPath = new Path(graph.getTable());
164
                final SQLRowValues copy = graph.deepCopy();
165
                copy.clear();
166
                for (final Path refPath : refPaths) {
65 ilm 167
                    final SQLRowValuesListFetcher f = new SQLRowValuesListFetcher(copy, refPath, true).setOrdered(ordered);
61 ilm 168
                    res.graft(f, graftPath);
169
                    graftedFetchers.put(refPath, f);
170
                }
171
            }
172
        }
65 ilm 173
        res.setOrdered(ordered);
174
 
61 ilm 175
        // now graft recursively created grafts
83 ilm 176
        for (final Entry<Path, ? extends Collection<SQLRowValuesListFetcher>> e : grafts.entrySet()) {
61 ilm 177
            final Path graftPath = e.getKey();
178
            final Path refPath = handledPaths.get(graftPath);
65 ilm 179
            // can be grafted on the main fetcher or on the referent fetchers
61 ilm 180
            final SQLRowValuesListFetcher f = graftedFetchers.containsKey(refPath) ? graftedFetchers.get(refPath) : res;
65 ilm 181
            for (final SQLRowValuesListFetcher recFetcher : e.getValue())
182
                f.graft(recFetcher, graftPath);
61 ilm 183
        }
184
        return res;
185
    }
186
 
17 ilm 187
    // return the referent single link path starting from graph
188
    private static Path computePath(SQLRowValues graph) {
189
        // check that there's only one referent for each row
190
        // (otherwise huge joins, e.g. LOCAL<-CPI,SOURCE,RECEPTEUR,etc.)
80 ilm 191
        final AtomicReference<Path> res = new AtomicReference<Path>(null);
192
        graph.getGraph().walk(graph, null, new ITransformer<State<Path>, Path>() {
17 ilm 193
            @Override
194
            public Path transformChecked(State<Path> input) {
195
                final Collection<SQLRowValues> referentRows = input.getCurrent().getReferentRows();
80 ilm 196
                final int size = referentRows.size();
197
                if (size > 1) {
17 ilm 198
                    // remove the foreign rows which are all the same (since they point to
199
                    // current) so the exn is more legible
200
                    final List<SQLRowValues> toPrint = SQLRowValues.trim(referentRows);
201
                    throw new IllegalArgumentException(input.getCurrent() + " is referenced by " + toPrint + "\nat " + input.getPath());
80 ilm 202
                } else if (size == 0) {
203
                    if (res.get() == null)
204
                        res.set(input.getPath());
205
                    else
206
                        throw new IllegalStateException();
17 ilm 207
                }
208
                return input.getAcc();
209
            }
83 ilm 210
        }, RecursionType.BREADTH_FIRST, Direction.REFERENT);
80 ilm 211
        // since includeStart=true
212
        assert res.get() != null;
213
        return res.get();
17 ilm 214
    }
215
 
83 ilm 216
    static private final ListMap<Tuple2<Path, Number>, SQLRowValues> createCollectionMap() {
17 ilm 217
        // we need a List in merge()
83 ilm 218
        return new ListMap<Tuple2<Path, Number>, SQLRowValues>() {
219
            @Override
220
            public List<SQLRowValues> createCollection(Collection<? extends SQLRowValues> v) {
221
                final List<SQLRowValues> res = new ArrayList<SQLRowValues>(8);
222
                res.addAll(v);
223
                return res;
224
            }
225
        };
17 ilm 226
    }
227
 
93 ilm 228
    // unmodifiable
17 ilm 229
    private final SQLRowValues graph;
230
    private final Path descendantPath;
93 ilm 231
    @GuardedBy("this")
132 ilm 232
    private List<ITransformer<SQLSelect, SQLSelect>> selTransf;
93 ilm 233
    @GuardedBy("this")
234
    private Number selID;
235
    @GuardedBy("this")
73 ilm 236
    private Set<Path> ordered;
93 ilm 237
    @GuardedBy("this")
73 ilm 238
    private boolean descendantsOrdered;
93 ilm 239
    @GuardedBy("this")
17 ilm 240
    private SQLRowValues minGraph;
93 ilm 241
    @GuardedBy("this")
17 ilm 242
    private boolean includeForeignUndef;
93 ilm 243
    @GuardedBy("this")
17 ilm 244
    private SQLSelect frozen;
93 ilm 245
    @GuardedBy("this")
246
    private boolean freezeRows;
247
    // graftPlace -> {referent path -> fetcher}, unmodifiable
248
    @GuardedBy("this")
249
    private Map<Path, Map<Path, SQLRowValuesListFetcher>> grafts;
151 ilm 250
    // {pathToAdd, existingPath}, unmodifiable
251
    @GuardedBy("this")
252
    private Map<Path, Path> postFetchLinks;
17 ilm 253
 
254
    /**
255
     * Construct a new instance with the passed graph of SQLRowValues.
256
     *
257
     * @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
258
     *        SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
259
     *        to return all sites (with all their fields) with their associated contacts.
260
     */
261
    public SQLRowValuesListFetcher(SQLRowValues graph) {
262
        this(graph, false);
263
    }
264
 
265
    /**
266
     * Construct a new instance with the passed graph of SQLRowValues. Eg if <code>graph</code> is a
267
     * BATIMENT which points to SITE, is pointed by LOCAL, CPI_BT and <code>referents</code> is
268
     * <code>true</code>, {@link #fetch()} could return
269
     *
270
     * <pre>
271
     * SITE[2]  BATIMENT[2]     LOCAL[2]    CPI_BT[3]
272
     *                                      CPI_BT[2]
273
     *                          LOCAL[3]
274
     *                          LOCAL[5]    CPI_BT[5]
275
     * SITE[7]  BATIMENT[3]     LOCAL[4]    CPI_BT[4]
276
     * SITE[7]  BATIMENT[4]
277
     * </pre>
278
     *
279
     * @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
280
     *        SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
281
     *        to return all sites (with all their fields) with their associated contacts.
282
     * @param referents <code>true</code> if referents to <code>graph</code> should also be fetched.
283
     */
284
    public SQLRowValuesListFetcher(SQLRowValues graph, final boolean referents) {
285
        this(graph, referents ? computePath(graph) : null);
286
    }
287
 
288
    /**
289
     * Construct a new instance.
290
     *
61 ilm 291
     * @param graph what SQLRowValues should be returned by {@link #fetch()}.
93 ilm 292
     * @param referentPath a {@link Path#isSingleField() single link} path from the primary table,
17 ilm 293
     *        <code>null</code> meaning don't fetch referent rows.
294
     */
295
    public SQLRowValuesListFetcher(SQLRowValues graph, final Path referentPath) {
61 ilm 296
        this(graph, referentPath, true);
297
    }
298
 
299
    /**
300
     * Construct a new instance.
301
     *
302
     * @param graph what SQLRowValues should be returned by {@link #fetch()}.
93 ilm 303
     * @param referentPath a {@link Path#isSingleField() single link} path from the primary table,
61 ilm 304
     *        <code>null</code> meaning don't fetch referent rows.
305
     * @param prune if <code>true</code> the graph will be pruned to only contain
306
     *        <code>referentPath</code>. If <code>false</code> the graph will be kept as is, which
307
     *        can produce undefined results if there exist more than one referent row outside of
308
     *        <code>referentPath</code>.
309
     */
310
    SQLRowValuesListFetcher(final SQLRowValues graph, final Path referentPath, final boolean prune) {
17 ilm 311
        super();
312
        this.graph = graph.deepCopy();
93 ilm 313
 
80 ilm 314
        this.descendantPath = referentPath == null ? Path.get(graph.getTable()) : referentPath;
67 ilm 315
        if (!this.descendantPath.isDirection(Direction.REFERENT))
316
            throw new IllegalArgumentException("path is not (exclusively) referent : " + this.descendantPath);
61 ilm 317
        final SQLRowValues descRow = this.graph.followPath(this.descendantPath);
318
        if (descRow == null)
319
            throw new IllegalArgumentException("path is not contained in the passed rowValues : " + referentPath + "\n" + this.graph.printTree());
17 ilm 320
        // followPath() do the following check
93 ilm 321
        assert this.descendantPath.getFirst() == this.graph.getTable() && this.descendantPath.isSingleField();
61 ilm 322
 
323
        if (prune) {
324
            this.graph.getGraph().walk(descRow, null, new ITransformer<State<Object>, Object>() {
325
                @Override
326
                public Object transformChecked(State<Object> input) {
327
                    if (input.getFrom() == null) {
328
                        input.getCurrent().clearReferents();
329
                    } else {
65 ilm 330
                        input.getCurrent().retainReferent(input.getPrevious());
61 ilm 331
                    }
332
                    return null;
333
                }
80 ilm 334
            }, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
61 ilm 335
        }
336
 
17 ilm 337
        // always need IDs
93 ilm 338
        for (final SQLRowValues curr : this.graph.getGraph().getItems()) {
17 ilm 339
            // don't overwrite existing values
340
            if (!curr.hasID())
341
                curr.setID(null);
342
        }
343
 
93 ilm 344
        this.graph.getGraph().freeze();
345
 
346
        synchronized (this) {
132 ilm 347
            this.selTransf = Collections.emptyList();
93 ilm 348
            this.selID = null;
349
            this.ordered = Collections.<Path> emptySet();
350
            this.descendantsOrdered = false;
351
            this.minGraph = null;
352
            this.includeForeignUndef = false;
353
            this.frozen = null;
354
            this.freezeRows = false;
355
            this.grafts = Collections.emptyMap();
151 ilm 356
            this.postFetchLinks = Collections.emptyMap();
93 ilm 357
        }
17 ilm 358
    }
359
 
93 ilm 360
    // be aware that the new instance will share the same selTransf, and if it doesn't directly
361
    // (with copyTransf) some state can still be shared
362
    private SQLRowValuesListFetcher(SQLRowValuesListFetcher f, final boolean copyTransf) {
363
        synchronized (f) {
364
            this.graph = f.getGraph().toImmutable();
365
            this.descendantPath = f.getReferentPath();
366
            // can't deadlock since this hasn't been published
367
            synchronized (this) {
368
                this.selTransf = copyTransf ? CopyUtils.copy(f.selTransf) : f.selTransf;
369
                this.selID = f.getSelID();
370
                this.ordered = f.getOrder();
371
                this.descendantsOrdered = f.areReferentsOrdered();
372
                this.minGraph = f.minGraph == null ? null : f.minGraph.toImmutable();
373
                this.includeForeignUndef = f.includeForeignUndef;
374
                // a new instance is always mutable
375
                this.frozen = null;
376
 
377
                this.freezeRows = f.freezeRows;
378
 
379
                // Recursively copy grafts
380
                final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(f.grafts);
381
                for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : outerMutable.entrySet()) {
382
                    final Map<Path, SQLRowValuesListFetcher> innerMutable = new HashMap<Path, SQLRowValuesListFetcher>(e.getValue());
383
                    for (final Entry<Path, SQLRowValuesListFetcher> innerEntry : innerMutable.entrySet()) {
384
                        innerEntry.setValue(new SQLRowValuesListFetcher(innerEntry.getValue(), copyTransf));
385
                    }
386
                    e.setValue(Collections.unmodifiableMap(innerMutable));
387
                }
388
                this.grafts = Collections.unmodifiableMap(outerMutable);
151 ilm 389
                this.postFetchLinks = f.postFetchLinks;
93 ilm 390
            }
391
        }
392
    }
393
 
17 ilm 394
    /**
93 ilm 395
     * Get a frozen version of this. If not already {@link #isFrozen() frozen}, copy this and its
396
     * grafts and {@link #freeze()} the copy.
397
     *
398
     * @return <code>this</code> if already frozen, otherwise a frozen copy of <code>this</code>.
399
     */
400
    public final SQLRowValuesListFetcher toUnmodifiable() {
401
        synchronized (this) {
402
            if (this.isFrozen())
403
                return this;
404
            // no need to try to deep copy since we freeze before releasing the lock
405
            return new SQLRowValuesListFetcher(this, false).freeze();
406
        }
407
    }
408
 
409
    /**
17 ilm 410
     * Make this instance immutable. Ie all setters will now throw {@link IllegalStateException}.
411
     * Furthermore the request will be computed now once and for all, so as not to be subject to
132 ilm 412
     * outside modification by {@link #getSelectTransformers()}.
17 ilm 413
     *
414
     * @return this.
415
     */
93 ilm 416
    public synchronized final SQLRowValuesListFetcher freeze() {
17 ilm 417
        if (!this.isFrozen()) {
418
            this.frozen = new SQLSelect(this.getReq());
419
            for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
420
                for (final SQLRowValuesListFetcher f : m.values())
421
                    f.freeze();
422
            }
423
        }
424
        return this;
425
    }
426
 
93 ilm 427
    public synchronized final boolean isFrozen() {
17 ilm 428
        return this.frozen != null;
429
    }
430
 
431
    private final void checkFrozen() {
432
        if (this.isFrozen())
433
            throw new IllegalStateException("this has been frozen: " + this);
434
    }
435
 
93 ilm 436
    /**
437
     * Whether the rows returned by {@link #fetch()} are {@link SQLRowValues#isFrozen()
438
     * unmodifiable}.
439
     *
440
     * @param b <code>true</code> to make all rows unmodifiable.
441
     */
442
    public synchronized final void setReturnedRowsUnmodifiable(final boolean b) {
443
        this.checkFrozen();
444
        this.freezeRows = b;
445
    }
446
 
447
    /**
448
     * Whether the rows returned by {@link #fetch()} are {@link SQLRowValues#isFrozen()
449
     * unmodifiable}.
450
     *
451
     * @return <code>true</code> if all rows are returned unmodifiable.
452
     */
453
    public synchronized boolean areReturnedRowsUnmodifiable() {
454
        return this.freezeRows;
455
    }
456
 
17 ilm 457
    public SQLRowValues getGraph() {
458
        return this.graph;
459
    }
460
 
80 ilm 461
    public final Path getReferentPath() {
462
        return this.descendantPath;
463
    }
464
 
17 ilm 465
    /**
466
     * Whether to include undefined rows (of tables other than the graph's).
467
     *
468
     * @param includeForeignUndef <code>true</code> to include undefined rows.
469
     */
93 ilm 470
    public synchronized final void setIncludeForeignUndef(boolean includeForeignUndef) {
17 ilm 471
        this.checkFrozen();
472
        this.includeForeignUndef = includeForeignUndef;
473
    }
474
 
475
    /**
476
     * Require that only rows with values for the full graph are returned. Eg if the graph is CPI ->
477
     * OBS, setting this to <code>true</code> will excludes CPI without OBS.
478
     *
479
     * @param b <code>true</code> if only full rows should be fetched.
480
     */
93 ilm 481
    public synchronized final void setFullOnly(boolean b) {
17 ilm 482
        this.checkFrozen();
483
        if (b)
61 ilm 484
            this.minGraph = this.getGraph().deepCopy();
17 ilm 485
        else
486
            this.minGraph = null;
487
    }
488
 
144 ilm 489
    // MAYBE allow to remove by changing to
490
    // addRequiredPath(Path)
491
    // removeRequiredPath(Path)
492
    // -> just a Set of Path, reduced at the start of fetch()
93 ilm 493
    public synchronized final void requirePath(final Path p) {
61 ilm 494
        this.checkFrozen();
495
        if (this.getGraph().followPath(p) == null)
67 ilm 496
            throw new IllegalArgumentException("Path not included in this graph : " + p + "\n" + this.getGraph().printGraph());
61 ilm 497
        if (this.minGraph == null)
498
            this.minGraph = new SQLRowValues(getGraph().getTable());
499
        this.minGraph.assurePath(p);
500
    }
501
 
93 ilm 502
    private synchronized final boolean isPathRequired(final Path p) {
17 ilm 503
        return this.minGraph != null && this.minGraph.followPath(p) != null;
504
    }
505
 
506
    private boolean fetchReferents() {
61 ilm 507
        return this.descendantPath.length() > 0;
17 ilm 508
    }
509
 
510
    /**
511
     * To modify the query before execution.
512
     *
513
     * @param selTransf will be passed the query which has been constructed, and the return value
514
     *        will be actually executed, can be <code>null</code>.
515
     */
93 ilm 516
    public synchronized void setSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
17 ilm 517
        this.checkFrozen();
132 ilm 518
        this.selTransf = selTransf != null ? Collections.singletonList(selTransf) : Collections.<ITransformer<SQLSelect, SQLSelect>> emptyList();
17 ilm 519
    }
520
 
132 ilm 521
    public void clearSelTransf() {
522
        this.setSelTransf(null);
523
    }
524
 
525
    public void appendSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
526
        this.addSelTransf(selTransf, -1);
527
    }
528
 
529
    public void prependSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
530
        this.addSelTransf(selTransf, 0);
531
    }
532
 
533
    public synchronized void addSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf, final int index) {
534
        this.checkFrozen();
535
        if (selTransf != null) {
536
            final List<ITransformer<SQLSelect, SQLSelect>> copy = new ArrayList<ITransformer<SQLSelect, SQLSelect>>(this.selTransf);
537
            final int size = copy.size();
538
            final int realIndex = index < 0 ? size + index + 1 : index;
539
            copy.add(realIndex, selTransf);
540
            this.selTransf = Collections.unmodifiableList(copy);
541
        }
542
    }
543
 
544
    public synchronized boolean removeSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
545
        this.checkFrozen();
546
        if (selTransf != null) {
547
            final List<ITransformer<SQLSelect, SQLSelect>> copy = new ArrayList<ITransformer<SQLSelect, SQLSelect>>(this.selTransf);
548
            if (copy.remove(selTransf)) {
549
                this.selTransf = Collections.unmodifiableList(copy);
550
                return true;
551
            }
552
        }
553
        return false;
554
    }
555
 
93 ilm 556
    public synchronized final ITransformer<SQLSelect, SQLSelect> getSelTransf() {
132 ilm 557
        if (this.selTransf.size() > 1)
558
            throw new IllegalStateException("More than one transformer");
559
        return CollectionUtils.getFirst(this.selTransf);
560
    }
561
 
562
    public synchronized final List<ITransformer<SQLSelect, SQLSelect>> getSelectTransformers() {
17 ilm 563
        return this.selTransf;
564
    }
565
 
566
    /**
567
     * Add a where in {@link #getReq()} to restrict the primary key.
568
     *
569
     * @param selID an ID for the primary key, <code>null</code> to not filter.
570
     */
93 ilm 571
    public synchronized void setSelID(Number selID) {
17 ilm 572
        this.checkFrozen();
573
        this.selID = selID;
574
    }
575
 
93 ilm 576
    public synchronized final Number getSelID() {
17 ilm 577
        return this.selID;
578
    }
579
 
580
    /**
581
     * Whether to add ORDER BY in {@link #getReq()}.
582
     *
583
     * @param b <code>true</code> if the query should be ordered.
65 ilm 584
     * @return this.
17 ilm 585
     */
93 ilm 586
    public synchronized final SQLRowValuesListFetcher setOrdered(final boolean b) {
80 ilm 587
        this.setOrder(b ? Collections.singleton(Path.get(getGraph().getTable())) : Collections.<Path> emptySet(), true);
73 ilm 588
        this.setReferentsOrdered(b, false);
589
        return this;
590
    }
591
 
592
    public final SQLRowValuesListFetcher setOrder(final List<Path> order) {
593
        return this.setOrder(order, false);
594
    }
595
 
93 ilm 596
    private synchronized final SQLRowValuesListFetcher setOrder(final Collection<Path> order, final boolean safeVal) {
17 ilm 597
        this.checkFrozen();
73 ilm 598
        for (final Path p : order)
599
            if (this.getGraph().followPath(p) == null)
600
                throw new IllegalArgumentException("Path not in this " + p);
601
        this.ordered = safeVal ? (Set<Path>) order : Collections.unmodifiableSet(new LinkedHashSet<Path>(order));
65 ilm 602
        return this;
17 ilm 603
    }
604
 
93 ilm 605
    public synchronized final Set<Path> getOrder() {
17 ilm 606
        return this.ordered;
607
    }
608
 
73 ilm 609
    /**
610
     * Whether to order referent rows in this fetcher.
611
     *
612
     * @param b <code>true</code> to order referent rows starting from the primary node, e.g. if the
613
     *        graph is
614
     *
615
     *        <pre>
616
     * *SITE* <- BATIMENT <- LOCAL
132 ilm 617
     *        </pre>
73 ilm 618
     *
619
     *        then this will cause ORDER BY BATIMENT.ORDRE, LOCAL.ORDRE.
620
     * @param rec if grafts should also be changed.
621
     * @return this.
622
     */
93 ilm 623
    public synchronized final SQLRowValuesListFetcher setReferentsOrdered(final boolean b, final boolean rec) {
73 ilm 624
        this.descendantsOrdered = b;
625
        if (rec) {
626
            for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
627
                for (final SQLRowValuesListFetcher f : m.values())
628
                    f.setReferentsOrdered(b, rec);
629
            }
630
        }
631
        return this;
632
    }
633
 
93 ilm 634
    public synchronized final boolean areReferentsOrdered() {
73 ilm 635
        return this.descendantsOrdered;
636
    }
637
 
151 ilm 638
    public final void addPostFetchLink(final Path toAdd, final Path existingDestination) {
639
        this.addPostFetchLink(toAdd, existingDestination, false);
640
    }
641
 
642
    /**
643
     * Add a link to be added at the end of fetch(). This is needed when the graph to be fetched
644
     * isn't a tree.
645
     *
646
     * @param toAdd the last step of this parameter will be added at the end of {@link #fetch()},
647
     *        e.g. /SOURCE/ --[ID_MOST_SERIOUS_OBS]--> /OBSERVATION/.
648
     * @param existingDestination where the destination rows of <code>toAdd</code> are, e.g.
649
     *        /SOURCE/ <--[ID_SOURCE]-- /SOURCE_OBSERVATION/ --[ID_OBSERVATION]--> /OBSERVATION/.
650
     * @param ignoreIfMissing what to do if a passed path isn't in this, <code>true</code> to do
651
     *        nothing, <code>false</code> to throw an exception.
652
     * @return <code>true</code> if the link was added.
653
     */
654
    public synchronized final boolean addPostFetchLink(final Path toAdd, final Path existingDestination, final boolean ignoreIfMissing) {
655
        checkFrozen();
656
        if (toAdd.getLast() != existingDestination.getLast())
657
            throw new IllegalArgumentException("Different destination tables");
658
        if (!toAdd.isSingleField())
659
            throw new IllegalArgumentException("Path to add isn't composed of single fields");
660
        final Step lastStep = toAdd.getStep(-1);
661
        if (lastStep.getDirection() != Direction.FOREIGN)
662
            throw new IllegalArgumentException("Last step isn't foreign : " + lastStep);
663
        if (!getFetchers(toAdd).isEmpty())
664
            throw new IllegalArgumentException("Path to add already fetched");
665
        final Path pathToFK = toAdd.minusLast();
666
        final ListMap<Path, SQLRowValuesListFetcher> fkFetchers = getFetchers(pathToFK);
667
        if (fkFetchers.isEmpty()) {
668
            if (ignoreIfMissing)
669
                return false;
670
            else
671
                throw new IllegalArgumentException("Path to add should only have the last step missing");
672
        }
673
        final String lastFieldName = lastStep.getSingleField().getName();
674
        for (final Entry<Path, List<SQLRowValuesListFetcher>> e : fkFetchers.entrySet()) {
675
            final int pathLength = e.getKey().length();
676
            for (final SQLRowValuesListFetcher fkFetcher : e.getValue()) {
677
                if (!fkFetcher.getGraph().followPath(pathToFK.subPath(pathLength)).contains(lastFieldName)) {
678
                    if (ignoreIfMissing)
679
                        return false;
680
                    else
681
                        throw new IllegalArgumentException("Foreign key " + lastFieldName + " isn't fetched");
682
                }
683
            }
684
        }
685
        if (getFetchers(existingDestination).isEmpty()) {
686
            if (ignoreIfMissing)
687
                return false;
688
            else
689
                throw new IllegalArgumentException("Destination won't be fetched : " + existingDestination);
690
        }
691
        final Map<Path, Path> copy = new HashMap<>(this.postFetchLinks);
692
        copy.put(toAdd, existingDestination);
693
        this.postFetchLinks = Collections.unmodifiableMap(copy);
694
        return true;
695
    }
696
 
697
    public synchronized Map<Path, Path> getPostFetchLinks() {
698
        return this.postFetchLinks;
699
    }
700
 
17 ilm 701
    public final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other) {
80 ilm 702
        return this.graft(other, Path.get(getGraph().getTable()));
17 ilm 703
    }
704
 
705
    public final SQLRowValuesListFetcher graft(final SQLRowValues other, Path graftPath) {
706
        // with referents otherwise it's useless
707
        return this.graft(new SQLRowValuesListFetcher(other, true), graftPath);
708
    }
709
 
710
    /**
711
     * Graft a fetcher on this graph.
712
     *
713
     * @param other another instance fetching rows of the table at <code>graftPath</code>.
714
     * @param graftPath a path from this values to where <code>other</code> rows should be grafted.
715
     * @return the previous fetcher.
716
     */
93 ilm 717
    public synchronized final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other, Path graftPath) {
17 ilm 718
        checkFrozen();
719
        if (this == other)
720
            throw new IllegalArgumentException("trying to graft onto itself");
721
        if (other.getGraph().getTable() != graftPath.getLast())
722
            throw new IllegalArgumentException("trying to graft " + other.getGraph().getTable() + " at " + graftPath);
723
        final SQLRowValues graftPlace = this.getGraph().followPath(graftPath);
724
        if (graftPlace == null)
725
            throw new IllegalArgumentException("path doesn't exist: " + graftPath);
726
        assert graftPath.getLast() == graftPlace.getTable();
83 ilm 727
        if (other.getGraph().hasForeigns())
17 ilm 728
            throw new IllegalArgumentException("shouldn't have foreign rows");
729
 
144 ilm 730
        final Path descendantPath = other.getReferentPath();
61 ilm 731
        final int descendantPathLength = descendantPath.length();
732
        if (descendantPathLength == 0)
17 ilm 733
            throw new IllegalArgumentException("empty path");
734
        // checked by computePath
93 ilm 735
        assert descendantPath.isSingleField();
65 ilm 736
        // we used to disallow that :
737
        // this is LOCAL* -> BATIMENT -> SITE and CPI -> LOCAL -> BATIMENT* is being grafted
738
        // but this is sometimes desirable, e.g. for each LOCAL find all of its siblings with the
739
        // same capacity (or any other predicate)
740
 
93 ilm 741
        // shallow copy : all values are still immutable
742
        final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(this.grafts);
743
        final Map<Path, SQLRowValuesListFetcher> innerMutable;
17 ilm 744
        if (!this.grafts.containsKey(graftPath)) {
80 ilm 745
            // allow getFetchers() to use a list, easing tests and avoiding using equals()
93 ilm 746
            innerMutable = new LinkedHashMap<Path, SQLRowValuesListFetcher>(4);
61 ilm 747
        } else {
748
            final Map<Path, SQLRowValuesListFetcher> map = this.grafts.get(graftPath);
93 ilm 749
            innerMutable = new LinkedHashMap<Path, SQLRowValuesListFetcher>(map);
61 ilm 750
            // e.g. fetching *BATIMENT* <- LOCAL and *BATIMENT* <- LOCAL <- CPI (with different
751
            // WHERE) and LOCAL have different fields. This isn't supported since we would have to
752
            // merge fields in merge() and it would be quite long
753
            for (Entry<Path, SQLRowValuesListFetcher> e : map.entrySet()) {
754
                final Path fetcherPath = e.getKey();
755
                final SQLRowValuesListFetcher fetcher = e.getValue();
756
                for (int i = 1; i <= descendantPathLength; i++) {
757
                    final Path subPath = descendantPath.subPath(0, i);
758
                    if (fetcherPath.startsWith(subPath)) {
759
                        if (!fetcher.getGraph().followPath(subPath).getFields().equals(other.getGraph().followPath(subPath).getFields()))
760
                            throw new IllegalArgumentException("The same node have different fields in different fetcher\n" + graftPath + "\n" + subPath);
761
                    } else {
762
                        break;
763
                    }
764
                }
17 ilm 765
            }
766
        }
93 ilm 767
        final SQLRowValuesListFetcher res = innerMutable.put(descendantPath, other);
768
        outerMutable.put(graftPath, Collections.unmodifiableMap(innerMutable));
769
        this.grafts = Collections.unmodifiableMap(outerMutable);
770
        return res;
17 ilm 771
    }
772
 
61 ilm 773
    public final Collection<SQLRowValuesListFetcher> ungraft() {
80 ilm 774
        return this.ungraft(Path.get(getGraph().getTable()));
17 ilm 775
    }
776
 
93 ilm 777
    public synchronized final Collection<SQLRowValuesListFetcher> ungraft(final Path graftPath) {
17 ilm 778
        checkFrozen();
93 ilm 779
        if (!this.grafts.containsKey(graftPath))
780
            return null;
781
        final Map<Path, Map<Path, SQLRowValuesListFetcher>> outerMutable = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(this.grafts);
782
        final Map<Path, SQLRowValuesListFetcher> res = outerMutable.remove(graftPath);
783
        this.grafts = Collections.unmodifiableMap(outerMutable);
61 ilm 784
        return res == null ? null : res.values();
17 ilm 785
    }
786
 
93 ilm 787
    private synchronized final Map<Path, Map<Path, SQLRowValuesListFetcher>> getGrafts() {
788
        return this.grafts;
789
    }
790
 
67 ilm 791
    /**
792
     * The fetchers grafted at the passed path.
793
     *
794
     * @param graftPath where the fetchers are grafted, e.g. MISSION, DOSSIER, SITE.
795
     * @return the grafts by their path to fetch, e.g. SITE, BATIMENT, LOCAL, CPI_BT.
796
     */
797
    public final Map<Path, SQLRowValuesListFetcher> getGrafts(final Path graftPath) {
93 ilm 798
        return this.getGrafts().get(graftPath);
67 ilm 799
    }
800
 
80 ilm 801
    /**
83 ilm 802
     * Get all fetchers.
803
     *
804
     * @param includeSelf <code>true</code> to include <code>this</code> (with a <code>null</code>
805
     *        key).
806
     * @return all instances indexed by the graft path.
807
     */
808
    public final ListMapItf<Path, SQLRowValuesListFetcher> getFetchers(final boolean includeSelf) {
809
        final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
93 ilm 810
        for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.getGrafts().entrySet()) {
83 ilm 811
            assert e.getKey() != null;
812
            res.putCollection(e.getKey(), e.getValue().values());
813
        }
814
        if (includeSelf)
815
            res.add(null, this);
816
        return ListMap.unmodifiableMap(res);
817
    }
818
 
819
    /**
80 ilm 820
     * Get instances which fetch the {@link Path#getLast() last table} of the passed path. E.g.
821
     * useful if you want to add a where to a join. This method is recursively called on
822
     * {@link #getGrafts(Path) grafts} thus the returned paths may be fetched by grafts.
823
     *
824
     * @param fetchedPath a path starting by this table.
825
     * @return all instances indexed by the graft path, i.e. <code>fetchedPath</code> is between
826
     *         with it and (it+fetchers.{@link #getReferentPath()}).
827
     */
828
    public final ListMap<Path, SQLRowValuesListFetcher> getFetchers(final Path fetchedPath) {
829
        final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
830
        if (this.getGraph().followPath(fetchedPath) != null)
831
            res.add(Path.get(getGraph().getTable()), this);
832
        // search grafts
93 ilm 833
        for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.getGrafts().entrySet()) {
80 ilm 834
            final Path graftPlace = e.getKey();
835
            if (fetchedPath.startsWith(graftPlace) && fetchedPath.length() > graftPlace.length()) {
836
                final Path rest = fetchedPath.subPath(graftPlace.length());
837
                // we want requests that use the last step of fetchedPath
838
                assert rest.length() > 0;
839
                for (final Entry<Path, SQLRowValuesListFetcher> e2 : e.getValue().entrySet()) {
840
                    final Path refPath = e2.getKey();
841
                    final SQLRowValuesListFetcher graft = e2.getValue();
842
                    if (refPath.startsWith(rest)) {
843
                        res.add(graftPlace, graft);
844
                    } else if (rest.startsWith(refPath)) {
845
                        // otherwise rest == refPath and the above if would have been executed
846
                        assert rest.length() > refPath.length();
847
                        for (final Entry<Path, List<SQLRowValuesListFetcher>> e3 : graft.getFetchers(rest).entrySet()) {
848
                            res.addAll(graftPlace.append(e3.getKey()), e3.getValue());
849
                        }
850
                    }
851
                }
852
            }
853
        }
854
        return res;
855
    }
856
 
17 ilm 857
    private final void addFields(final SQLSelect sel, final SQLRowValues vals, final String alias) {
83 ilm 858
        // put key first
859
        final SQLField key = vals.getTable().getKey();
860
        sel.addSelect(new AliasedField(key, alias));
861
        for (final String fieldName : vals.getFields()) {
862
            if (!fieldName.equals(key.getName()))
863
                sel.addSelect(new AliasedField(vals.getTable().getField(fieldName), alias));
864
        }
17 ilm 865
    }
866
 
867
    public final SQLSelect getReq() {
142 ilm 868
        return this.getReq(null, null);
93 ilm 869
    }
17 ilm 870
 
142 ilm 871
    static private final SQLSelect checkTr(final List<String> origSelect, final SQLSelect tr) {
872
        if (!origSelect.equals(tr.getSelect()))
873
            throw new IllegalArgumentException("Select clause cannot be modified");
874
        return tr;
875
    }
876
 
877
    public synchronized final SQLSelect getReq(final Where w, final ITransformer<SQLSelect, SQLSelect> selTransf) {
878
        checkTable(w);
879
        final boolean isNopTransf = selTransf == null || selTransf == Transformer.<SQLSelect> nopTransformer();
93 ilm 880
        if (this.isFrozen()) {
142 ilm 881
            if (w == null && isNopTransf) {
93 ilm 882
                return this.frozen;
883
            } else {
142 ilm 884
                final SQLSelect copy = new SQLSelect(this.frozen);
885
                final SQLSelect res = isNopTransf ? copy : checkTr(copy.getSelect(), selTransf.transformChecked(copy));
886
                return res.andWhere(w);
93 ilm 887
            }
888
        }
889
 
17 ilm 890
        final SQLTable t = this.getGraph().getTable();
73 ilm 891
        final SQLSelect sel = new SQLSelect();
17 ilm 892
 
893
        if (this.includeForeignUndef) {
894
            sel.setExcludeUndefined(false);
895
            sel.setExcludeUndefined(true, t);
896
        }
897
 
93 ilm 898
        walk(null, new ITransformer<State<String>, String>() {
17 ilm 899
            @Override
93 ilm 900
            public String transformChecked(State<String> input) {
17 ilm 901
                final String alias;
902
                if (input.getFrom() != null) {
93 ilm 903
                    alias = getAlias(sel, input.getPath());
904
                    final String aliasPrev = input.getAcc();
144 ilm 905
                    // MAYBE use "INNER" for first step of a referent graft since the first node is
906
                    // ignored (the node from the parent graph is used)
907
                    // SITE <-- BATIMENT and graft is BATIMENT <-- LOCAL, empty BATIMENT are just
908
                    // discarded so best not to fetch them for nothing
61 ilm 909
                    final String joinType = isPathRequired(input.getPath()) ? "INNER" : "LEFT";
93 ilm 910
                    sel.addJoin(joinType, aliasPrev, input.getPath().getStep(-1), alias);
83 ilm 911
                } else {
17 ilm 912
                    alias = null;
83 ilm 913
                }
93 ilm 914
                addFields(sel, input.getCurrent(), alias);
17 ilm 915
 
93 ilm 916
                return alias;
17 ilm 917
            }
918
 
919
        });
73 ilm 920
        for (final Path p : this.getOrder())
921
            sel.addOrder(sel.followPath(t.getName(), p), false);
922
        // after getOrder() since it can specify more precise order
923
        if (this.areReferentsOrdered()) {
924
            final int descSize = this.descendantPath.length();
925
            for (int i = 1; i <= descSize; i++) {
926
                sel.addOrder(sel.followPath(t.getName(), this.descendantPath.subPath(0, i)), false);
927
            }
928
        }
929
 
93 ilm 930
        if (this.getSelID() != null)
144 ilm 931
            sel.andWhere(getIDWhere(this.getSelID()));
142 ilm 932
        final List<String> origSel = new ArrayList<String>(sel.getSelect());
132 ilm 933
        SQLSelect res = sel;
934
        for (final ITransformer<SQLSelect, SQLSelect> tr : this.getSelectTransformers()) {
935
            res = tr.transformChecked(res);
936
        }
142 ilm 937
        if (!isNopTransf)
938
            res = selTransf.transformChecked(res);
939
        return checkTr(origSel, res).andWhere(w);
17 ilm 940
    }
941
 
144 ilm 942
    public final Where getIDWhere(final Number id) {
132 ilm 943
        if (id == null)
944
            return null;
144 ilm 945
        return new Where(getGraph().getTable().getKey(), "=", id);
132 ilm 946
    }
947
 
17 ilm 948
    static String getAlias(final SQLSelect sel, final Path path) {
949
        String res = "tAlias";
950
        final int stop = path.length();
951
        for (int i = 0; i < stop; i++) {
93 ilm 952
            res += "__" + path.getSingleField(i).getName();
17 ilm 953
        }
954
        // needed for backward, otherwise tableAlias__ID_BATIMENT for LOCAL
955
        res += "__" + path.getTable(stop).getName();
956
        return sel.getUniqueAlias(res);
957
    }
958
 
959
    // assure that the graph is explored the same way for the construction of the request
960
    // and the reading of the resultSet
961
    private <S> void walk(final S sel, final ITransformer<State<S>, S> transf) {
962
        // walk through foreign keys and never walk back (use graft())
80 ilm 963
        this.getGraph().getGraph().walk(this.getGraph(), sel, transf, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
17 ilm 964
        // walk starting backwards but allowing forwards
965
        this.getGraph().getGraph().walk(this.getGraph(), sel, new ITransformer<State<S>, S>() {
966
            @Override
967
            public S transformChecked(State<S> input) {
968
                final Path p = input.getPath();
969
                if (p.getStep(0).isForeign())
970
                    throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
971
                final Step lastStep = p.getStep(p.length() - 1);
972
                // if we go backwards it should be from the start (i.e. we can't go backwards, then
973
                // forwards and backwards again)
67 ilm 974
                if (!lastStep.isForeign() && p.getDirection() != Direction.REFERENT)
17 ilm 975
                    throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
976
                return transf.transformChecked(input);
977
            }
83 ilm 978
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
17 ilm 979
    }
980
 
981
    // models the graph, so that we don't have to walk it for each row
982
    private static final class GraphNode {
983
        private final SQLTable t;
984
        private final int fieldCount;
83 ilm 985
        private final int foreignCount;
17 ilm 986
        private final int linkIndex;
987
        private final Step from;
988
 
989
        private GraphNode(final State<Integer> input) {
990
            super();
991
            this.t = input.getCurrent().getTable();
992
            this.fieldCount = input.getCurrent().size();
83 ilm 993
            this.foreignCount = input.getCurrent().getForeigns().size();
17 ilm 994
            this.linkIndex = input.getAcc();
995
            final int length = input.getPath().length();
996
            this.from = length == 0 ? null : input.getPath().getStep(length - 1);
997
        }
998
 
999
        public final SQLTable getTable() {
1000
            return this.t;
1001
        }
1002
 
1003
        public final int getFieldCount() {
1004
            return this.fieldCount;
1005
        }
1006
 
83 ilm 1007
        public final int getForeignCount() {
1008
            return this.foreignCount;
1009
        }
1010
 
17 ilm 1011
        public final int getLinkIndex() {
1012
            return this.linkIndex;
1013
        }
1014
 
1015
        public final String getFromName() {
1016
            return this.from.getSingleField().getName();
1017
        }
1018
 
1019
        public final boolean isBackwards() {
73 ilm 1020
            return !this.from.isForeign();
17 ilm 1021
        }
1022
 
1023
        @Override
25 ilm 1024
        public int hashCode() {
1025
            final int prime = 31;
1026
            int result = 1;
1027
            result = prime * result + this.fieldCount;
1028
            result = prime * result + ((this.from == null) ? 0 : this.from.hashCode());
1029
            result = prime * result + this.linkIndex;
1030
            result = prime * result + this.t.hashCode();
1031
            return result;
1032
        }
1033
 
1034
        @Override
1035
        public boolean equals(Object obj) {
1036
            if (this == obj)
1037
                return true;
1038
            if (obj == null)
1039
                return false;
1040
            if (getClass() != obj.getClass())
1041
                return false;
1042
            final GraphNode other = (GraphNode) obj;
1043
            return this.fieldCount == other.fieldCount && this.linkIndex == other.linkIndex && this.t.equals(other.t) && CompareUtils.equals(this.from, other.from);
1044
        }
1045
 
1046
        @Override
17 ilm 1047
        public String toString() {
1048
            final String link = this.from == null ? "" : " linked to " + getLinkIndex() + " by " + this.getFromName() + (this.isBackwards() ? " backwards" : " forewards");
1049
            return this.getFieldCount() + " fields of " + this.getTable() + link;
1050
        }
1051
    }
1052
 
25 ilm 1053
    static private final class RSH implements ResultSetHandler {
1054
        private final List<String> selectFields;
1055
        private final List<GraphNode> graphNodes;
93 ilm 1056
        private final boolean freezeRows;
25 ilm 1057
 
93 ilm 1058
        private RSH(List<String> selectFields, List<GraphNode> l, boolean freezeRows) {
25 ilm 1059
            this.selectFields = selectFields;
1060
            this.graphNodes = l;
93 ilm 1061
            this.freezeRows = freezeRows;
25 ilm 1062
        }
1063
 
1064
        @Override
1065
        public Object handle(final ResultSet rs) throws SQLException {
1066
            final List<GraphNode> l = this.graphNodes;
1067
            final int graphSize = l.size();
1068
            int nextToLink = 0;
1069
            final List<Future<?>> futures = new ArrayList<Future<?>>();
1070
 
1071
            final List<SQLRowValues> res = new ArrayList<SQLRowValues>(64);
1072
            final List<List<SQLRowValues>> rows = Collections.synchronizedList(new ArrayList<List<SQLRowValues>>(64));
1073
            // for each rs row, create all SQLRowValues without linking them together
1074
            // if we're multi-threaded, link them in another thread
1075
            while (rs.next()) {
1076
                int rsIndex = 1;
1077
 
1078
                // MAYBE cancel() futures
1079
                if (Thread.currentThread().isInterrupted())
1080
                    throw new RTInterruptedException("interrupted while fetching");
1081
                final List<SQLRowValues> row = new ArrayList<SQLRowValues>(graphSize);
1082
                for (int i = 0; i < graphSize; i++) {
1083
                    final GraphNode node = l.get(i);
83 ilm 1084
                    final int stop = rsIndex + node.getFieldCount();
1085
                    final SQLRowValues creatingVals;
1086
                    // the PK is always first and it can only be null if there was no row, i.e. all
1087
                    // other fields will be null.
1088
                    final Object first = rs.getObject(rsIndex);
1089
                    if (first == null) {
1090
                        creatingVals = null;
1091
                        // don't bother reading all nulls
1092
                        rsIndex = stop;
1093
                    } else {
1094
                        // don't pass referent count as it can be fetched by a graft, or else
1095
                        // several rows might later be merged (e.g. *BATIMENT* <- LOCAL has only one
1096
                        // referent but all locals of a batiment will point to the same row)
1097
                        creatingVals = new SQLRowValues(node.getTable(), node.getFieldCount(), node.getForeignCount(), -1);
1098
                        put(creatingVals, rsIndex, first);
1099
                        rsIndex++;
1100
                    }
1101
                    if (i == 0) {
1102
                        if (creatingVals == null)
1103
                            throw new IllegalStateException("Null primary row");
25 ilm 1104
                        res.add(creatingVals);
83 ilm 1105
                    }
25 ilm 1106
 
1107
                    for (; rsIndex < stop; rsIndex++) {
1108
                        try {
83 ilm 1109
                            put(creatingVals, rsIndex, rs.getObject(rsIndex));
25 ilm 1110
                        } catch (SQLException e) {
1111
                            throw new IllegalStateException("unable to fill " + creatingVals, e);
1112
                        }
1113
                    }
1114
                    row.add(creatingVals);
1115
                }
1116
                rows.add(row);
1117
                // become multi-threaded only for large values
1118
                final int currentCount = rows.size();
1119
                if (currentCount % 1000 == 0) {
93 ilm 1120
                    futures.add(exec.submit(new Linker(l, rows, nextToLink, currentCount, this.freezeRows)));
25 ilm 1121
                    nextToLink = currentCount;
1122
                }
1123
            }
1124
            final int rowSize = rows.size();
1125
            assert nextToLink > 0 == futures.size() > 0;
1126
            if (nextToLink > 0)
93 ilm 1127
                futures.add(exec.submit(new Linker(l, rows, nextToLink, rowSize, this.freezeRows)));
25 ilm 1128
 
1129
            // either link all rows, or...
1130
            if (nextToLink == 0)
93 ilm 1131
                link(l, rows, 0, rowSize, this.freezeRows);
25 ilm 1132
            else {
1133
                // ...wait for every one and most importantly check for any exceptions
1134
                try {
1135
                    for (final Future<?> f : futures)
1136
                        f.get();
1137
                } catch (Exception e) {
1138
                    throw new IllegalStateException("couldn't link", e);
1139
                }
1140
            }
1141
 
1142
            return res;
1143
        }
1144
 
83 ilm 1145
        protected void put(final SQLRowValues creatingVals, int rsIndex, final Object obj) {
1146
            // -1 since rs starts at 1
1147
            // field names checked only once when nodes are created
1148
            creatingVals.put(this.selectFields.get(rsIndex - 1), obj, false);
1149
        }
1150
 
25 ilm 1151
        @Override
1152
        public int hashCode() {
1153
            final int prime = 31;
1154
            int result = 1;
1155
            result = prime * result + this.graphNodes.hashCode();
1156
            result = prime * result + this.selectFields.hashCode();
1157
            return result;
1158
        }
1159
 
1160
        @Override
1161
        public boolean equals(Object obj) {
1162
            if (this == obj)
1163
                return true;
1164
            if (obj == null)
1165
                return false;
1166
            if (getClass() != obj.getClass())
1167
                return false;
1168
            final RSH other = (RSH) obj;
1169
            return this.graphNodes.equals(other.graphNodes) && this.selectFields.equals(other.selectFields);
1170
        }
1171
 
1172
    }
1173
 
17 ilm 1174
    /**
1175
     * Execute the request transformed by <code>selTransf</code> and return the result as a list of
25 ilm 1176
     * SQLRowValues. NOTE: this method doesn't use the cache of SQLDataSource.
17 ilm 1177
     *
1178
     * @return a list of SQLRowValues, one item per row, each item having the same structure as the
1179
     *         SQLRowValues passed to the constructor.
1180
     */
1181
    public final List<SQLRowValues> fetch() {
93 ilm 1182
        return this.fetch(null);
17 ilm 1183
    }
1184
 
93 ilm 1185
    private void checkTable(final Where w) throws IllegalArgumentException {
1186
        if (w == null)
1187
            return;
1188
        final SQLTable t = this.getGraph().getTable();
1189
        for (final FieldRef f : w.getFields()) {
1190
            if (!f.getTableRef().equals(t))
1191
                throw new IllegalArgumentException("Not all from the primary table " + t + " : " + w);
1192
        }
1193
    }
1194
 
132 ilm 1195
    public final SQLRowValues fetchOne(final Number id) {
1196
        return this.fetchOne(id, null);
1197
    }
1198
 
1199
    public final SQLRowValues fetchOne(final Number id, final Boolean unmodifiableRows) {
1200
        if (id == null)
1201
            throw new NullPointerException("Null ID");
1202
        if (this.getSelID() != null)
1203
            throw new IllegalStateException("ID already set to " + getSelID());
144 ilm 1204
        final List<SQLRowValues> res = this.fetch(getIDWhere(id), unmodifiableRows);
132 ilm 1205
        if (res.size() > 1)
1206
            throw new IllegalStateException("More than one row for ID " + id + " : " + res);
1207
        return CollectionUtils.getFirst(res);
1208
    }
1209
 
93 ilm 1210
    /**
1211
     * Execute the request transformed by <code>selTransf</code> and with the passed where (even if
1212
     * {@link #isFrozen()}) and return the result as a list of SQLRowValues. NOTE: this method
1213
     * doesn't use the cache of SQLDataSource.
1214
     *
1215
     * @param w a where to {@link SQLSelect#andWhere(Where) restrict} the result, can only uses the
1216
     *        primary table (others have unspecified aliases), can be <code>null</code>.
1217
     * @return a list of SQLRowValues, one item per row, each item having the same structure as the
1218
     *         SQLRowValues passed to the constructor.
1219
     * @throws IllegalArgumentException if the passed where doesn't refer exclusively to the primary
1220
     *         table.
1221
     */
1222
    public final List<SQLRowValues> fetch(final Where w) throws IllegalArgumentException {
1223
        return this.fetch(w, null);
1224
    }
1225
 
1226
    /**
1227
     * Execute the request transformed by <code>selTransf</code> and with the passed where (even if
1228
     * {@link #isFrozen()}) and return the result as a list of SQLRowValues. NOTE: this method
1229
     * doesn't use the cache of SQLDataSource.
1230
     *
1231
     * @param w a where to {@link SQLSelect#andWhere(Where) restrict} the result, can only uses the
1232
     *        primary table (others have unspecified aliases), can be <code>null</code>.
1233
     * @param unmodifiableRows whether to return unmodifiable rows, <code>null</code> to use
1234
     *        {@link #areReturnedRowsUnmodifiable() the default}.
1235
     * @return a list of SQLRowValues, one item per row, each item having the same structure as the
1236
     *         SQLRowValues passed to the constructor.
1237
     * @throws IllegalArgumentException if the passed where doesn't refer exclusively to the primary
1238
     *         table.
1239
     */
1240
    public final List<SQLRowValues> fetch(final Where w, final Boolean unmodifiableRows) throws IllegalArgumentException {
142 ilm 1241
        return this.fetch(w, null, unmodifiableRows);
93 ilm 1242
    }
1243
 
142 ilm 1244
    public final List<SQLRowValues> fetch(final Where w, final ITransformer<SQLSelect, SQLSelect> selTransf, final Boolean unmodifiableRows) throws IllegalArgumentException {
144 ilm 1245
        return this.fetch(null, w, selTransf, unmodifiableRows);
142 ilm 1246
    }
1247
 
144 ilm 1248
    // same object passed to all recursive calls
1249
    static private final class MainResult {
1250
        private final Deque<GraftState> graftStates = new LinkedList<>();
1251
 
1252
        private MainResult() {
1253
            super();
1254
        }
1255
 
1256
        private GraftState getLastGraftState() {
1257
            return this.graftStates.peekLast();
1258
        }
1259
 
1260
        private void push(final List<SQLRowValues> merged, final Path graftPlace) {
1261
            final GraftState graftState = this.getLastGraftState();
1262
            final Path recPath = graftState == null ? null : graftState.pathFromMain;
1263
            final GraftState recGraftState = new GraftState(merged, recPath, graftPlace);
1264
            this.graftStates.addLast(recGraftState);
1265
        }
1266
 
1267
        private void pop() {
1268
            this.graftStates.removeLast();
1269
        }
1270
    }
1271
 
1272
    static private final class GraftState {
1273
 
1274
        private final Path pathFromMain;
1275
 
1276
        // list of BATIMENT to only fetch what's necessary
1277
        private final Set<Number> ids = new HashSet<Number>();
1278
        // CollectionMap since the same row can be in multiple index of merged, e.g. when
1279
        // fetching *BATIMENT* -> SITE each site will be repeated as many times as it has
1280
        // children and if we want their DOSSIER they must be grafted on each line.
1281
        private final ListMap<Tuple2<Path, Number>, SQLRowValues> byRows = createCollectionMap();
1282
 
1283
        private GraftState(final List<SQLRowValues> parentRows, final Path pathFromMain, final Path graftPlace) {
1284
            this.pathFromMain = pathFromMain == null ? graftPlace : pathFromMain.append(graftPlace);
1285
            final Path mapPath = Path.get(graftPlace.getLast());
1286
            for (final SQLRowValues vals : parentRows) {
1287
                // can be empty when grafting on optional row
1288
                for (final SQLRowValues graftPlaceVals : vals.followPath(graftPlace, CreateMode.CREATE_NONE, false)) {
1289
                    this.ids.add(graftPlaceVals.getIDNumber());
1290
                    this.byRows.add(Tuple2.create(mapPath, graftPlaceVals.getIDNumber()), graftPlaceVals);
1291
                }
1292
            }
1293
            assert this.ids.size() == this.byRows.size();
1294
        }
1295
 
1296
        private Where createWhere() {
1297
            return new Where(this.pathFromMain.getLast().getKey(), this.ids);
1298
        }
1299
    }
1300
 
1301
    private final List<SQLRowValues> fetch(MainResult mainRes, Where w, final ITransformer<SQLSelect, SQLSelect> selTransf, final Boolean unmodifiableRows) throws IllegalArgumentException {
1302
        final GraftState graftState = mainRes == null ? null : mainRes.getLastGraftState();
1303
        if (graftState != null) {
1304
            final Where graftWhere = graftState.createWhere();
1305
            if (graftWhere.equals(Where.FALSE))
1306
                return Collections.emptyList();
1307
            w = Where.and(w, graftWhere);
1308
        }
93 ilm 1309
        final SQLSelect req;
1310
        final Map<Path, Map<Path, SQLRowValuesListFetcher>> grafts;
151 ilm 1311
        final Map<Path, Path> postLinks;
93 ilm 1312
        final boolean freezeRows;
1313
        // the only other internal state used is this.descendantPath which is final immutable
1314
        synchronized (this) {
142 ilm 1315
            req = this.getReq(w, selTransf);
93 ilm 1316
            grafts = this.getGrafts();
151 ilm 1317
            postLinks = this.postFetchLinks;
93 ilm 1318
            freezeRows = unmodifiableRows == null ? this.areReturnedRowsUnmodifiable() : unmodifiableRows.booleanValue();
1319
        }
17 ilm 1320
        // getName() would take 5% of ResultSetHandler.handle()
83 ilm 1321
        final List<FieldRef> selectFields = req.getSelectFields();
1322
        final int selectFieldsSize = selectFields.size();
142 ilm 1323
        final List<String> selectFieldsNames = req.getSelectNames();
17 ilm 1324
        final SQLTable table = getGraph().getTable();
1325
 
1326
        // create a flat list of the graph nodes, we just need the table, field count and the index
1327
        // in this list of its linked table, eg for CPI -> LOCAL -> BATIMENT -> SITE :
1328
        // <LOCAL,2,0>, <BATIMENT,2,0>, <SITE,5,1>, <CPI,4,0>
1329
        final int graphSize = this.getGraph().getGraph().size();
1330
        final List<GraphNode> l = new ArrayList<GraphNode>(graphSize);
83 ilm 1331
        // check field names only once since each row has the same fields
1332
        final AtomicInteger fieldIndex = new AtomicInteger(0);
17 ilm 1333
        walk(0, new ITransformer<State<Integer>, Integer>() {
1334
            @Override
1335
            public Integer transformChecked(State<Integer> input) {
1336
                final int index = l.size();
83 ilm 1337
                final GraphNode node = new GraphNode(input);
1338
                final int stop = fieldIndex.get() + node.getFieldCount();
1339
                for (int i = fieldIndex.get(); i < stop; i++) {
1340
                    if (i >= selectFieldsSize)
1341
                        throw new IllegalStateException("Fields were removed from the select");
1342
                    final FieldRef field = selectFields.get(i);
1343
                    if (!node.getTable().equals(field.getTableRef().getTable()))
1344
                        throw new IllegalStateException("Select field not in " + node + " : " + field);
1345
                }
1346
                fieldIndex.set(stop);
1347
                l.add(node);
1348
                // used by link index of GraphNode
17 ilm 1349
                return index;
1350
            }
1351
        });
83 ilm 1352
        // otherwise walk() would already have thrown an exception
1353
        assert fieldIndex.get() <= selectFieldsSize;
1354
        if (fieldIndex.get() != selectFieldsSize) {
142 ilm 1355
            throw new IllegalStateException("Items have been added to the select (which is useless, since only fields specified by rows are returned and WHERE cannot access SELECT columns) : "
1356
                    + selectFields.subList(fieldIndex.get(), selectFieldsSize));
83 ilm 1357
        }
61 ilm 1358
        assert l.size() == graphSize : "All nodes weren't explored once : " + l.size() + " != " + graphSize + "\n" + this.getGraph().printGraph();
17 ilm 1359
 
144 ilm 1360
        final boolean mergeReferents = this.fetchReferents();
93 ilm 1361
        final boolean mergeGrafts = grafts.size() > 0;
151 ilm 1362
        final boolean addPostLinks = !postLinks.isEmpty();
93 ilm 1363
        // if it is possible let the handler do the freeze, avoid another loop and further is
1364
        // multi-threaded
151 ilm 1365
        final boolean handlerCanFreeze = !mergeReferents && !mergeGrafts && !addPostLinks;
93 ilm 1366
 
25 ilm 1367
        // if we wanted to use the cache, we'd need to copy the returned list and its items (i.e.
1368
        // deepCopy()), since we modify them afterwards. Or perhaps include the code after this line
1369
        // into the result set handler.
93 ilm 1370
        final IResultSetHandler rsh = new IResultSetHandler(new RSH(selectFieldsNames, l, freezeRows && handlerCanFreeze), false);
21 ilm 1371
        @SuppressWarnings("unchecked")
25 ilm 1372
        final List<SQLRowValues> res = (List<SQLRowValues>) table.getBase().getDataSource().execute(req.asString(), rsh, false);
17 ilm 1373
        // e.g. list of batiment pointing to site
144 ilm 1374
        final List<SQLRowValues> merged;
1375
        if (!mergeReferents) {
1376
            merged = res;
1377
        } else if (graftState == null) {
1378
            merged = merge(res);
1379
        } else {
1380
            // merge before recursive call, so it can access the main graph
1381
            merged = mergeGraft(res, graftState.byRows);
1382
        }
93 ilm 1383
        if (mergeGrafts) {
144 ilm 1384
            if (mainRes == null) {
1385
                mainRes = new MainResult();
1386
            }
93 ilm 1387
            for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> graftPlaceEntry : grafts.entrySet()) {
17 ilm 1388
                // e.g. BATIMENT
1389
                final Path graftPlace = graftPlaceEntry.getKey();
144 ilm 1390
                // common to all grafts to support CPI -> LOCAL -> BATIMENT and RECEPTEUR
17 ilm 1391
                // -> LOCAL -> BATIMENT (ie avoid duplicate LOCAL)
144 ilm 1392
                mainRes.push(merged, graftPlace);
17 ilm 1393
                for (final Entry<Path, SQLRowValuesListFetcher> e : graftPlaceEntry.getValue().entrySet()) {
1394
                    // e.g BATIMENT <- LOCAL <- CPI
1395
                    final Path descendantPath = e.getKey();
1396
                    assert descendantPath.getFirst() == graftPlace.getLast() : descendantPath + " != " + graftPlace;
1397
                    final SQLRowValuesListFetcher graft = e.getValue();
144 ilm 1398
                    graft.fetch(mainRes, null, null, false);
17 ilm 1399
                }
144 ilm 1400
                mainRes.pop();
17 ilm 1401
            }
1402
        }
151 ilm 1403
        if (addPostLinks) {
1404
            // group by destinationPath to index only once for all links to add
1405
            // SetMap<Tuple2<commonPath, destinationPath>, toAddPath>
1406
            final SetMap<Tuple2<Path, Path>, Path> byCommonPath = new SetMap<>();
1407
            for (final Entry<Path, Path> e : postLinks.entrySet()) {
1408
                final Path toAdd = e.getKey();
1409
                final Path existingPath = e.getValue();
1410
                final Path commonPath = toAdd.getCommonPath(existingPath);
1411
                byCommonPath.add(Tuple2.create(commonPath, existingPath.subPath(commonPath.length())), toAdd.subPath(commonPath.length()));
1412
            }
1413
            /**
1414
             * <pre>
1415
             * LOCAL <-- SRC <-- JOIN --> OBSERVATION
1416
             *            \--ID_OLDEST_OBS--/
1417
             *            \--ID_MOST_SERIOUS_OBS--/
1418
             * </pre>
1419
             */
1420
            for (final Entry<Tuple2<Path, Path>, Set<Path>> e : byCommonPath.entrySet()) {
1421
                // LOCAL <-- SRC
1422
                final Path commonPath = e.getKey().get0();
1423
                // SRC <-- JOIN --> OBSERVATION
1424
                final Path throughTargetRows = e.getKey().get1();
1425
                // SRC -- ID_OLDEST_OBS --> OBSERVATION
1426
                // SRC -- ID_MOST_SERIOUS_OBS --> OBSERVATION
1427
                final Set<Path> pathsToAdd = e.getValue();
1428
 
1429
                for (final SQLRowValues v : merged) {
1430
                    for (final SQLRowValues commonRow : v.getDistantRows(commonPath)) {
1431
                        // index target rows
1432
                        final Map<Number, SQLRowValues> byIDs = new HashMap<>();
1433
                        for (final SQLRowValues target : commonRow.getDistantRows(throughTargetRows)) {
1434
                            byIDs.put(target.getIDNumber(), target);
1435
                        }
1436
                        // add links
1437
                        for (final Path toAdd : pathsToAdd) {
1438
                            final String fkName = toAdd.getStep(-1).getSingleField().getName();
1439
                            // SRC
1440
                            final Path throughRowToUpdate = toAdd.minusLast();
1441
                            for (final SQLRowValues toUpdate : commonRow.getDistantRows(throughRowToUpdate)) {
1442
                                final Number foreignID = toUpdate.getNonEmptyForeignIDNumber(fkName);
1443
                                if (foreignID != null) {
1444
                                    final SQLRowValues target = byIDs.get(foreignID);
1445
                                    if (target == null)
1446
                                        throw new IllegalStateException("Missing row for " + foreignID + " at " + throughTargetRows);
1447
                                    toUpdate.put(fkName, target);
1448
                                }
1449
                            }
1450
                        }
1451
                    }
1452
                }
1453
            }
1454
        }
93 ilm 1455
        if (freezeRows && !handlerCanFreeze) {
1456
            for (final SQLRowValues r : merged) {
1457
                r.getGraph().freeze();
1458
            }
1459
        }
17 ilm 1460
        return merged;
1461
    }
1462
 
1463
    // no need to set keep-alive too low, since on finalize() the pool shutdowns itself
1464
    private static final ExecutorService exec = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
1465
 
1466
    private static final class Linker implements Callable<Object> {
1467
 
1468
        private final List<GraphNode> l;
1469
        private final List<List<SQLRowValues>> rows;
1470
        private final int fromIndex;
1471
        private final int toIndex;
93 ilm 1472
        private final boolean freezeRows;
17 ilm 1473
 
93 ilm 1474
        public Linker(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int first, final int last, final boolean freezeRows) {
17 ilm 1475
            super();
1476
            this.l = l;
1477
            this.rows = rows;
1478
            this.fromIndex = first;
1479
            this.toIndex = last;
93 ilm 1480
            this.freezeRows = freezeRows;
17 ilm 1481
        }
1482
 
1483
        @Override
1484
        public Object call() throws Exception {
93 ilm 1485
            link(this.l, this.rows, this.fromIndex, this.toIndex, this.freezeRows);
17 ilm 1486
            return null;
1487
        }
1488
 
1489
    }
1490
 
93 ilm 1491
    private static void link(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int start, final int stop, final boolean freezeRows) {
17 ilm 1492
        final int graphSize = l.size();
1493
        for (int nodeIndex = 1; nodeIndex < graphSize; nodeIndex++) {
1494
            final GraphNode node = l.get(nodeIndex);
1495
 
1496
            final String fromName = node.getFromName();
1497
            final int linkIndex = node.getLinkIndex();
1498
            final boolean backwards = node.isBackwards();
1499
 
93 ilm 1500
            // freeze after the last put()
1501
            final boolean freeze = freezeRows && nodeIndex == graphSize - 1;
1502
 
17 ilm 1503
            for (int i = start; i < stop; i++) {
1504
                final List<SQLRowValues> row = rows.get(i);
1505
                final SQLRowValues creatingVals = row.get(nodeIndex);
1506
                // don't link empty values (LEFT JOIN produces rowValues filled with
1507
                // nulls) to the graph
83 ilm 1508
                if (creatingVals != null) {
17 ilm 1509
                    final SQLRowValues valsToFill;
1510
                    final SQLRowValues valsToPut;
1511
                    if (backwards) {
1512
                        valsToFill = creatingVals;
1513
                        valsToPut = row.get(linkIndex);
1514
                    } else {
1515
                        valsToFill = row.get(linkIndex);
1516
                        valsToPut = creatingVals;
1517
                    }
1518
 
1519
                    // check is done by updateLinks()
1520
                    valsToFill.put(fromName, valsToPut, false);
1521
                }
93 ilm 1522
                // can't use creatingVals, use primary row which is never null
1523
                if (freeze)
1524
                    row.get(0).getGraph().freeze();
17 ilm 1525
            }
1526
        }
93 ilm 1527
        if (freezeRows && graphSize == 1) {
1528
            for (int i = start; i < stop; i++) {
1529
                final List<SQLRowValues> row = rows.get(i);
1530
                final boolean justFrozen = row.get(0).getGraph().freeze();
1531
                assert justFrozen : "Already frozen";
1532
            }
1533
        }
17 ilm 1534
    }
1535
 
1536
    /**
1537
     * Merge a list of fetched rowValues so that remove any duplicated rowValues. Eg, transforms
1538
     * this :
1539
     *
1540
     * <pre>
1541
     * BATIMENT[2]     LOCAL[2]        CPI_BT[3]
1542
     * BATIMENT[2]     LOCAL[2]        CPI_BT[2]
1543
     * BATIMENT[2]     LOCAL[3]
1544
     * BATIMENT[2]     LOCAL[5]        CPI_BT[5]
1545
     * BATIMENT[3]     LOCAL[4]        CPI_BT[4]
1546
     * BATIMENT[4]
1547
     * </pre>
1548
     *
1549
     * into this :
1550
     *
1551
     * <pre>
1552
     * BATIMENT[2]     LOCAL[2]        CPI_BT[3]
1553
     *                                 CPI_BT[2]
1554
     *                 LOCAL[3]
1555
     *                 LOCAL[5]        CPI_BT[5]
1556
     * BATIMENT[3]     LOCAL[4]        CPI_BT[4]
1557
     * BATIMENT[4]
1558
     * </pre>
1559
     *
1560
     * @param l a list of fetched rowValues.
1561
     * @return a smaller list in which all rowValues are unique.
1562
     */
1563
    private final List<SQLRowValues> merge(final List<SQLRowValues> l) {
93 ilm 1564
        return merge(l, l, null, this.descendantPath);
17 ilm 1565
    }
1566
 
144 ilm 1567
    private final List<SQLRowValues> mergeGraft(final List<SQLRowValues> l, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows) {
1568
        if (graftPlaceRows == null)
1569
            throw new IllegalArgumentException("Missing map");
1570
        return merge(null, l, graftPlaceRows, this.descendantPath);
1571
    }
1572
 
17 ilm 1573
    /**
1574
     * Merge a list of rowValues and optionally graft it onto another one.
1575
     *
1576
     * @param tree the list receiving the graft.
1577
     * @param graft the list being merged and optionally grafted on <code>tree</code>, can be the
1578
     *        same as <code>tree</code>.
1579
     * @param graftPlaceRows if this is a graft the destination rowValues, otherwise
1580
     *        <code>null</code>, this instance will be modified.
1581
     * @param descendantPath the path to merge.
144 ilm 1582
     * @return the merged list of main values, or the graft places if it's a graft.
17 ilm 1583
     */
132 ilm 1584
    static private final List<SQLRowValues> merge(final List<SQLRowValues> tree, final List<SQLRowValues> graft, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows,
1585
            Path descendantPath) {
17 ilm 1586
        final boolean isGraft = graftPlaceRows != null;
1587
        assert (tree != graft) == isGraft : "Trying to graft onto itself";
144 ilm 1588
        final Collection<SQLRowValues> res = isGraft ? new LinkedIdentitySet<SQLRowValues>() : new ArrayList<SQLRowValues>();
17 ilm 1589
        // so that every graft is actually grafted onto the tree
83 ilm 1590
        final ListMap<Tuple2<Path, Number>, SQLRowValues> map = isGraft ? graftPlaceRows : createCollectionMap();
17 ilm 1591
 
1592
        final int stop = descendantPath.length();
1593
        for (final SQLRowValues v : graft) {
1594
            boolean doAdd = true;
61 ilm 1595
            SQLRowValues previous = null;
1596
            for (int i = stop; i >= 0 && doAdd; i--) {
17 ilm 1597
                final Path subPath = descendantPath.subPath(0, i);
1598
                final SQLRowValues desc = v.followPath(subPath);
1599
                if (desc != null) {
1600
                    final Tuple2<Path, Number> row = Tuple2.create(subPath, desc.getIDNumber());
1601
                    if (map.containsKey(row)) {
1602
                        doAdd = false;
83 ilm 1603
                        assert map.get(row).get(0).getFields().containsAll(desc.getFields()) : "Discarding an SQLRowValues with more fields : " + desc;
17 ilm 1604
                        // previous being null can happen when 2 grafted paths share some steps at
1605
                        // the start, e.g. SOURCE -> LOCAL and CPI -> LOCAL with a LOCAL having a
1606
                        // SOURCE but no CPI
1607
                        if (previous != null) {
83 ilm 1608
                            final List<SQLRowValues> destinationRows = map.get(row);
17 ilm 1609
                            final int destinationSize = destinationRows.size();
1610
                            assert destinationSize > 0 : "Map contains row but have no corresponding value: " + row;
93 ilm 1611
                            final String ffName = descendantPath.getSingleField(i).getName();
67 ilm 1612
                            // avoid the first deepCopy() (needed since rows of 'previous' have
1613
                            // already been added to 'map') and copy before merging
17 ilm 1614
                            for (int j = 1; j < destinationSize; j++) {
67 ilm 1615
                                final SQLRowValues previousCopy = previous.deepCopy().put(ffName, destinationRows.get(j));
1616
                                // put the copied rowValues into 'map' otherwise they'd be
1617
                                // unreachable and thus couldn't have referents. Tested by
1618
                                // SQLRowValuesListFetcherTest.testSameReferentMergedMultipleTimes()
1619
                                // i+1 since we start from 'previous' not 'desc'
1620
                                for (int k = stop; k >= i + 1; k--) {
1621
                                    final SQLRowValues descCopy = previousCopy.followPath(descendantPath.subPath(i + 1, k));
1622
                                    if (descCopy != null) {
1623
                                        final Tuple2<Path, Number> rowCopy = Tuple2.create(descendantPath.subPath(0, k), descCopy.getIDNumber());
1624
                                        assert map.containsKey(rowCopy) : "Since we already iterated with i";
83 ilm 1625
                                        map.add(rowCopy, descCopy);
67 ilm 1626
                                    }
1627
                                }
17 ilm 1628
                            }
67 ilm 1629
                            // don't call map.put() it has already been handled below
17 ilm 1630
                            previous.put(ffName, destinationRows.get(0));
144 ilm 1631
 
1632
                            if (isGraft) {
1633
                                final Path pathToGraftPlace = subPath.reverse();
1634
                                for (final SQLRowValues r : destinationRows) {
1635
                                    final SQLRowValues graftPlaceRow = r.followPath(pathToGraftPlace);
1636
                                    if (graftPlaceRow == null)
1637
                                        throw new IllegalStateException("Row at graft place not found");
1638
                                    res.add(graftPlaceRow);
1639
                                }
1640
                            }
17 ilm 1641
                        }
1642
                    } else {
83 ilm 1643
                        map.add(row, desc);
17 ilm 1644
                    }
1645
                    previous = desc;
1646
                }
1647
            }
1648
            if (doAdd) {
1649
                assert !isGraft : "Adding graft values as tree values";
1650
                res.add(v);
1651
            }
1652
        }
144 ilm 1653
        return res instanceof List ? (List<SQLRowValues>) res : new ArrayList<>(res);
17 ilm 1654
    }
1655
 
1656
    @Override
1657
    public String toString() {
132 ilm 1658
        return this.getClass().getSimpleName() + " for " + this.getGraph() + " with " + this.getSelID() + " and " + this.getSelectTransformers();
17 ilm 1659
    }
1660
 
1661
    @Override
1662
    public boolean equals(Object obj) {
1663
        if (obj instanceof SQLRowValuesListFetcher) {
1664
            final SQLRowValuesListFetcher o = (SQLRowValuesListFetcher) obj;
93 ilm 1665
            final SQLSelect thisReq, oReq;
1666
            final Map<Path, Map<Path, SQLRowValuesListFetcher>> thisGrafts, oGrafts;
1667
            synchronized (this) {
1668
                thisReq = this.getReq();
1669
                thisGrafts = this.getGrafts();
1670
            }
1671
            synchronized (o) {
1672
                oReq = o.getReq();
1673
                oGrafts = o.getGrafts();
1674
            }
17 ilm 1675
            // use getReq() to avoid selTransf equality pb (ie we generally use anonymous classes
1676
            // which thus lack equals())
93 ilm 1677
            return thisReq.equals(oReq) && CompareUtils.equals(this.descendantPath, o.descendantPath) && thisGrafts.equals(oGrafts);
17 ilm 1678
        } else
1679
            return false;
1680
    }
1681
 
1682
    @Override
1683
    public int hashCode() {
1684
        return this.getReq().hashCode();
1685
    }
1686
}