OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.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;
17 ilm 23
import org.openconcerto.utils.CompareUtils;
80 ilm 24
import org.openconcerto.utils.ListMap;
17 ilm 25
import org.openconcerto.utils.RTInterruptedException;
26
import org.openconcerto.utils.RecursionType;
27
import org.openconcerto.utils.Tuple2;
28
import org.openconcerto.utils.cc.ITransformer;
29
 
30
import java.sql.ResultSet;
31
import java.sql.SQLException;
32
import java.util.ArrayList;
33
import java.util.Collection;
34
import java.util.Collections;
35
import java.util.HashMap;
36
import java.util.HashSet;
80 ilm 37
import java.util.LinkedHashMap;
73 ilm 38
import java.util.LinkedHashSet;
17 ilm 39
import java.util.List;
40
import java.util.Map;
41
import java.util.Map.Entry;
42
import java.util.Set;
43
import java.util.concurrent.Callable;
44
import java.util.concurrent.ExecutorService;
45
import java.util.concurrent.Future;
46
import java.util.concurrent.LinkedBlockingQueue;
47
import java.util.concurrent.ThreadPoolExecutor;
48
import java.util.concurrent.TimeUnit;
83 ilm 49
import java.util.concurrent.atomic.AtomicInteger;
80 ilm 50
import java.util.concurrent.atomic.AtomicReference;
17 ilm 51
 
52
import org.apache.commons.dbutils.ResultSetHandler;
53
 
54
/**
55
 * Construct a list of linked SQLRowValues from one request.
56
 *
57
 * @author Sylvain
58
 */
59
public class SQLRowValuesListFetcher {
60
 
61 ilm 61
    /**
65 ilm 62
     * Create an ordered fetcher with the necessary grafts to fetch the passed graph.
61 ilm 63
     *
64
     * @param graph what to fetch, can be any tree.
65
     * @return the fetcher.
66
     */
67
    public static SQLRowValuesListFetcher create(final SQLRowValues graph) {
65 ilm 68
        // ORDER shouldn't slow down the query and it makes the result predictable and repeatable
69
        return create(graph, true);
70
    }
71
 
72
    public static SQLRowValuesListFetcher create(final SQLRowValues graph, final boolean ordered) {
61 ilm 73
        // path -> longest referent only path
74
        // i.e. map each path to the main fetcher or a referent graft
75
        final Map<Path, Path> handledPaths = new HashMap<Path, Path>();
80 ilm 76
        final Path emptyPath = Path.get(graph.getTable());
61 ilm 77
        handledPaths.put(emptyPath, emptyPath);
78
        // find out referent only paths (yellow in the diagram)
79
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
80
            @Override
81
            public Path transformChecked(State<Object> input) {
82
                final Path p = input.getPath();
83
                for (int i = p.length(); i > 0; i--) {
84
                    final Path subPath = p.subPath(0, i);
85
                    if (handledPaths.containsKey(subPath))
86
                        break;
87
                    handledPaths.put(subPath, p);
88
                }
89
                return null;
90
            }
80 ilm 91
        }, RecursionType.DEPTH_FIRST, Direction.REFERENT);
61 ilm 92
 
93
        // find out needed grafts
83 ilm 94
        final ListMap<Path, SQLRowValuesListFetcher> grafts = new ListMap<Path, SQLRowValuesListFetcher>();
61 ilm 95
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
96
            @Override
97
            public Path transformChecked(State<Object> input) {
98
                final Path p = input.getPath();
99
                if (!handledPaths.containsKey(p)) {
100
                    final Path pMinusLast = p.minusLast();
101
                    if (!input.isBackwards()) {
102
                        // Forwards can be fetched by existing fetcher (blue in the diagram)
103
                        final Path existingRefPath = handledPaths.get(pMinusLast);
104
                        assert existingRefPath != null;
105
                        handledPaths.put(p, existingRefPath);
106
                    } else {
107
                        // Backwards needs another fetcher
108
                        if (!grafts.containsKey(pMinusLast)) {
109
                            final SQLRowValues copy = graph.deepCopy();
110
                            final SQLRowValues graftNode = copy.followPath(pMinusLast);
111
                            graftNode.clear();
112
                            final SQLRowValues previous = copy.followPath(pMinusLast.minusLast());
113
                            assert p.getStep(-2).isForeign();
114
                            previous.remove(p.getStep(-2).getSingleField().getName());
115
                            // don't recurse forever
116
                            if (previous.getGraph() == graftNode.getGraph())
117
                                throw new IllegalArgumentException("Graph is not a tree");
65 ilm 118
                            // ATTN pMinusLast might not be on the main fetcher so don't graft now
119
                            // also we can only graft non empty descendant path fetchers (plus
120
                            // removing a fetcher saves one request)
121
                            final SQLRowValuesListFetcher rec = create(graftNode, ordered);
122
                            final Collection<SQLRowValuesListFetcher> ungrafted = rec.ungraft();
123
                            if (ungrafted == null || ungrafted.size() == 0) {
124
                                // i.e. only one referent and thus graft not necessary
125
                                assert rec.descendantPath.length() > 0;
83 ilm 126
                                grafts.add(pMinusLast, rec);
65 ilm 127
                            } else {
83 ilm 128
                                grafts.addAll(pMinusLast, ungrafted);
65 ilm 129
                            }
61 ilm 130
                        }
131
                        throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
132
                    }
133
                }
134
                return null;
135
            }
83 ilm 136
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
61 ilm 137
 
138
        final Set<Path> refPaths = new HashSet<Path>(handledPaths.values());
139
        // remove the main fetcher
140
        refPaths.remove(emptyPath);
65 ilm 141
        // fetchers for the referent paths (yellow part)
61 ilm 142
        final Map<Path, SQLRowValuesListFetcher> graftedFetchers;
143
        // create the main fetcher and grafts
144
        final SQLRowValuesListFetcher res;
145
        if (refPaths.size() == 1) {
146
            res = new SQLRowValuesListFetcher(graph, refPaths.iterator().next());
147
            graftedFetchers = Collections.emptyMap();
148
        } else {
149
            res = new SQLRowValuesListFetcher(graph, false);
150
            graftedFetchers = new HashMap<Path, SQLRowValuesListFetcher>();
151
            if (refPaths.size() > 0) {
152
                final Path graftPath = new Path(graph.getTable());
153
                final SQLRowValues copy = graph.deepCopy();
154
                copy.clear();
155
                for (final Path refPath : refPaths) {
65 ilm 156
                    final SQLRowValuesListFetcher f = new SQLRowValuesListFetcher(copy, refPath, true).setOrdered(ordered);
61 ilm 157
                    res.graft(f, graftPath);
158
                    graftedFetchers.put(refPath, f);
159
                }
160
            }
161
        }
65 ilm 162
        res.setOrdered(ordered);
163
 
61 ilm 164
        // now graft recursively created grafts
