OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
132 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.
132 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.element;
15
 
16
import org.openconcerto.sql.element.SQLElement.ReferenceAction;
17
import org.openconcerto.sql.model.SQLField;
182 ilm 18
import org.openconcerto.sql.model.SQLFieldRowProcessor;
19
import org.openconcerto.sql.model.SQLResultSet;
20
import org.openconcerto.sql.model.SQLRow;
21
import org.openconcerto.sql.model.SQLSelect;
22
import org.openconcerto.sql.model.SQLSyntax;
23
import org.openconcerto.sql.model.SQLTable;
24
import org.openconcerto.sql.model.SQLType;
25
import org.openconcerto.sql.model.Where;
132 ilm 26
import org.openconcerto.sql.model.graph.Link;
27
import org.openconcerto.sql.model.graph.Link.Direction;
28
import org.openconcerto.sql.model.graph.Path;
29
import org.openconcerto.sql.model.graph.Step;
182 ilm 30
import org.openconcerto.utils.CollectionUtils;
132 ilm 31
 
182 ilm 32
import java.sql.ResultSet;
33
import java.sql.SQLException;
34
import java.util.ArrayList;
35
import java.util.Collection;
36
import java.util.Collections;
37
import java.util.HashMap;
132 ilm 38
import java.util.List;
182 ilm 39
import java.util.Map;
40
import java.util.Objects;
41
import java.util.Set;
132 ilm 42
 
182 ilm 43
import org.apache.commons.dbutils.ResultSetHandler;
44
 
45
import net.jcip.annotations.Immutable;
46
 
132 ilm 47
/**
48
 * A logical link between two elements. It can be a direct foreign {@link Link} or two links through
49
 * a {@link JoinSQLElement join}. The {@link #getOwner()} needs the {@link #getOwned()}, i.e. if the
50
 * owner is unarchived the owned will also be unarchived. The owner is responsible for
51
 * {@link SQLElement#setupLinks(org.openconcerto.sql.element.SQLElement.LinksSetup) setting up} the properties of
52
 * the link.
53
 *
54
 * @author Sylvain
55
 */