83 ilm 165
        for (final Entry<Path, ? extends Collection<SQLRowValuesListFetcher>> e : grafts.entrySet()) {
61 ilm 166
            final Path graftPath = e.getKey();
167
            final Path refPath = handledPaths.get(graftPath);
65 ilm 168
            // can be grafted on the main fetcher or on the referent fetchers
61 ilm 169
            final SQLRowValuesListFetcher f = graftedFetchers.containsKey(refPath) ? graftedFetchers.get(refPath) : res;
65 ilm 170
            for (final SQLRowValuesListFetcher recFetcher : e.getValue())
171
                f.graft(recFetcher, graftPath);
61 ilm 172
        }
173
        return res;
174
    }
175
 
17 ilm 176
    // return the referent single link path starting from graph
177
    private static Path computePath(SQLRowValues graph) {
178
        // check that there's only one referent for each row
179
        // (otherwise huge joins, e.g. LOCAL<-CPI,SOURCE,RECEPTEUR,etc.)
80 ilm 180
        final AtomicReference<Path> res = new AtomicReference<Path>(null);
181
        graph.getGraph().walk(graph, null, new ITransformer<State<Path>, Path>() {
17 ilm 182
            @Override
183
            public Path transformChecked(State<Path> input) {
184
                final Collection<SQLRowValues> referentRows = input.getCurrent().getReferentRows();
80 ilm 185
                final int size = referentRows.size();
186
                if (size > 1) {
17 ilm 187
                    // remove the foreign rows which are all the same (since they point to
188
                    // current) so the exn is more legible
189
                    final List<SQLRowValues> toPrint = SQLRowValues.trim(referentRows);
190
                    throw new IllegalArgumentException(input.getCurrent() + " is referenced by " + toPrint + "\nat " + input.getPath());
80 ilm 191
                } else if (size == 0) {
192
                    if (res.get() == null)
193
                        res.set(input.getPath());
194
                    else
195
                        throw new IllegalStateException();
17 ilm 196
                }
197
                return input.getAcc();
198
            }
83 ilm 199
        }, RecursionType.BREADTH_FIRST, Direction.REFERENT);
80 ilm 200
        // since includeStart=true
201
        assert res.get() != null;
202
        return res.get();
17 ilm 203
    }
204
 
83 ilm 205
    static private final ListMap<Tuple2<Path, Number>, SQLRowValues> createCollectionMap() {
17 ilm 206
        // we need a List in merge()
83 ilm 207
        return new ListMap<Tuple2<Path, Number>, SQLRowValues>() {
208
            @Override
209
            public List<SQLRowValues> createCollection(Collection<? extends SQLRowValues> v) {
210
                final List<SQLRowValues> res = new ArrayList<SQLRowValues>(8);
211
                res.addAll(v);
212
                return res;
213
            }
214
        };
17 ilm 215
    }
216
 
217
    private final SQLRowValues graph;
218
    private final Path descendantPath;
219
    private ITransformer<SQLSelect, SQLSelect> selTransf;
220
    private Integer selID;
73 ilm 221
    private Set<Path> ordered;
222
    private boolean descendantsOrdered;
17 ilm 223
    private SQLRowValues minGraph;
224
    private boolean includeForeignUndef;
225
    private SQLSelect frozen;
226
    // graftPlace -> {referent path -> fetcher}
227
    private final Map<Path, Map<Path, SQLRowValuesListFetcher>> grafts;
228
 
229
    /**
230
     * Construct a new instance with the passed graph of SQLRowValues.
231
     *
232
     * @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
233
     *        SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
234
     *        to return all sites (with all their fields) with their associated contacts.
235
     */
236
    public SQLRowValuesListFetcher(SQLRowValues graph) {
237
        this(graph, false);
238
    }
239
 
240
    /**
241
     * Construct a new instance with the passed graph of SQLRowValues. Eg if <code>graph</code> is a
242
     * BATIMENT which points to SITE, is pointed by LOCAL, CPI_BT and <code>referents</code> is
243
     * <code>true</code>, {@link #fetch()} could return
244
     *
245
     * <pre>
246
     * SITE[2]  BATIMENT[2]     LOCAL[2]    CPI_BT[3]
247
     *                                      CPI_BT[2]
248
     *                          LOCAL[3]
249
     *                          LOCAL[5]    CPI_BT[5]
250
     * SITE[7]  BATIMENT[3]     LOCAL[4]    CPI_BT[4]
251
     * SITE[7]  BATIMENT[4]
252
     * </pre>
253
     *
254
     * @param graph what SQLRowValues should be returned by {@link #fetch()}, eg <code>new
255
     *        SQLRowValues("SITE").setAllToNull().put("ID_CONTACT", new SQLRowValues("CONTACT"))</code>
256
     *        to return all sites (with all their fields) with their associated contacts.
257
     * @param referents <code>true</code> if referents to <code>graph</code> should also be fetched.
258
     */
259
    public SQLRowValuesListFetcher(SQLRowValues graph, final boolean referents) {
260
        this(graph, referents ? computePath(graph) : null);
261
    }
262
 
263
    /**
264
     * Construct a new instance.
265
     *
61 ilm 266
     * @param graph what SQLRowValues should be returned by {@link #fetch()}.
17 ilm 267
     * @param referentPath a {@link Path#isSingleLink() single link} path from the primary table,
268
     *        <code>null</code> meaning don't fetch referent rows.
269
     */
270
    public SQLRowValuesListFetcher(SQLRowValues graph, final Path referentPath) {
61 ilm 271
        this(graph, referentPath, true);
272
    }
273
 
274
    /**
275
     * Construct a new instance.
276
     *
277
     * @param graph what SQLRowValues should be returned by {@link #fetch()}.
278
     * @param referentPath a {@link Path#isSingleLink() single link} path from the primary table,
279
     *        <code>null</code> meaning don't fetch referent rows.
280
     * @param prune if <code>true</code> the graph will be pruned to only contain
281
     *        <code>referentPath</code>. If <code>false</code> the graph will be kept as is, which
282
     *        can produce undefined results if there exist more than one referent row outside of
283
     *        <code>referentPath</code>.
284
     */
285
    SQLRowValuesListFetcher(final SQLRowValues graph, final Path referentPath, final boolean prune) {
17 ilm 286
        super();
287
        this.graph = graph.deepCopy();
80 ilm 288
        this.descendantPath = referentPath == null ? Path.get(graph.getTable()) : referentPath;
67 ilm 289
        if (!this.descendantPath.isDirection(Direction.REFERENT))
290
            throw new IllegalArgumentException("path is not (exclusively) referent : " + this.descendantPath);
61 ilm 291
        final SQLRowValues descRow = this.graph.followPath(this.descendantPath);
292
        if (descRow == null)
293
            throw new IllegalArgumentException("path is not contained in the passed rowValues : " + referentPath + "\n" + this.graph.printTree());
17 ilm 294
        // followPath() do the following check
61 ilm 295
        assert this.descendantPath.getFirst() == this.graph.getTable() && this.descendantPath.isSingleLink();
296
 
297
        if (prune) {
298
            this.graph.getGraph().walk(descRow, null, new ITransformer<State<Object>, Object>() {
299
                @Override
300
                public Object transformChecked(State<Object> input) {
301
                    if (input.getFrom() == null) {
302
                        input.getCurrent().clearReferents();
303
                    } else {
65 ilm 304
                        input.getCurrent().retainReferent(input.getPrevious());
61 ilm 305
                    }
306
                    return null;
307
                }
80 ilm 308
            }, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
61 ilm 309
        }
310
 
17 ilm 311
        // always need IDs
312
        for (final SQLRowValues curr : this.getGraph().getGraph().getItems()) {
313
            // don't overwrite existing values
314
            if (!curr.hasID())
315
                curr.setID(null);
316
        }
317
 
318
        this.selTransf = null;
319
        this.selID = null;
73 ilm 320
        this.ordered = Collections.<Path> emptySet();
321
        this.descendantsOrdered = false;
17 ilm 322
        this.minGraph = null;
323
        this.includeForeignUndef = false;
324
        this.frozen = null;
325
        this.grafts = new HashMap<Path, Map<Path, SQLRowValuesListFetcher>>(4);
326
    }
327
 
328
    /**
329
     * Make this instance immutable. Ie all setters will now throw {@link IllegalStateException}.
330
     * Furthermore the request will be computed now once and for all, so as not to be subject to
331
     * outside modification by {@link #getSelTransf()}.
332
     *
333
     * @return this.
334
     */
335
    public final SQLRowValuesListFetcher freeze() {
336
        if (!this.isFrozen()) {
337
            this.frozen = new SQLSelect(this.getReq());
338
            for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
339
                for (final SQLRowValuesListFetcher f : m.values())
340
                    f.freeze();
341
            }
342
        }
343
        return this;
344
    }
345
 
346
    public final boolean isFrozen() {
347
        return this.frozen != null;
348
    }
349
 
350
    private final void checkFrozen() {
351
        if (this.isFrozen())
352
            throw new IllegalStateException("this has been frozen: " + this);
353
    }
354
 
355
    public SQLRowValues getGraph() {
356
        return this.graph;
357
    }
358
 
80 ilm 359
    public final Path getReferentPath() {
360
        return this.descendantPath;
361
    }
362
 
17 ilm 363
    /**
364
     * Whether to include undefined rows (of tables other than the graph's).
365
     *
366
     * @param includeForeignUndef <code>true</code> to include undefined rows.
367
     */
368
    public final void setIncludeForeignUndef(boolean includeForeignUndef) {
369
        this.checkFrozen();
370
        this.includeForeignUndef = includeForeignUndef;
371
    }
372
 
373
    /**
374
     * Require that only rows with values for the full graph are returned. Eg if the graph is CPI ->
375
     * OBS, setting this to <code>true</code> will excludes CPI without OBS.
376
     *
377
     * @param b <code>true</code> if only full rows should be fetched.
378
     */
379
    public final void setFullOnly(boolean b) {
380
        this.checkFrozen();
381
        if (b)
61 ilm 382
            this.minGraph = this.getGraph().deepCopy();
17 ilm 383
        else
384
            this.minGraph = null;
385
    }
386
 
61 ilm 387
    public final void requirePath(final Path p) {
388
        this.checkFrozen();
389
        if (this.getGraph().followPath(p) == null)
67 ilm 390
            throw new IllegalArgumentException("Path not included in this graph : " + p + "\n" + this.getGraph().printGraph());
61 ilm 391
        if (this.minGraph == null)
392
            this.minGraph = new SQLRowValues(getGraph().getTable());
393
        this.minGraph.assurePath(p);
394
    }
395
 
396
    private final boolean isPathRequired(final Path p) {
17 ilm 397
        return this.minGraph != null && this.minGraph.followPath(p) != null;
398
    }
399
 
400
    private boolean fetchReferents() {
61 ilm 401
        return this.descendantPath.length() > 0;
17 ilm 402
    }
403
 
404
    /**
405
     * To modify the query before execution.
406
     *
407
     * @param selTransf will be passed the query which has been constructed, and the return value
408
     *        will be actually executed, can be <code>null</code>.
409
     */
410
    public void setSelTransf(ITransformer<SQLSelect, SQLSelect> selTransf) {
411
        this.checkFrozen();
412
        this.selTransf = selTransf;
413
    }
414
 
415
    public final ITransformer<SQLSelect, SQLSelect> getSelTransf() {
416
        return this.selTransf;
417
    }
418
 
419
    /**
420
     * Add a where in {@link #getReq()} to restrict the primary key.
421
     *
422
     * @param selID an ID for the primary key, <code>null</code> to not filter.
423
     */
424
    public void setSelID(Integer selID) {
425
        this.checkFrozen();
426
        this.selID = selID;
427
    }
428
 
429
    public final Integer getSelID() {
430
        return this.selID;
431
    }
432
 
433
    /**
434
     * Whether to add ORDER BY in {@link #getReq()}.
435
     *
436
     * @param b <code>true</code> if the query should be ordered.
65 ilm 437
     * @return this.
17 ilm 438
     */
65 ilm 439
    public final SQLRowValuesListFetcher setOrdered(final boolean b) {
80 ilm 440
        this.setOrder(b ? Collections.singleton(Path.get(getGraph().getTable())) : Collections.<Path> emptySet(), true);
73 ilm 441
        this.setReferentsOrdered(b, false);
442
        return this;
443
    }
444
 
445
    public final SQLRowValuesListFetcher setOrder(final List<Path> order) {
446
        return this.setOrder(order, false);
447
    }
448
 
449
    private final SQLRowValuesListFetcher setOrder(final Collection<Path> order, final boolean safeVal) {
17 ilm 450
        this.checkFrozen();
73 ilm 451
        for (final Path p : order)
452
            if (this.getGraph().followPath(p) == null)
453
                throw new IllegalArgumentException("Path not in this " + p);
454
        this.ordered = safeVal ? (Set<Path>) order : Collections.unmodifiableSet(new LinkedHashSet<Path>(order));
65 ilm 455
        return this;
17 ilm 456
    }
457
 
73 ilm 458
    public final Set<Path> getOrder() {
17 ilm 459
        return this.ordered;
460
    }
461
 
73 ilm 462
    /**
463
     * Whether to order referent rows in this fetcher.
464
     *
465
     * @param b <code>true</code> to order referent rows starting from the primary node, e.g. if the
466
     *        graph is
467
     *
468
     *        <pre>
469
     * *SITE* <- BATIMENT <- LOCAL
470
     * </pre>
471
     *
472
     *        then this will cause ORDER BY BATIMENT.ORDRE, LOCAL.ORDRE.
473
     * @param rec if grafts should also be changed.
474
     * @return this.
475
     */
476
    public final SQLRowValuesListFetcher setReferentsOrdered(final boolean b, final boolean rec) {
477
        this.descendantsOrdered = b;
478
        if (rec) {
479
            for (final Map<Path, SQLRowValuesListFetcher> m : this.grafts.values()) {
480
                for (final SQLRowValuesListFetcher f : m.values())
481
                    f.setReferentsOrdered(b, rec);
482
            }
483
        }
484
        return this;
485
    }
486
 