56
public final class SQLElementLink {
57
 
58
    public static enum LinkType {
59
        /** One element is the parent of the other */
60
        PARENT,
61
        /** One element is part of the other */
62
        COMPOSITION,
63
        /** One element references the other */
64
        ASSOCIATION
65
    }
66
 
67
    private final SQLElement owner, owned;
68
    private final Path path;
69
    private final LinkType type;
70
    private final String name;
144 ilm 71
    // TODO final (see #setAction())
132 ilm 72
    private ReferenceAction action;
73
 
74
    protected SQLElementLink(SQLElement owner, Path path, SQLElement owned, final LinkType type, final String name, final ReferenceAction action) {
75
        super();
76
        final int length = path.length();
77
        if (length == 0)
78
            throw new IllegalArgumentException("Empty path");
79
        if (owner != null && owner.getTable() != path.getFirst() || owned.getTable() != path.getLast())
80
            throw new IllegalArgumentException("Wrong path : " + path + " not from owner : " + owner + " to owned : " + owned);
81
        if (!path.isSingleLink())
82
            throw new IllegalArgumentException("Isn't single link : " + path);
83
        // either foreign key or join
84
        if (length > 2)
85
            throw new IllegalArgumentException("Path too long : " + path);
86
        final Step lastStep = path.getStep(-1);
87
        final boolean endsWithForeign = lastStep.getDirection().equals(Direction.FOREIGN);
88
        if (length == 1) {
89
            if (!endsWithForeign)
90
                throw new IllegalArgumentException("Single step path isn't foreign : " + path);
91
        } else {
92
            assert length == 2;
93
            if (!endsWithForeign || !path.getStep(0).getDirection().equals(Direction.REFERENT))
94
                throw new IllegalArgumentException("Two steps path isn't a join : " + path);
95
        }
96
        if (lastStep.getSingleField() == null)
97
            throw new IllegalArgumentException("Multi-field not yet supported : " + lastStep);
98
        this.path = path;
99
        this.owner = owner;
100
        this.owned = owned;
101
        if (type == null || action == null)
102
            throw new NullPointerException();
103
        this.type = type;
104
        if (name != null) {
105
            this.name = name;
144 ilm 106
        } else if (length == 1) {
132 ilm 107
            final SQLField singleField = lastStep.getSingleField();
108
            this.name = singleField != null ? singleField.getName() : lastStep.getSingleLink().getName();
144 ilm 109
        } else {
110
            this.name = lastStep.getFrom().getName();
132 ilm 111
        }
112
        assert this.getName() != null;
113
        this.action = action;
114
    }
115
 
116
    /**
117
     * The path from the {@link #getOwner() owner} to the {@link #getOwned() owned}. NOTE : the last
118
     * step is always {@link Direction#FOREIGN}.
119
     *
120
     * @return the path of this link, its {@link Path#length() length} is 1 or 2 for a join.
121
     */
122
    public final Path getPath() {
123
        return this.path;
124
    }
125
 
126
    public final boolean isJoin() {
127
        return this.path.length() == 2;
128
    }
129
 
130
    /**
131
     * Return the single link.
132
     *
133
     * @return the single foreign link, <code>null</code> if and only if this is a {@link #isJoin()
134
     *         join} as multi-link paths are invalid.
135
     */
136
    public final Link getSingleLink() {
137
        if (this.isJoin())
138
            return null;
139
        final Link res = this.path.getStep(0).getSingleLink();
140
        // checked in the constructor
141
        assert res != null;
142
        return res;
143
    }
144
 
145
    /**
146
     * Return the single field of this link.
147
     *
148
     * @return the foreign field of this link, <code>null</code> if and only if this is a
149
     *         {@link #isJoin() join} as multi-field link are not yet supported.
150
     */
151
    public final SQLField getSingleField() {
152
        final Link l = this.getSingleLink();
153
        if (l == null)
154
            return null;
155
        final SQLField res = l.getSingleField();
156
        // checked in the constructor
157
        assert res != null;
158
        return res;
159
    }
160
 
161
    public final SQLElement getOwner() {
162
        return this.owner;
163
    }
164
 
165
    public final SQLElement getOwned() {
166
        return this.owned;
167
    }
168
 
169
    public final JoinSQLElement getJoinElement() {
170
        if (!this.isJoin())
171
            return null;
172
        return (JoinSQLElement) this.getOwner().getElement(this.getPath().getTable(1));
173
    }
174
 
175
    public final boolean isOwnerTheParent() {
176
        final boolean owner;
177
        if (this.getLinkType().equals(LinkType.COMPOSITION))
178
            owner = true;
179
        else if (this.getLinkType().equals(LinkType.PARENT))
180
            owner = false;
181
        else
182
            throw new IllegalStateException("Invalid type : " + this.getLinkType());
183
        return owner;
184
    }
185
 
186
    public final SQLElement getParent() {
187
        return this.getParentOrChild(true);
188
    }
189
 
190
    private final SQLElement getParentOrChild(final boolean parent) {
191
        return parent == isOwnerTheParent() ? this.getOwner() : this.getOwned();
192
    }
193
 
194
    public final SQLElement getChild() {
195
        return this.getParentOrChild(false);
196
    }
197
 
198
    public final Path getPathToParent() {
199
        return this.getPathToParentOrChild(true);
200
    }
201
 
202
    public final Step getStepToParent() {
203
        return this.getPathToParent().getStep(-1);
204
    }
205
 
206
    private final Path getPathToParentOrChild(final boolean toParent) {
207
        return toParent == isOwnerTheParent() ? this.getPath().reverse() : this.getPath();
208
    }
209
 
210
    public final Path getPathToChild() {
211
        return this.getPathToParentOrChild(false);
212
    }
213
 
214
    public final Step getStepToChild() {
215
        return this.getPathToChild().getStep(-1);
216
    }
217
 
182 ilm 218
    public final List<SQLRow> getRowsUntilRoot(final Number id) {
219
        return this.getRowsUntilRoot(id, null).getRows();
220
    }
221
 
222
    /**
223
     * Get all rows above the passed row (including it).
224
     *
225
     * @param id the first row.
226
     * @param fields which fields to fetch, <code>null</code> to fetch all.
227
     * @return all rows from the passed one to the root.
228
     */
229
    public final RecursiveRows getRowsUntilRoot(final Number id, Set<String> fields) {
230
        return getRecursiveRows(true, id, fields, -1, null);
231
    }
232
 
233
    public final List<SQLRow> getSubTreeRows(final Number id) {
234
        return this.getSubTreeRows(id, null).getRows();
235
    }
236
 
237
    public final RecursiveRows getSubTreeRows(final Number id, final Set<String> fields) {
238
        return getSubTreeRows(id, fields, -1);
239
    }
240
 
241
    /**
242
     * Get all rows beneath the passed root (including it).
243
     *
244
     * @param id the root row.
245
     * @param fields which fields to fetch, <code>null</code> to fetch all.
246
     * @param maxLevel the max number of times to go through the link.
247
     * @return all rows are deterministically ordered (by level, parent order, order ; i.e. root
248
     *         first).
249
     */
250
    public final RecursiveRows getSubTreeRows(final Number id, final Set<String> fields, final int maxLevel) {
251
        return getRecursiveRows(false, id, fields, maxLevel, getOwned().getTable().getOrderField());
252
    }
253
 
254
    static private final String findUnusedName(final Collection<String> usedNames, final String base) {
255
        String res = base;
256
        int i = 0;
257
        while (usedNames.contains(res)) {
258
            res = base + i++;
259
        }
260
        return res;
261
    }
262
 
263
    @Immutable
264
    static public final class RecursiveRows {
265
 
266
        static public final RecursiveRows ZERO_LEVEL = new RecursiveRows(0, Collections.emptyList(), Collections.emptyMap());
267
 
268
        private final int maxLevel;
269
        private final List<SQLRow> rows;
270
        private final Map<SQLRow, List<Number>> cycles;
271
 
272
        RecursiveRows(final int maxLevel, final List<SQLRow> rows, final Map<SQLRow, List<Number>> cycles) {
273
            super();
274
            this.maxLevel = maxLevel;
275
            this.rows = Collections.unmodifiableList(rows);
276
            // OK since List<Number> are already immutable
277
            this.cycles = Collections.unmodifiableMap(cycles);
278
        }
279
 
280
        public final int getMaxLevelRequested() {
281
            return this.maxLevel;
282
        }
283
 
284
        public final List<SQLRow> getRows() {
285
            if (this.getCycles().isEmpty())
286
                return this.getPartialRows();
287
            else
288
                throw new IllegalStateException("Cycle detected : " + this.getCycles());
289
        }
290
 
291
        public final List<SQLRow> getPartialRows() {
292
            return this.rows;
293
        }
294
 
295
        public final Map<SQLRow, List<Number>> getCycles() {
296
            return this.cycles;
297
        }
298
    }
299
 
300
    private final RecursiveRows getRecursiveRows(final boolean foreign, final Number id, Set<String> fields, final int maxLevel, final SQLField orderField) {
301
        if (this.getOwner() != this.getOwned() || this.isJoin())
302
            throw new IllegalStateException("Not a recurive link : " + this);
303
        final SQLTable t = getOwned().getTable();
304
        final Link singleLink = this.getSingleLink();
305
        final SQLField singleField = singleLink.getSingleField();
306
        if (singleField == null)
307
            throw new UnsupportedOperationException("Multiple fields not yet supported : " + singleLink);
308
        Objects.requireNonNull(id, "id is null");
309
 
310
        if (maxLevel == 0)
311
            return RecursiveRows.ZERO_LEVEL;
312
 
313
        final SQLSyntax syntax = t.getDBSystemRoot().getSyntax();
314
 
315
        if (fields == null)
316
            fields = t.getFieldsName();
317
        final String recursiveT = "recT";
318
        // use array to prevent infinite loop
319
        final String visitedIDsF = findUnusedName(fields, "visitedIDs");
320
        final String visitedIDsRef = recursiveT + '.' + visitedIDsF;
321
        final String visitedIDsCount = syntax.getSQLArrayLength(visitedIDsRef);
322
        // boolean to know about endless loops : we don't stop before visiting a row a second time,
323
        // but just after
324
        final String loopF = findUnusedName(fields, "loop");
325
 
326
        // firstly visitedIDsF, secondly optional order, then the asked fields
327
        final SQLSelect selNonRec = new SQLSelect();
328
        selNonRec.addRawSelect(syntax.getSQLArray(Collections.singletonList(t.getKey().getFieldRef())), visitedIDsF);
329
        selNonRec.addRawSelect(SQLType.getBoolean(syntax).toString(Boolean.FALSE), loopF);
330
        final boolean useOrder = !foreign && orderField != null;
331
        if (useOrder)
332
            selNonRec.addRawSelect(syntax.cast("null", orderField.getType().getJavaType()), "parentOrder");
333
        selNonRec.addAllSelect(t, fields);
334
        if (!fields.contains(singleField.getName()))
335
            selNonRec.addSelect(singleField);
336
        // need PK for SQLRow
337
        if (!fields.contains(t.getKey().getName()))
338
            selNonRec.addSelect(t.getKey());
339
        selNonRec.setWhere(new Where(t.getKey(), "=", id));
340
 
341
        // recursive SELECT
342
        final StringBuilder recSelect = new StringBuilder("SELECT ");
343
        recSelect.append(syntax.getSQLArrayAppend(visitedIDsRef, t.getKey().getFieldRef())).append(", ");
344
        recSelect.append(syntax.getSQLArrayContains(visitedIDsRef, t.getKey().getFieldRef())).append(", ");
345
        final int index;
346
        if (useOrder) {
347
            recSelect.append(recursiveT).append('.').append(orderField.getName()).append(", ");
348
            index = 3;
349
        } else {
350
            index = 2;
351
        }
352
        recSelect.append(CollectionUtils.join(selNonRec.getSelect().subList(index, selNonRec.getSelect().size()), ", "));
353
        recSelect.append("\nFROM ").append(t.getSQLName().quote()).append(", ").append(recursiveT);
354
        recSelect.append("\nWHERE ");
355
        if (foreign) {
356
            recSelect.append(t.getKey().getFieldRef()).append(" = ").append(recursiveT + '.' + singleField.getName());
357
        } else {
358
            recSelect.append(singleField.getFieldRef()).append(" = ").append(recursiveT + '.' + t.getKey().getName());
359
        }
360
        // avoid infinite loop
361
        recSelect.append(" and not (").append(recursiveT).append('.').append(loopF).append(')');
362
        if (t.getUndefinedIDNumber() != null) {
363
            recSelect.append(" and ").append(new Where(t.getKey(), "!=", t.getUndefinedID()).getClause());
364
        }
365
        if (maxLevel > 0) {
366
            recSelect.append(" and ").append(visitedIDsCount).append(" < ").append(maxLevel);
367
        }
368
 
369
        String cte = "with recursive " + recursiveT + "(" + CollectionUtils.join(selNonRec.getSelectNames(), ", ") + ") as (\n" + selNonRec.asString() + "\nunion all\n" + recSelect
370
                + ")\nSELECT * from " + recursiveT + " ORDER BY " + visitedIDsCount;
371
        if (useOrder) {
372
            cte += ", 2, " + recursiveT + '.' + orderField.getName();
373
        }
374
 
375
        final List<String> rsNames = new ArrayList<>(selNonRec.getSelectNames());
376
        // int[] visited IDs
377
        rsNames.set(0, null);
378
        // boolean loop
379
        rsNames.set(1, null);
380
        if (useOrder)
381
            rsNames.set(2, null);
382
 
383
        final List<SQLRow> res = new ArrayList<>();
384
        final Map<SQLRow, List<Number>> cycleRows = new HashMap<>();
385
        final Class<? extends Number> keyType = t.getKey().getType().getJavaType().asSubclass(Number.class);
386
        t.getDBSystemRoot().getDataSource().execute(cte, new ResultSetHandler() {
387
            @Override
388
            public Object handle(ResultSet rs) throws SQLException {
389
                final SQLFieldRowProcessor rowProc = new SQLFieldRowProcessor(t, rsNames);
390
                while (rs.next()) {
391
                    final SQLRow row = SQLRow.createFromRS(t, rs, rowProc, true);
392
                    final boolean looped = rs.getBoolean(2);
393
                    if (looped) {
394
                        cycleRows.put(row, SQLResultSet.getList(rs, 1, keyType));
395
                    } else {
396
                        res.add(row);
397
                    }
398
                }
399
                return null;
400
            }
401
        });
402
        return new RecursiveRows(maxLevel, res, cycleRows);
403
    }
404
 
132 ilm 405
    public final LinkType getLinkType() {
406
        return this.type;
407
    }
408
 
409
    public final String getName() {
410
        return this.name;
411
    }
412
 
413
    public final ReferenceAction getAction() {
414
        return this.action;
415
    }
416
 
144 ilm 417
    // use SQLElementLinkSetup
418
    @Deprecated
132 ilm 419
    public final void setAction(ReferenceAction action) {
420
        final List<ReferenceAction> possibleActions = getOwner().getPossibleActions(this.getLinkType(), this.getOwned());
421
        if (!possibleActions.contains(action))
422
            throw new IllegalArgumentException("Not allowed : " + action);
423
        this.action = action;
424
    }
425
 
426
    @Override
427
    public int hashCode() {
428
        final int prime = 31;
429
        int result = 1;
430
        result = prime * result + this.action.hashCode();
431
        result = prime * result + this.path.hashCode();
432
        result = prime * result + this.type.hashCode();
433
        return result;
434
    }
435
 
436
    // don't use SQLElement to avoid walking the graph
437
    @Override
438
    public boolean equals(Object obj) {
439
        if (this == obj)
440
            return true;
441
        if (obj == null)
442
            return false;
443
        if (getClass() != obj.getClass())
444
            return false;
445
        final SQLElementLink other = (SQLElementLink) obj;
446
        return this.action.equals(other.action) && this.path.equals(other.path) && this.type.equals(other.type);
447
    }
448
 
449
    @Override
450
    public String toString() {
451
        return this.getClass().getSimpleName() + " '" + this.getName() + "' " + this.getLinkType() + " " + this.getPath();
452
    }
453
}