487
    public final boolean areReferentsOrdered() {
488
        return this.descendantsOrdered;
489
    }
490
 
17 ilm 491
    public final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other) {
80 ilm 492
        return this.graft(other, Path.get(getGraph().getTable()));
17 ilm 493
    }
494
 
495
    public final SQLRowValuesListFetcher graft(final SQLRowValues other, Path graftPath) {
496
        // with referents otherwise it's useless
497
        return this.graft(new SQLRowValuesListFetcher(other, true), graftPath);
498
    }
499
 
500
    /**
501
     * Graft a fetcher on this graph.
502
     *
503
     * @param other another instance fetching rows of the table at <code>graftPath</code>.
504
     * @param graftPath a path from this values to where <code>other</code> rows should be grafted.
505
     * @return the previous fetcher.
506
     */
507
    public final SQLRowValuesListFetcher graft(final SQLRowValuesListFetcher other, Path graftPath) {
508
        checkFrozen();
509
        if (this == other)
510
            throw new IllegalArgumentException("trying to graft onto itself");
511
        if (other.getGraph().getTable() != graftPath.getLast())
512
            throw new IllegalArgumentException("trying to graft " + other.getGraph().getTable() + " at " + graftPath);
513
        final SQLRowValues graftPlace = this.getGraph().followPath(graftPath);
514
        if (graftPlace == null)
515
            throw new IllegalArgumentException("path doesn't exist: " + graftPath);
516
        assert graftPath.getLast() == graftPlace.getTable();
83 ilm 517
        if (other.getGraph().hasForeigns())
17 ilm 518
            throw new IllegalArgumentException("shouldn't have foreign rows");
519
 
520
        final Path descendantPath = computePath(other.getGraph());
61 ilm 521
        final int descendantPathLength = descendantPath.length();
522
        if (descendantPathLength == 0)
17 ilm 523
            throw new IllegalArgumentException("empty path");
524
        // checked by computePath
525
        assert descendantPath.isSingleLink();
65 ilm 526
        // we used to disallow that :
527
        // this is LOCAL* -> BATIMENT -> SITE and CPI -> LOCAL -> BATIMENT* is being grafted
528
        // but this is sometimes desirable, e.g. for each LOCAL find all of its siblings with the
529
        // same capacity (or any other predicate)
530
 
17 ilm 531
        if (!this.grafts.containsKey(graftPath)) {
80 ilm 532
            // allow getFetchers() to use a list, easing tests and avoiding using equals()
533
            this.grafts.put(graftPath, new LinkedHashMap<Path, SQLRowValuesListFetcher>(4));
61 ilm 534
        } else {
535
            final Map<Path, SQLRowValuesListFetcher> map = this.grafts.get(graftPath);
536
            // e.g. fetching *BATIMENT* <- LOCAL and *BATIMENT* <- LOCAL <- CPI (with different
537
            // WHERE) and LOCAL have different fields. This isn't supported since we would have to
538
            // merge fields in merge() and it would be quite long
539
            for (Entry<Path, SQLRowValuesListFetcher> e : map.entrySet()) {
540
                final Path fetcherPath = e.getKey();
541
                final SQLRowValuesListFetcher fetcher = e.getValue();
542
                for (int i = 1; i <= descendantPathLength; i++) {
543
                    final Path subPath = descendantPath.subPath(0, i);
544
                    if (fetcherPath.startsWith(subPath)) {
545
                        if (!fetcher.getGraph().followPath(subPath).getFields().equals(other.getGraph().followPath(subPath).getFields()))
546
                            throw new IllegalArgumentException("The same node have different fields in different fetcher\n" + graftPath + "\n" + subPath);
547
                    } else {
548
                        break;
549
                    }
550
                }
17 ilm 551
            }
552
        }
553
        return this.grafts.get(graftPath).put(descendantPath, other);
554
    }
555
 
61 ilm 556
    public final Collection<SQLRowValuesListFetcher> ungraft() {
80 ilm 557
        return this.ungraft(Path.get(getGraph().getTable()));
17 ilm 558
    }
559
 
61 ilm 560
    public final Collection<SQLRowValuesListFetcher> ungraft(final Path graftPath) {
17 ilm 561
        checkFrozen();
61 ilm 562
        final Map<Path, SQLRowValuesListFetcher> res = this.grafts.remove(graftPath);
563
        return res == null ? null : res.values();
17 ilm 564
    }
565
 
67 ilm 566
    /**
567
     * The fetchers grafted at the passed path.
568
     *
569
     * @param graftPath where the fetchers are grafted, e.g. MISSION, DOSSIER, SITE.
570
     * @return the grafts by their path to fetch, e.g. SITE, BATIMENT, LOCAL, CPI_BT.
571
     */
572
    public final Map<Path, SQLRowValuesListFetcher> getGrafts(final Path graftPath) {
573
        return Collections.unmodifiableMap(this.grafts.get(graftPath));
574
    }
575
 
80 ilm 576
    /**
83 ilm 577
     * Get all fetchers.
578
     *
579
     * @param includeSelf <code>true</code> to include <code>this</code> (with a <code>null</code>
580
     *        key).
581
     * @return all instances indexed by the graft path.
582
     */
583
    public final ListMapItf<Path, SQLRowValuesListFetcher> getFetchers(final boolean includeSelf) {
584
        final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
585
        for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.grafts.entrySet()) {
586
            assert e.getKey() != null;
587
            res.putCollection(e.getKey(), e.getValue().values());
588
        }
589
        if (includeSelf)
590
            res.add(null, this);
591
        return ListMap.unmodifiableMap(res);
592
    }
593
 
594
    /**
80 ilm 595
     * Get instances which fetch the {@link Path#getLast() last table} of the passed path. E.g.
596
     * useful if you want to add a where to a join. This method is recursively called on
597
     * {@link #getGrafts(Path) grafts} thus the returned paths may be fetched by grafts.
598
     *
599
     * @param fetchedPath a path starting by this table.
600
     * @return all instances indexed by the graft path, i.e. <code>fetchedPath</code> is between
601
     *         with it and (it+fetchers.{@link #getReferentPath()}).
602
     */
603
    public final ListMap<Path, SQLRowValuesListFetcher> getFetchers(final Path fetchedPath) {
604
        final ListMap<Path, SQLRowValuesListFetcher> res = new ListMap<Path, SQLRowValuesListFetcher>();
605
        if (this.getGraph().followPath(fetchedPath) != null)
606
            res.add(Path.get(getGraph().getTable()), this);
607
        // search grafts
608
        for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> e : this.grafts.entrySet()) {
609
            final Path graftPlace = e.getKey();
610
            if (fetchedPath.startsWith(graftPlace) && fetchedPath.length() > graftPlace.length()) {
611
                final Path rest = fetchedPath.subPath(graftPlace.length());
612
                // we want requests that use the last step of fetchedPath
613
                assert rest.length() > 0;
614
                for (final Entry<Path, SQLRowValuesListFetcher> e2 : e.getValue().entrySet()) {
615
                    final Path refPath = e2.getKey();
616
                    final SQLRowValuesListFetcher graft = e2.getValue();
617
                    if (refPath.startsWith(rest)) {
618
                        res.add(graftPlace, graft);
619
                    } else if (rest.startsWith(refPath)) {
620
                        // otherwise rest == refPath and the above if would have been executed
621
                        assert rest.length() > refPath.length();
622
                        for (final Entry<Path, List<SQLRowValuesListFetcher>> e3 : graft.getFetchers(rest).entrySet()) {
623
                            res.addAll(graftPlace.append(e3.getKey()), e3.getValue());
624
                        }
625
                    }
626
                }
627
            }
628
        }
629
        return res;
630
    }
631
 
17 ilm 632
    private final void addFields(final SQLSelect sel, final SQLRowValues vals, final String alias) {
83 ilm 633
        // put key first
634
        final SQLField key = vals.getTable().getKey();
635
        sel.addSelect(new AliasedField(key, alias));
636
        for (final String fieldName : vals.getFields()) {
637
            if (!fieldName.equals(key.getName()))
638
                sel.addSelect(new AliasedField(vals.getTable().getField(fieldName), alias));
639
        }
17 ilm 640
    }
641
 
642
    public final SQLSelect getReq() {
643
        if (this.isFrozen())
644
            return this.frozen;
645
 
646
        final SQLTable t = this.getGraph().getTable();
73 ilm 647
        final SQLSelect sel = new SQLSelect();
17 ilm 648
 
649
        if (this.includeForeignUndef) {
650
            sel.setExcludeUndefined(false);
651
            sel.setExcludeUndefined(true, t);
652
        }
653
 
654
        walk(sel, new ITransformer<State<SQLSelect>, SQLSelect>() {
655
            @Override
656
            public SQLSelect transformChecked(State<SQLSelect> input) {
657
                final String alias;
658
                if (input.getFrom() != null) {
659
                    alias = getAlias(input.getAcc(), input.getPath());
65 ilm 660
                    final String aliasPrev = input.getPath().length() == 1 ? null : input.getAcc().followPath(t.getName(), input.getPath().subPath(0, -1)).getAlias();
61 ilm 661
                    final String joinType = isPathRequired(input.getPath()) ? "INNER" : "LEFT";
17 ilm 662
                    if (input.isBackwards()) {
663
                        // eg LEFT JOIN loc on loc.ID_BATIMENT = BATIMENT.ID
61 ilm 664
                        input.getAcc().addBackwardJoin(joinType, alias, input.getFrom(), aliasPrev);
17 ilm 665
                    } else {
666
                        input.getAcc().addJoin(joinType, new AliasedField(input.getFrom(), aliasPrev), alias);
667
                    }
668
 
83 ilm 669
                } else {
17 ilm 670
                    alias = null;
83 ilm 671
                }
17 ilm 672
                addFields(input.getAcc(), input.getCurrent(), alias);
673
 
674
                return input.getAcc();
675
            }
676
 
677
        });
73 ilm 678
        for (final Path p : this.getOrder())
679
            sel.addOrder(sel.followPath(t.getName(), p), false);
680
        // after getOrder() since it can specify more precise order
681
        if (this.areReferentsOrdered()) {
682
            final int descSize = this.descendantPath.length();
683
            for (int i = 1; i <= descSize; i++) {
684
                sel.addOrder(sel.followPath(t.getName(), this.descendantPath.subPath(0, i)), false);
685
            }
686
        }
687
 
17 ilm 688
        if (this.selID != null)
689
            sel.andWhere(new Where(t.getKey(), "=", this.selID));
690
        return this.getSelTransf() == null ? sel : this.getSelTransf().transformChecked(sel);
691
    }
692
 
693
    static String getAlias(final SQLSelect sel, final Path path) {
694
        String res = "tAlias";
695
        final int stop = path.length();
696
        for (int i = 0; i < stop; i++) {
697
            res += "__" + path.getSingleStep(i).getName();
698
        }
699
        // needed for backward, otherwise tableAlias__ID_BATIMENT for LOCAL
700
        res += "__" + path.getTable(stop).getName();
701
        return sel.getUniqueAlias(res);
702
    }
703
 
704
    // assure that the graph is explored the same way for the construction of the request
705
    // and the reading of the resultSet
706
    private <S> void walk(final S sel, final ITransformer<State<S>, S> transf) {
707
        // walk through foreign keys and never walk back (use graft())
80 ilm 708
        this.getGraph().getGraph().walk(this.getGraph(), sel, transf, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
17 ilm 709
        // walk starting backwards but allowing forwards
710
        this.getGraph().getGraph().walk(this.getGraph(), sel, new ITransformer<State<S>, S>() {
711
            @Override
712
            public S transformChecked(State<S> input) {
713
                final Path p = input.getPath();
714
                if (p.getStep(0).isForeign())
715
                    throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
716
                final Step lastStep = p.getStep(p.length() - 1);
717
                // if we go backwards it should be from the start (i.e. we can't go backwards, then
718
                // forwards and backwards again)
67 ilm 719
                if (!lastStep.isForeign() && p.getDirection() != Direction.REFERENT)
17 ilm 720
                    throw new SQLRowValuesCluster.StopRecurseException().setCompletely(false);
721
                return transf.transformChecked(input);
722
            }
83 ilm 723
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false));
17 ilm 724
    }
725
 
726
    // models the graph, so that we don't have to walk it for each row
727
    private static final class GraphNode {
728
        private final SQLTable t;
729
        private final int fieldCount;
83 ilm 730
        private final int foreignCount;
17 ilm 731
        private final int linkIndex;
732
        private final Step from;
733
 
734
        private GraphNode(final State<Integer> input) {
735
            super();
736
            this.t = input.getCurrent().getTable();
737
            this.fieldCount = input.getCurrent().size();
83 ilm 738
            this.foreignCount = input.getCurrent().getForeigns().size();
17 ilm 739
            this.linkIndex = input.getAcc();
740
            final int length = input.getPath().length();
741
            this.from = length == 0 ? null : input.getPath().getStep(length - 1);
742
        }
743
 
744
        public final SQLTable getTable() {
745
            return this.t;
746
        }
747
 
748
        public final int getFieldCount() {
749
            return this.fieldCount;
750
        }
751
 
83 ilm 752
        public final int getForeignCount() {
753
            return this.foreignCount;
754
        }
755
 
17 ilm 756
        public final int getLinkIndex() {
757
            return this.linkIndex;
758
        }
759
 
760
        public final String getFromName() {
761
            return this.from.getSingleField().getName();
762
        }
763
 
764
        public final boolean isBackwards() {
73 ilm 765
            return !this.from.isForeign();
17 ilm 766
        }
767
 
768
        @Override
25 ilm 769
        public int hashCode() {
770
            final int prime = 31;
771
            int result = 1;
772
            result = prime * result + this.fieldCount;
773
            result = prime * result + ((this.from == null) ? 0 : this.from.hashCode());
774
            result = prime * result + this.linkIndex;
775
            result = prime * result + this.t.hashCode();
776
            return result;
777
        }
778
 
779
        @Override
780
        public boolean equals(Object obj) {
781
            if (this == obj)
782
                return true;
783
            if (obj == null)
784
                return false;
785
            if (getClass() != obj.getClass())
786
                return false;
787
            final GraphNode other = (GraphNode) obj;
788
            return this.fieldCount == other.fieldCount && this.linkIndex == other.linkIndex && this.t.equals(other.t) && CompareUtils.equals(this.from, other.from);
789
        }
790
 
791
        @Override
17 ilm 792
        public String toString() {
793
            final String link = this.from == null ? "" : " linked to " + getLinkIndex() + " by " + this.getFromName() + (this.isBackwards() ? " backwards" : " forewards");
794
            return this.getFieldCount() + " fields of " + this.getTable() + link;
795
        }
796
    }
797
 
25 ilm 798
    static private final class RSH implements ResultSetHandler {
799
        private final List<String> selectFields;
800
        private final List<GraphNode> graphNodes;
801
 
802
        private RSH(List<String> selectFields, List<GraphNode> l) {
803
            this.selectFields = selectFields;
804
            this.graphNodes = l;
805
        }
806
 
807
        @Override
808
        public Object handle(final ResultSet rs) throws SQLException {
809
            final List<GraphNode> l = this.graphNodes;
810
            final int graphSize = l.size();
811
            int nextToLink = 0;
812
            final List<Future<?>> futures = new ArrayList<Future<?>>();
813
 
814
            final List<SQLRowValues> res = new ArrayList<SQLRowValues>(64);
815
            final List<List<SQLRowValues>> rows = Collections.synchronizedList(new ArrayList<List<SQLRowValues>>(64));
816
            // for each rs row, create all SQLRowValues without linking them together
817
            // if we're multi-threaded, link them in another thread
818
            while (rs.next()) {
819
                int rsIndex = 1;
820
 
821
                // MAYBE cancel() futures
822
                if (Thread.currentThread().isInterrupted())
823
                    throw new RTInterruptedException("interrupted while fetching");
824
                final List<SQLRowValues> row = new ArrayList<SQLRowValues>(graphSize);
825
                for (int i = 0; i < graphSize; i++) {
826
                    final GraphNode node = l.get(i);
83 ilm 827
                    final int stop = rsIndex + node.getFieldCount();
828
                    final SQLRowValues creatingVals;
829
                    // the PK is always first and it can only be null if there was no row, i.e. all
830
                    // other fields will be null.
831
                    final Object first = rs.getObject(rsIndex);
832
                    if (first == null) {
833
                        creatingVals = null;
834
                        // don't bother reading all nulls
835
                        rsIndex = stop;
836
                    } else {
837
                        // don't pass referent count as it can be fetched by a graft, or else
838
                        // several rows might later be merged (e.g. *BATIMENT* <- LOCAL has only one
839
                        // referent but all locals of a batiment will point to the same row)
840
                        creatingVals = new SQLRowValues(node.getTable(), node.getFieldCount(), node.getForeignCount(), -1);
841
                        put(creatingVals, rsIndex, first);
842
                        rsIndex++;
843
                    }
844
                    if (i == 0) {
845
                        if (creatingVals == null)
846
                            throw new IllegalStateException("Null primary row");
25 ilm 847
                        res.add(creatingVals);
83 ilm 848
                    }
25 ilm 849
 
850
                    for (; rsIndex < stop; rsIndex++) {
851
                        try {
83 ilm 852
                            put(creatingVals, rsIndex, rs.getObject(rsIndex));
25 ilm 853
                        } catch (SQLException e) {
854
                            throw new IllegalStateException("unable to fill " + creatingVals, e);
855
                        }
856
                    }
857
                    row.add(creatingVals);
858
                }
859
                rows.add(row);
860
                // become multi-threaded only for large values
861
                final int currentCount = rows.size();
862
                if (currentCount % 1000 == 0) {
863
                    futures.add(exec.submit(new Linker(l, rows, nextToLink, currentCount)));
864
                    nextToLink = currentCount;
865
                }
866
            }
867
            final int rowSize = rows.size();
868
            assert nextToLink > 0 == futures.size() > 0;
869
            if (nextToLink > 0)
870
                futures.add(exec.submit(new Linker(l, rows, nextToLink, rowSize)));
871
 
872
            // either link all rows, or...
873
            if (nextToLink == 0)
874
                link(l, rows, 0, rowSize);
875
            else {
876
                // ...wait for every one and most importantly check for any exceptions
877
                try {
878
                    for (final Future<?> f : futures)
879
                        f.get();
880
                } catch (Exception e) {
881
                    throw new IllegalStateException("couldn't link", e);
882
                }
883
            }
884
 
885
            return res;
886
        }
887
 
83 ilm 888
        protected void put(final SQLRowValues creatingVals, int rsIndex, final Object obj) {
889
            // -1 since rs starts at 1
890
            // field names checked only once when nodes are created
891
            creatingVals.put(this.selectFields.get(rsIndex - 1), obj, false);
892
        }
893
 
25 ilm 894
        @Override
895
        public int hashCode() {
896
            final int prime = 31;
897
            int result = 1;
898
            result = prime * result + this.graphNodes.hashCode();
899
            result = prime * result + this.selectFields.hashCode();
900
            return result;
901
        }
902
 
903
        @Override
904
        public boolean equals(Object obj) {
905
            if (this == obj)
906
                return true;
907
            if (obj == null)
908
                return false;
909
            if (getClass() != obj.getClass())
910
                return false;
911
            final RSH other = (RSH) obj;
912
            return this.graphNodes.equals(other.graphNodes) && this.selectFields.equals(other.selectFields);
913
        }
914
 
915
    }
916
 
17 ilm 917
    /**
918
     * Execute the request transformed by <code>selTransf</code> and return the result as a list of
25 ilm 919
     * SQLRowValues. NOTE: this method doesn't use the cache of SQLDataSource.
17 ilm 920
     *
921
     * @return a list of SQLRowValues, one item per row, each item having the same structure as the
922
     *         SQLRowValues passed to the constructor.
923
     */
924
    public final List<SQLRowValues> fetch() {
925
        return this.fetch(true);
926
    }
927
 
928
    private final List<SQLRowValues> fetch(final boolean merge) {
929
        final SQLSelect req = this.getReq();
930
        // getName() would take 5% of ResultSetHandler.handle()
83 ilm 931
        final List<FieldRef> selectFields = req.getSelectFields();
932
        final int selectFieldsSize = selectFields.size();
933
        final List<String> selectFieldsNames = new ArrayList<String>(selectFieldsSize);
934
        for (final FieldRef f : selectFields)
935
            selectFieldsNames.add(f.getField().getName());
17 ilm 936
        final SQLTable table = getGraph().getTable();
937
 
938
        // create a flat list of the graph nodes, we just need the table, field count and the index
939
        // in this list of its linked table, eg for CPI -> LOCAL -> BATIMENT -> SITE :
940
        // <LOCAL,2,0>, <BATIMENT,2,0>, <SITE,5,1>, <CPI,4,0>
941
        final int graphSize = this.getGraph().getGraph().size();
942
        final List<GraphNode> l = new ArrayList<GraphNode>(graphSize);
83 ilm 943
        // check field names only once since each row has the same fields
944
        final AtomicInteger fieldIndex = new AtomicInteger(0);
17 ilm 945
        walk(0, new ITransformer<State<Integer>, Integer>() {
946
            @Override
947
            public Integer transformChecked(State<Integer> input) {
948
                final int index = l.size();
83 ilm 949
                final GraphNode node = new GraphNode(input);
950
                final int stop = fieldIndex.get() + node.getFieldCount();
951
                for (int i = fieldIndex.get(); i < stop; i++) {
952
                    if (i >= selectFieldsSize)
953
                        throw new IllegalStateException("Fields were removed from the select");
954
                    final FieldRef field = selectFields.get(i);
955
                    if (!node.getTable().equals(field.getTableRef().getTable()))
956
                        throw new IllegalStateException("Select field not in " + node + " : " + field);
957
                }
958
                fieldIndex.set(stop);
959
                l.add(node);
960
                // used by link index of GraphNode
17 ilm 961
                return index;
962
            }
963
        });
83 ilm 964
        // otherwise walk() would already have thrown an exception
965
        assert fieldIndex.get() <= selectFieldsSize;
966
        if (fieldIndex.get() != selectFieldsSize) {
967
            throw new IllegalStateException("Fields have been added to the select (which is useless, since only fields specified by rows are returned) : "
968
                    + selectFields.subList(fieldIndex.get(), selectFieldsSize));
969
        }
61 ilm 970
        assert l.size() == graphSize : "All nodes weren't explored once : " + l.size() + " != " + graphSize + "\n" + this.getGraph().printGraph();
17 ilm 971
 
25 ilm 972
        // if we wanted to use the cache, we'd need to copy the returned list and its items (i.e.
973
        // deepCopy()), since we modify them afterwards. Or perhaps include the code after this line
974
        // into the result set handler.
83 ilm 975
        final IResultSetHandler rsh = new IResultSetHandler(new RSH(selectFieldsNames, l), false);
21 ilm 976
        @SuppressWarnings("unchecked")
25 ilm 977
        final List<SQLRowValues> res = (List<SQLRowValues>) table.getBase().getDataSource().execute(req.asString(), rsh, false);
17 ilm 978
        // e.g. list of batiment pointing to site
979
        final List<SQLRowValues> merged = merge && this.fetchReferents() ? merge(res) : res;
980
        if (this.grafts.size() > 0) {
981
            for (final Entry<Path, Map<Path, SQLRowValuesListFetcher>> graftPlaceEntry : this.grafts.entrySet()) {
982
                // e.g. BATIMENT
983
                final Path graftPlace = graftPlaceEntry.getKey();
80 ilm 984
                final Path mapPath = Path.get(graftPlace.getLast());
17 ilm 985
                // list of BATIMENT to only fetch what's necessary
986
                final Set<Number> ids = new HashSet<Number>();
987
                // byRows is common to all grafts to support CPI -> LOCAL -> BATIMENT and RECEPTEUR
988
                // -> LOCAL -> BATIMENT (ie avoid duplicate LOCAL)
989
                // CollectionMap since the same row can be in multiple index of merged, e.g. when
990
                // fetching *BATIMENT* -> SITE each site will be repeated as many times as it has
991
                // children and if we want their DOSSIER they must be grafted on each line.
83 ilm 992
                final ListMap<Tuple2<Path, Number>, SQLRowValues> byRows = createCollectionMap();
17 ilm 993
                for (final SQLRowValues vals : merged) {
80 ilm 994
                    // can be empty when grafting on optional row
995
                    for (final SQLRowValues graftPlaceVals : vals.followPath(graftPlace, CreateMode.CREATE_NONE, false)) {
17 ilm 996
                        ids.add(graftPlaceVals.getIDNumber());
83 ilm 997
                        byRows.add(Tuple2.create(mapPath, graftPlaceVals.getIDNumber()), graftPlaceVals);
17 ilm 998
                    }
999
                }
1000
                assert ids.size() == byRows.size();
1001
                for (final Entry<Path, SQLRowValuesListFetcher> e : graftPlaceEntry.getValue().entrySet()) {
1002
                    // e.g BATIMENT <- LOCAL <- CPI
1003
                    final Path descendantPath = e.getKey();
1004
                    assert descendantPath.getFirst() == graftPlace.getLast() : descendantPath + " != " + graftPlace;
1005
                    final SQLRowValuesListFetcher graft = e.getValue();
1006
 
1007
                    final SQLSelect toRestore = graft.frozen;
1008
                    graft.frozen = new SQLSelect(graft.getReq()).andWhere(new Where(graft.getGraph().getTable().getKey(), ids));
1009
                    // don't merge then...
1010
                    final List<SQLRowValues> referentVals = graft.fetch(false);
1011
                    graft.frozen = toRestore;
1012
                    // ...but now
1013
                    this.merge(merged, referentVals, byRows, descendantPath);
1014
                }
1015
            }
1016
        }
1017
        return merged;
1018
    }
1019
 
1020
    // no need to set keep-alive too low, since on finalize() the pool shutdowns itself
1021
    private static final ExecutorService exec = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
1022
 
1023
    private static final class Linker implements Callable<Object> {
1024
 
1025
        private final List<GraphNode> l;
1026
        private final List<List<SQLRowValues>> rows;
1027
        private final int fromIndex;
1028
        private final int toIndex;
1029
 
1030
        public Linker(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int first, final int last) {
1031
            super();
1032
            this.l = l;
1033
            this.rows = rows;
1034
            this.fromIndex = first;
1035
            this.toIndex = last;
1036
        }
1037
 
1038
        @Override
1039
        public Object call() throws Exception {
1040
            link(this.l, this.rows, this.fromIndex, this.toIndex);
1041
            return null;
1042
        }
1043
 
1044
    }
1045
 
1046
    private static void link(final List<GraphNode> l, final List<List<SQLRowValues>> rows, final int start, final int stop) {
1047
        final int graphSize = l.size();
1048
        for (int nodeIndex = 1; nodeIndex < graphSize; nodeIndex++) {
1049
            final GraphNode node = l.get(nodeIndex);
1050
 
1051
            final String fromName = node.getFromName();
1052
            final int linkIndex = node.getLinkIndex();
1053
            final boolean backwards = node.isBackwards();
1054
 
1055
            for (int i = start; i < stop; i++) {
1056
                final List<SQLRowValues> row = rows.get(i);
1057
                final SQLRowValues creatingVals = row.get(nodeIndex);
1058
                // don't link empty values (LEFT JOIN produces rowValues filled with
1059
                // nulls) to the graph
83 ilm 1060
                if (creatingVals != null) {
17 ilm 1061
                    final SQLRowValues valsToFill;
1062
                    final SQLRowValues valsToPut;
1063
                    if (backwards) {
1064
                        valsToFill = creatingVals;
1065
                        valsToPut = row.get(linkIndex);
1066
                    } else {
1067
                        valsToFill = row.get(linkIndex);
1068
                        valsToPut = creatingVals;
1069
                    }
1070
 
1071
                    // check is done by updateLinks()
1072
                    valsToFill.put(fromName, valsToPut, false);
1073
                }
1074
            }
1075
        }
1076
    }
1077
 
1078
    /**
1079
     * Merge a list of fetched rowValues so that remove any duplicated rowValues. Eg, transforms
1080
     * this :
1081
     *
1082
     * <pre>
1083
     * BATIMENT[2]     LOCAL[2]        CPI_BT[3]
1084
     * BATIMENT[2]     LOCAL[2]        CPI_BT[2]
1085
     * BATIMENT[2]     LOCAL[3]
1086
     * BATIMENT[2]     LOCAL[5]        CPI_BT[5]
1087
     * BATIMENT[3]     LOCAL[4]        CPI_BT[4]
1088
     * BATIMENT[4]
1089
     * </pre>
1090
     *
1091
     * into this :
1092
     *
1093
     * <pre>
1094
     * BATIMENT[2]     LOCAL[2]        CPI_BT[3]
1095
     *                                 CPI_BT[2]
1096
     *                 LOCAL[3]
1097
     *                 LOCAL[5]        CPI_BT[5]
1098
     * BATIMENT[3]     LOCAL[4]        CPI_BT[4]
1099
     * BATIMENT[4]
1100
     * </pre>
1101
     *
1102
     * @param l a list of fetched rowValues.
1103
     * @return a smaller list in which all rowValues are unique.
1104
     */
1105
    private final List<SQLRowValues> merge(final List<SQLRowValues> l) {
1106
        return this.merge(l, l, null, this.descendantPath);
1107
    }
1108
 
1109
    /**
1110
     * Merge a list of rowValues and optionally graft it onto another one.
1111
     *
1112
     * @param tree the list receiving the graft.
1113
     * @param graft the list being merged and optionally grafted on <code>tree</code>, can be the
1114
     *        same as <code>tree</code>.
1115
     * @param graftPlaceRows if this is a graft the destination rowValues, otherwise
1116
     *        <code>null</code>, this instance will be modified.
1117
     * @param descendantPath the path to merge.
1118
     * @return the merged and grafted values.
1119
     */
83 ilm 1120
    private final List<SQLRowValues> merge(final List<SQLRowValues> tree, final List<SQLRowValues> graft, final ListMap<Tuple2<Path, Number>, SQLRowValues> graftPlaceRows, Path descendantPath) {
17 ilm 1121
        final boolean isGraft = graftPlaceRows != null;
1122
        assert (tree != graft) == isGraft : "Trying to graft onto itself";
1123
        final List<SQLRowValues> res = isGraft ? tree : new ArrayList<SQLRowValues>();
1124
        // so that every graft is actually grafted onto the tree
83 ilm 1125
        final ListMap<Tuple2<Path, Number>, SQLRowValues> map = isGraft ? graftPlaceRows : createCollectionMap();
17 ilm 1126
 
1127
        final int stop = descendantPath.length();
1128
        for (final SQLRowValues v : graft) {
1129
            boolean doAdd = true;
61 ilm 1130
            SQLRowValues previous = null;
1131
            for (int i = stop; i >= 0 && doAdd; i--) {
17 ilm 1132
                final Path subPath = descendantPath.subPath(0, i);
1133
                final SQLRowValues desc = v.followPath(subPath);
1134
                if (desc != null) {
1135
                    final Tuple2<Path, Number> row = Tuple2.create(subPath, desc.getIDNumber());
1136
                    if (map.containsKey(row)) {
1137
                        doAdd = false;
83 ilm 1138
                        assert map.get(row).get(0).getFields().containsAll(desc.getFields()) : "Discarding an SQLRowValues with more fields : " + desc;
17 ilm 1139
                        // previous being null can happen when 2 grafted paths share some steps at
1140
                        // the start, e.g. SOURCE -> LOCAL and CPI -> LOCAL with a LOCAL having a
1141
                        // SOURCE but no CPI
1142
                        if (previous != null) {
83 ilm 1143
                            final List<SQLRowValues> destinationRows = map.get(row);
17 ilm 1144
                            final int destinationSize = destinationRows.size();
1145
                            assert destinationSize > 0 : "Map contains row but have no corresponding value: " + row;
1146
                            final String ffName = descendantPath.getSingleStep(i).getName();
67 ilm 1147
                            // avoid the first deepCopy() (needed since rows of 'previous' have
1148
                            // already been added to 'map') and copy before merging
17 ilm 1149
                            for (int j = 1; j < destinationSize; j++) {
67 ilm 1150
                                final SQLRowValues previousCopy = previous.deepCopy().put(ffName, destinationRows.get(j));
1151
                                // put the copied rowValues into 'map' otherwise they'd be
1152
                                // unreachable and thus couldn't have referents. Tested by
1153
                                // SQLRowValuesListFetcherTest.testSameReferentMergedMultipleTimes()
1154
                                // i+1 since we start from 'previous' not 'desc'
1155
                                for (int k = stop; k >= i + 1; k--) {
1156
                                    final SQLRowValues descCopy = previousCopy.followPath(descendantPath.subPath(i + 1, k));
1157
                                    if (descCopy != null) {
1158
                                        final Tuple2<Path, Number> rowCopy = Tuple2.create(descendantPath.subPath(0, k), descCopy.getIDNumber());
1159
                                        assert map.containsKey(rowCopy) : "Since we already iterated with i";
83 ilm 1160
                                        map.add(rowCopy, descCopy);
67 ilm 1161
                                    }
1162
                                }
17 ilm 1163
                            }
67 ilm 1164
                            // don't call map.put() it has already been handled below
17 ilm 1165
                            previous.put(ffName, destinationRows.get(0));
1166
                        }
1167
                    } else {
83 ilm 1168
                        map.add(row, desc);
17 ilm 1169
                    }
1170
                    previous = desc;
1171
                }
1172
            }
1173
            if (doAdd) {
1174
                assert !isGraft : "Adding graft values as tree values";
1175
                res.add(v);
1176
            }
1177
        }
1178
        return res;
1179
    }
1180
 
1181
    @Override
1182
    public String toString() {
1183
        return this.getClass().getSimpleName() + " for " + this.getGraph() + " with " + this.getSelID() + " and " + this.getSelTransf();
1184
    }
1185
 
1186
    @Override
1187
    public boolean equals(Object obj) {
1188
        if (obj instanceof SQLRowValuesListFetcher) {
1189
            final SQLRowValuesListFetcher o = (SQLRowValuesListFetcher) obj;
1190
            // use getReq() to avoid selTransf equality pb (ie we generally use anonymous classes
1191
            // which thus lack equals())
1192
            return this.getReq().equals(o.getReq()) && CompareUtils.equals(this.descendantPath, o.descendantPath) && this.grafts.equals(o.grafts);
1193
        } else
1194
            return false;
1195
    }
1196
 
1197
    @Override
1198
    public int hashCode() {
1199
        return this.getReq().hashCode();
1200
    }
1201
}