OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | 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
 /*
15
 * SQLRow created on 20 mai 2004
16
 */
17
package org.openconcerto.sql.model;
18
 
19
import org.openconcerto.sql.Log;
20
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
142 ilm 21
import org.openconcerto.sql.model.SQLSelect.LockStrength;
132 ilm 22
import org.openconcerto.sql.model.SQLTable.VirtualFields;
17 ilm 23
import org.openconcerto.sql.model.graph.Link;
67 ilm 24
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 25
import org.openconcerto.sql.model.graph.Path;
182 ilm 26
import org.openconcerto.utils.CollectionUtils;
83 ilm 27
import org.openconcerto.utils.ListMap;
93 ilm 28
import org.openconcerto.utils.SetMap;
156 ilm 29
import org.openconcerto.utils.Tuple2.List2;
17 ilm 30
 
31
import java.sql.ResultSet;
32
import java.sql.ResultSetMetaData;
33
import java.sql.SQLException;
34
import java.util.ArrayList;
35
import java.util.Arrays;
36
import java.util.Collection;
37
import java.util.Collections;
38
import java.util.HashMap;
39
import java.util.HashSet;
40
import java.util.Iterator;
41
import java.util.LinkedHashSet;
42
import java.util.List;
43
import java.util.Map;
93 ilm 44
import java.util.Map.Entry;
17 ilm 45
import java.util.Set;
182 ilm 46
import java.util.function.BiFunction;
132 ilm 47
import java.util.logging.Level;
17 ilm 48
 
49
import org.apache.commons.dbutils.ResultSetHandler;
50
 
51
/**
52
 * Une ligne d'une table. Cette classe décrit une ligne et ne représente pas exactement une ligne
53
 * réelle, il n'y a pas unicité (cela reviendrait à recréer la base en Java !). Pour charger les
54
 * valeurs depuis la base manuellement à tout moment utiliser fetchValues(), cette méthode est
55
 * appelée automatiquement si nécessaire. Les valeurs des champs sont stockées, ainsi toutes les
56
 * méthodes renvoient l'état de la ligne réelle au moment du dernier fetchValues().
57
 * <p>
58
 * Une ligne peut ne pas exister ou être archivée, de plus elle peut ne pas contenir tous les champs
59
 * de la table. Pour accéder à la valeur des champs il existe getString() et getInt(), pour des
60
 * demandes plus complexes passer par getObject(). Si un champ qui n'est pas dans la ligne est
61
 * demandé, un fetchValues() est automatiquement fait.
62
 * </p>
63
 * <p>
64
 * On peut obtenir un ligne en la demandant à sa table, mais si l'on souhaite une SQLRow décrivant
65
 * une ligne n'existant pas dans la base il faut passer par le constructeur.
66
 * </p>
67
 *
68
 * @author ILM Informatique 20 mai 2004
69
 * @see #isValid()
70
 * @see #getObject(String)
71
 * @see org.openconcerto.sql.model.SQLTable#getRow(int)
72
 */
73
public class SQLRow extends SQLRowAccessor {
74
 
75
    /**
76
     * Each table must have a row with this ID, that others refer to to indicate the absence of a
77
     * link.
78
     *
79
     * @deprecated use either {@link SQLRowAccessor#isForeignEmpty(String)} /
80
     *             {@link SQLRowValues#putEmptyLink(String)} or if you must
81
     *             {@link SQLTable#getUndefinedID()}
82
     */
83
    public static final int UNDEFINED_ID = 1;
84
    /**
85
     * No valid database rows should have an ID thats less than MIN_VALID_ID. But remember, you CAN
86
     * have a SQLRow with any ID.
87
     */
88
    public static final int MIN_VALID_ID = 0;
89
    /** Value representing no ID, no table can have a row with this ID. */
90
    public static final int NONEXISTANT_ID = MIN_VALID_ID - 1;
91
    /** <code>true</code> to print a stack trace when fetching missing values */
92
    public static final boolean printSTForMissingField = false;
93
 
94
    /**
95
     * Crée une ligne avec les valeurs du ResultSet.
96
     *
97
     * @param table la table de la ligne.
98
     * @param rs les valeurs.
99
     * @param onlyTable pass <code>true</code> if <code>rs</code> only contains columns from
100
     *        <code>table</code>, if unsure pass <code>false</code>. This allows to avoid calling
101
     *        {@link ResultSetMetaData#getTableName(int)} which is expensive on some systems.
102
     * @return la ligne correspondante.
103
     * @throws SQLException si problème lors de l'accès au ResultSet.
104
     * @see SQLRow#SQLRow(SQLTable, Map)
105
     * @deprecated use {@link SQLRowListRSH} or {@link SQLRowValuesListFetcher} instead or if you
106
     *             must use a {@link ResultSet} call
182 ilm 107
     *             {@link #createListFromRS(SQLTable, ResultSet, boolean)} thus avoiding the
108
     *             potentially costly {@link ResultSet#getMetaData()}
17 ilm 109
     */
110
    public static final SQLRow createFromRS(SQLTable table, ResultSet rs, final boolean onlyTable) throws SQLException {
111
        return createFromRS(table, rs, rs.getMetaData(), onlyTable);
112
    }
113
 
182 ilm 114
    // see createListFromRS()
115
    @Deprecated
17 ilm 116
    public static final SQLRow createFromRS(SQLTable table, ResultSet rs, final ResultSetMetaData rsmd, final boolean onlyTable) throws SQLException {
182 ilm 117
        return createFromRS(table, rs, new SQLFieldRowProcessor(table, getFieldNames(table, rsmd, onlyTable)), true);
17 ilm 118
    }
119
 
120
    private static final List<String> getFieldNames(SQLTable table, final ResultSetMetaData rsmd, final boolean tableOnly) throws SQLException {
121
        final int colCount = rsmd.getColumnCount();
122
        final List<String> names = new ArrayList<String>(colCount);
123
        for (int i = 1; i <= colCount; i++) {
124
            // n'inclure que les colonnes de la table demandée
125
            // use a boolean since some systems (eg pg) require a request to the db to return the
126
            // table name
127
            if (tableOnly || rsmd.getTableName(i).equals(table.getName())) {
128
                names.add(rsmd.getColumnName(i));
129
            } else {
130
                names.add(null);
131
            }
132
        }
133
 
134
        return names;
135
    }
136
 
182 ilm 137
    // ATTN doesn't check that names are fields of table
138
    public static final SQLRow createFromRS(SQLTable table, ResultSet rs, final SQLFieldRowProcessor rowProc, final boolean immutable) throws SQLException {
139
        final Map<String, Object> m = rowProc.toMap(rs);
17 ilm 140
 
83 ilm 141
        final Number id = getID(m, table, true);
142
        // e.g. LEFT JOIN : missing values are null
143
        if (id == null)
144
            return null;
145
 
146
        // pass already found ID
182 ilm 147
        final SQLRow res = new SQLRow(table, id, Collections.unmodifiableMap(m));
148
        if (immutable)
149
            res.freeze();
150
        return res;
17 ilm 151
    }
152
 
153
    /**
154
     * Create a list of rows using the metadata to find the columns' names.
155
     *
156
     * @param table the table of the rows.
157
     * @param rs the result set.
158
     * @param tableOnly <code>true</code> if <code>rs</code> only contains columns from
159
     *        <code>table</code>.
160
     * @return the data of the result set as SQLRows.
161
     * @throws SQLException if an error occurs while reading <code>rs</code>.
162
     */
163
    public static final List<SQLRow> createListFromRS(SQLTable table, ResultSet rs, final boolean tableOnly) throws SQLException {
182 ilm 164
        return createListFromRS(table, rs, getFieldNames(table, rs.getMetaData(), tableOnly), true);
17 ilm 165
    }
166
 
167
    /**
168
     * Create a list of rows without using the metadata.
169
     *
170
     * @param table the table of the rows.
171
     * @param rs the result set.
172
     * @param names the name of the field for each column, nulls are ignored, e.g. ["DESIGNATION",
173
     *        null, "ID"].
182 ilm 174
     * @param immutable <code>true</code> if the result should be immutable.
17 ilm 175
     * @return the data of the result set as SQLRows.
176
     * @throws SQLException if an error occurs while reading <code>rs</code>.
177
     */
182 ilm 178
    static final List<SQLRow> createListFromRS(SQLTable table, ResultSet rs, final List<String> names, final boolean immutable) throws SQLException {
17 ilm 179
        final List<SQLRow> res = new ArrayList<SQLRow>();
182 ilm 180
        final SQLFieldRowProcessor rowProc = new SQLFieldRowProcessor(table, names);
17 ilm 181
        while (rs.next()) {
182 ilm 182
            final SQLRow row = createFromRS(table, rs, rowProc, immutable);
83 ilm 183
            if (row != null)
184
                res.add(row);
17 ilm 185
        }
182 ilm 186
        return immutable ? Collections.unmodifiableList(res) : res;
17 ilm 187
    }
188
 
142 ilm 189
    static final SQLRow createFromSelect(final SQLTable t, final VirtualFields vfs, final int id, final LockStrength l) {
190
        final SQLSelect sel = new SQLSelect(true).addAllSelect(t.getFields(vfs));
191
        sel.setLockStrength(l);
192
        sel.setWhere(new Where(t.getKey(), "=", id));
144 ilm 193
        @SuppressWarnings("unchecked")
182 ilm 194
        final Map<String, Object> map = (Map<String, Object>) t.getDBSystemRoot().getDataSource().execute(sel.asString(),
195
                new IResultSetHandler(SQLDataSource.MAP_HANDLER, l.equals(LockStrength.NONE)));
196
        return new SQLRow(t, id, map == null ? null : Collections.unmodifiableMap(map));
142 ilm 197
    }
198
 
199
    /**
200
     * Create an empty existing row (without checking the DB).
201
     *
202
     * @param t the table.
203
     * @param id the ID.
182 ilm 204
     * @return a new {@link #exists() existing} {@link #isFilled() filled} {@link #isFrozen()
205
     *         frozen} {@link #getFields() empty} row.
142 ilm 206
     */
182 ilm 207
    public static final SQLRow createEmpty(final SQLTable t, final int id) {
208
        final SQLRow res = new SQLRow(t, id, Collections.<String, Object> emptyMap());
209
        res.freeze();
210
        return res;
142 ilm 211
    }
212
 
182 ilm 213
    public static final <R extends SQLRowAccessor, A> SQLRow trim(final R r, final BiFunction<? super R, ? super A, ? extends Map<String, Object>> getVals, final A arg) {
214
        final SQLRow res = new SQLRow(r.getTable(), null, CollectionUtils.toImmutableMap(getVals.apply(r, arg)));
215
        res.freeze();
216
        return res;
217
    }
218
 
17 ilm 219
    private final int ID;
220
    private final Number idNumber;
221
    private Map<String, Object> values;
222
    private boolean fetched;
182 ilm 223
    private boolean frozen = false;
17 ilm 224
 
225
    private SQLRow(SQLTable table, Number id) {
226
        super(table);
227
        this.fetched = false;
228
        this.ID = id.intValue();
229
        this.idNumber = id;
230
        this.checkTable();
231
    }
232
 
233
    // public pour pouvoir créer une ligne n'exisant pas
234
    public SQLRow(SQLTable table, int ID) {
235
        // have to cast to Number, if you use Integer.valueOf() (or cast to Integer) the resulting
236
        // Integer is converted to Long
237
        this(table, table.getKey().getType().getJavaType() == Integer.class ? (Number) ID : Long.valueOf(ID));
238
    }
239
 
240
    private void checkTable() {
241
        if (!this.getTable().isRowable())
242
            throw new IllegalArgumentException(this.getTable() + " is not rowable");
243
    }
244
 
245
    /**
246
     * Crée une ligne avec les valeurs fournies. Evite une requête à la base.
247
     *
248
     * @param table la table.
249
     * @param values les valeurs de la lignes.
250
     * @throws IllegalArgumentException si values ne contient pas la clef de la table.
251
     */
19 ilm 252
    public SQLRow(SQLTable table, Map<String, ?> values) {
182 ilm 253
        this(table, null, values == null ? null : CollectionUtils.toImmutableMap(values));
83 ilm 254
    }
255
 
256
    // allow to call getID() only once
182 ilm 257
    // Private since it doesn't make sure the map is immutable.
258
    private SQLRow(SQLTable table, final Number id, Map<String, Object> values) {
83 ilm 259
        this(table, id == null ? getID(values, table, false) : id);
182 ilm 260
        this.setValues(values);
17 ilm 261
    }
262
 
83 ilm 263
    // return ID, must always be present but may be null if <code>nullAllowed</code>
264
    private static Number getID(Map<String, ?> values, final SQLTable table, final boolean nullAllowed) {
17 ilm 265
        final String keyName = table.getKey().getName();
83 ilm 266
        if (!values.containsKey(keyName))
17 ilm 267
            throw new IllegalArgumentException(values + " does not contain the key of " + table);
83 ilm 268
        final Object keyValue = values.get(keyName);
269
        if (keyValue instanceof Number) {
270
            return (Number) keyValue;
271
        } else if (nullAllowed && keyValue == null) {
272
            return null;
273
        } else {
274
            final String valS = keyValue == null ? "' is null" : "' isn't a Number : " + keyValue.getClass() + " " + keyValue;
275
            throw new IllegalArgumentException("The value of '" + keyName + valS);
276
        }
17 ilm 277
    }
278
 
182 ilm 279
    public final SQLRow copy(final boolean freeze) {
280
        final SQLRow res = new SQLRow(this.getTable(), this.getIDNumber());
281
        if (this.isFilled())
282
            // safe to share map, since it is immutable
283
            res.setValues(this.getValues());
284
        assert res.isFilled() == this.isFilled();
285
        if (freeze)
286
            res.freeze();
287
        assert res.isFrozen() == freeze;
288
        return res;
289
    }
290
 
291
    @Override
292
    public final boolean isFrozen() {
293
        return this.frozen;
294
    }
295
 
296
    private void checkFrozen() {
297
        if (this.isFrozen())
298
            throw new IllegalStateException("SQLRow is not modifiable");
299
    }
300
 
132 ilm 301
    /**
182 ilm 302
     * Freeze this instance so that no modification can be made. Once this method returns, this
303
     * instance can be safely published (e.g. stored into a field that is properly guarded by a
304
     * lock) to other threads without further synchronizations.
305
     *
306
     * @return <code>true</code> if this call changed the frozen status.
307
     */
308
    public final boolean freeze() {
309
        if (this.frozen)
310
            return false;
311
        this.frozen = true;
312
        return true;
313
    }
314
 
315
    @Override
316
    public final SQLRow toImmutable() {
317
        if (this.isFrozen())
318
            return this;
319
        return this.copy(true);
320
    }
321
 
322
    /**
132 ilm 323
     * Whether this contains values or just the {@link #getIDNumber() id}. NOTE that
324
     * {@link #getObject(String)} (and thus any other methods that call it) will access the DB if
325
     * the requested field is {@link #getFields() missing} even if this returns <code>true</code>.
326
     *
327
     * @return <code>true</code> if {@link #exists()} and {@link #getAbsolutelyAll()} and some other
328
     *         methods won't access the DB, <code>false</code> if any call to a method about values
329
     *         will access the DB.
330
     */
331
    public final boolean isFilled() {
332
        return this.fetched;
333
    }
334
 
151 ilm 335
    @Override
336
    protected void initValues() {
132 ilm 337
        if (!this.isFilled())
17 ilm 338
            this.fetchValues();
151 ilm 339
    }
340
 
341
    private Map<String, Object> getValues() {
342
        this.initValues();
17 ilm 343
        return this.values;
344
    }
345
 
346
    /**
347
     * Recharge les valeurs des champs depuis la base.
348
     */
144 ilm 349
    public final void fetchValues() {
17 ilm 350
        this.fetchValues(true);
351
    }
352
 
156 ilm 353
    /**
354
     * Fetch up-to-date values from the DB.
355
     *
356
     * @param useCache <code>true</code> to use the {@link SQLDataSource#isCacheEnabled() cache}.
357
     * @return this.
358
     */
144 ilm 359
    public final SQLRow fetchValues(final boolean useCache) {
17 ilm 360
        return this.fetchValues(useCache, useCache);
361
    }
362
 
156 ilm 363
    /**
364
     * Return a new instance with up-to-date values.
365
     *
366
     * @param useCache <code>true</code> to use the {@link SQLDataSource#isCacheEnabled() cache}.
367
     * @return a new instance.
368
     */
369
    public final SQLRow fetchNew(final boolean useCache) {
182 ilm 370
        return this.fetchNewRow(useCache);
156 ilm 371
    }
372
 
17 ilm 373
    @SuppressWarnings("unchecked")
374
    SQLRow fetchValues(final boolean readCache, final boolean writeCache) {
182 ilm 375
        // not necessary here, but allows to fail early and avoid a request
376
        // implique trop de regression dans la branche : checkFrozen();
132 ilm 377
        final IResultSetHandler handler = new IResultSetHandler(SQLDataSource.MAP_HANDLER, readCache, writeCache) {
17 ilm 378
            @Override
379
            public Set<SQLRow> getCacheModifiers() {
380
                return Collections.singleton(SQLRow.this);
381
            }
382
        };
182 ilm 383
        final Map<String, Object> values = (Map<String, Object>) this.getTable().getBase().getDataSource().execute(this.getQuery(), handler, false);
384
        this.setValues(values == null ? null : Collections.unmodifiableMap(values));
17 ilm 385
        return this;
386
    }
387
 
182 ilm 388
    // Attention ne vérifie pas que tous les champs soient présents.
389
    // Private since it doesn't make sure the map is immutable.
17 ilm 390
    private final void setValues(Map<String, Object> values) {
182 ilm 391
        // implique trop de regression dans la branche : checkFrozen();
17 ilm 392
        this.values = values;
393
        if (!this.fetched)
394
            this.fetched = true;
395
    }
396
 
397
    /**
398
     * Retourne les noms des champs qui ont été chargé depuis la base.
399
     *
400
     * @return les noms des champs qui ont été chargé depuis la base.
401
     */
132 ilm 402
    @Override
17 ilm 403
    public Set<String> getFields() {
182 ilm 404
        return this.isFilled() ? this.getValues().keySet() : Collections.<String> emptySet();
17 ilm 405
    }
406
 
151 ilm 407
    // avoid Collections.unmodifiableSet() allocation
408
    @Override
409
    public boolean contains(String fieldName) {
182 ilm 410
        return this.isFilled() ? this.getValues().containsKey(fieldName) : false;
151 ilm 411
    }
412
 
17 ilm 413
    private String getQuery() {
414
        return "SELECT * FROM " + this.getTable().getSQLName().quote() + " WHERE " + this.getWhere().getClause();
415
    }
416
 
417
    public Where getWhere() {
418
        return new Where(this.getTable().getKey(), "=", this.getID());
419
    }
420
 
421
    /**
422
     * Est ce que cette ligne existe dans la base de donnée.
423
     *
424
     * @return <code>true</code> si la ligne existait lors de son instanciation.
425
     */
426
    public boolean exists() {
427
        return this.getValues() != null;
428
    }
429
 
430
    /**
431
     * Est ce que cette ligne existe et n'est pas archivée.
432
     *
433
     * @return <code>true</code> si cette ligne est valide.
434
     */
435
    public boolean isValid() {
436
        return this.exists() && this.getID() >= MIN_VALID_ID && !this.isArchived();
437
    }
438
 
439
    public boolean isData() {
440
        return this.isValid() && !this.isUndefined();
441
    }
442
 
443
    /**
444
     * Retourne le champ nommé <code>field</code> de cette ligne.
445
     *
446
     * @param field le nom du champ que l'on veut.
447
     * @return la valeur du champ sous forme d'objet Java, ou <code>null</code> si la valeur est
448
     *         NULL.
449
     * @throws IllegalStateException si cette ligne n'existe pas.
450
     * @throws IllegalArgumentException si cette ligne ne contient pas le champ demandé.
451
     */
132 ilm 452
    @Override
17 ilm 453
    public final Object getObject(String field) {
454
        if (!this.exists())
455
            throw new IllegalStateException("The row " + this + "does not exist.");
456
        if (!this.getTable().contains(field))
457
            throw new IllegalArgumentException("The table of the row " + this + " doesn't contain the field '" + field + "'.");
458
        // pour différencier entre la valeur est NULL (SQL) et la ligne ne contient pas ce champ
459
        if (!this.getValues().containsKey(field)) {
460
            // on ne l'a pas fetché
461
            this.fetchValues();
462
            // MAYBE mettre un boolean pour choisir si on accède à la base ou pas
463
            // since we just made a trip to the db we can afford to print at least a message
464
            final String msg = "The row " + this.simpleToString() + " doesn't contain the field '" + field + "' ; refetching.";
465
            Log.get().warning(msg);
466
            if (printSTForMissingField)
467
                new IllegalArgumentException(msg).printStackTrace();
468
        }
132 ilm 469
        assert this.getValues().containsKey(field);
17 ilm 470
        return this.getValues().get(field);
471
    }
472
 
182 ilm 473
    @Override
474
    public final Object getObjectNoCheck(String field) {
475
        return this.values.get(field);
476
    }
477
 
156 ilm 478
    /**
479
     * Fetch from the DB this row and the next/previous one. ATTN the rows are locked
480
     * {@link LockStrength#UPDATE for update}, but if this method is not called from within a
481
     * transaction, they will immediately be obsolete.
482
     *
483
     * @param after <code>true</code> to return the next row, <code>false</code> to return the
484
     *        previous.
485
     * @return {@link List2#get0() this row} and the next/previous one with only
486
     *         {@link SQLTable#getOrderField()} and {@link SQLTable#getArchiveField()} fetched,
487
     *         <code>null</code> if this row doesn't exist, the {@link List2#get1() next/previous
488
     *         row} is <code>null</code> if this is the last/first row of the table or has
489
     *         <code>null</code> order.
490
     * @throws IllegalStateException if this is the {@link #isUndefined() undefined} row.
491
     */
492
    public final List2<SQLRow> fetchThisAndSequentialRow(boolean after) throws IllegalStateException {
493
        if (this.isUndefined())
494
            throw new IllegalStateException("Cannot order against the undefined");
17 ilm 495
        final SQLTable t = this.getTable();
496
        final int diff = (!after) ? -1 : 1;
497
 
156 ilm 498
        // this is one statement (subquery included) and thus atomic : the inner FOR UPDATE ensures
499
        // that the ORDER doesn't change by the time the outer query is executed
500
        // SELECT * FROM "test"."BATIMENT"
501
        // WHERE "ORDRE" >= (SELECT "ORDRE" FROM "test"."BATIMENT" WHERE "ID" = 3 FOR UPDATE)
502
        // ORDER BY "ORDRE"
503
        // LIMIT 2
504
        // FOR UPDATE;
505
 
506
        final SQLSelect selOrder = new SQLSelect();
507
        // OK to order against an archived
508
        selOrder.setArchivedPolicy(SQLSelect.BOTH);
509
        selOrder.addSelect(t.getOrderField());
510
        selOrder.setWhere(this.getWhere());
511
        selOrder.setLockStrength(LockStrength.UPDATE);
512
 
73 ilm 513
        final SQLSelect sel = new SQLSelect();
156 ilm 514
        // don't ignore undefined or the caller might want to use its order
515
        sel.setExcludeUndefined(false);
17 ilm 516
        // unique index prend aussi en compte les archivés
517
        sel.setArchivedPolicy(SQLSelect.BOTH);
518
        sel.addSelect(t.getKey());
519
        sel.addSelect(t.getOrderField());
73 ilm 520
        if (t.isArchivable())
521
            sel.addSelect(t.getArchiveField());
156 ilm 522
        final Where orderWhere = Where.createRaw(t.getOrderField().getFieldRef() + (diff < 0 ? "<=" : ">=") + "(" + selOrder + ")", t.getOrderField());
523
        // this.getWhere() needed when ORDER is null
524
        sel.setWhere(orderWhere.or(this.getWhere()));
65 ilm 525
        sel.addFieldOrder(t.getOrderField(), diff < 0 ? Order.desc() : Order.asc());
156 ilm 526
        sel.setLimit(2);
527
        sel.setLockStrength(LockStrength.UPDATE);
17 ilm 528
 
156 ilm 529
        final List<SQLRow> rows = SQLRowListRSH.execute(sel);
530
        assert rows.size() <= 2;
531
        if (rows.isEmpty()) {
73 ilm 532
            return null;
17 ilm 533
        } else {
156 ilm 534
            assert rows.get(0).equals(this);
535
            return new List2<>(rows.get(0), rows.size() == 1 ? null : rows.get(1));
17 ilm 536
        }
537
    }
538
 
25 ilm 539
    @Override
17 ilm 540
    public SQLRow getForeign(String fieldName) {
541
        return this.getForeignRow(fieldName);
542
    }
543
 
544
    /**
545
     * Retourne la ligne sur laquelle pointe le champ passé. Elle peut être archivé ou indéfinie.
546
     *
547
     * @param field le nom de la clef externe.
25 ilm 548
     * @return la ligne sur laquelle pointe le champ passé.
17 ilm 549
     * @throws IllegalArgumentException si <code>field</code> n'est pas une clef étrangère de la
550
     *         table de cette ligne.
551
     * @throws IllegalStateException si <code>field</code> contient l'ID d'une ligne inexistante.
552
     */
553
    public SQLRow getForeignRow(String field) {
554
        return this.getForeignRow(field, SQLRowMode.EXIST);
555
    }
556
 
557
    /**
558
     * Retourne la ligne sur laquelle pointe le champ passé.
559
     *
560
     * @param field le nom de la clef externe.
561
     * @param mode quel type de ligne retourner.
562
     * @return la ligne sur laquelle pointe le champ passé, ou <code>null</code> si elle ne
563
     *         correspond pas au mode.
564
     * @throws IllegalArgumentException si <code>field</code> n'est pas une clef étrangère de la
565
     *         table de cette ligne.
566
     * @throws IllegalStateException si <code>field</code> contient l'ID d'une ligne inexistante et
567
     *         que l'on n'en veut pas (mode.wantExisting() == <code>true</code>).
568
     */
569
    public SQLRow getForeignRow(String field, SQLRowMode mode) {
570
        final SQLField f = this.getTable().getField(field);
93 ilm 571
        final Link foreignLink = this.getTable().getDBSystemRoot().getGraph().getForeignLink(f);
572
        if (foreignLink == null)
17 ilm 573
            throw new IllegalArgumentException(field + " is not a foreign key of " + this.getTable());
93 ilm 574
        return this.getUncheckedForeignRow(foreignLink, mode);
17 ilm 575
    }
576
 
93 ilm 577
    public SQLRow getForeignRow(Link foreignLink, SQLRowMode mode) {
578
        if (!foreignLink.getSource().equals(this.getTable()))
579
            throw new IllegalArgumentException(foreignLink + " is not a foreign key of " + this.getTable());
580
        return this.getUncheckedForeignRow(foreignLink, mode);
581
    }
582
 
17 ilm 583
    private SQLRow getUncheckedForeignRow(Link foreignLink, SQLRowMode mode) {
584
        final SQLField field = foreignLink.getLabel();
585
        final SQLTable foreignTable = foreignLink.getTarget();
586
        if (this.getObject(field.getName()) == null) {
587
            return null;
588
        } else {
589
            final int foreignID = this.getInt(field.getName());
590
            final SQLRow foreignRow = new SQLRow(foreignTable, foreignID);
591
            // we used to check coherence here before all our dbs had real foreign keys
592
            return mode.filter(foreignRow);
593
        }
594
    }
595
 
596
    /**
597
     * Retourne l'ensemble des lignes de destTable liées à cette ligne.
598
     *
599
     * @param destTable la table dont on veut les lignes, eg "CPI_BT".
600
     * @return l'ensemble des lignes liées à cette ligne, eg les cpis de LOCAL[5822].
601
     * @see #getLinkedRows(String)
602
     */
603
    public Set<SQLRow> getLinkedRows(String destTable) {
604
        return this.getDistantRows(Collections.singletonList(destTable));
605
    }
606
 
607
    /**
608
     * Retourne l'ensemble des lignes de destTable qui sont pointées par celle-ci.
609
     *
610
     * @param destTable la table dont on veut les lignes, eg "OBSERVATION".
611
     * @return l'ensemble des lignes liées à cette ligne, eg les lignes pointées par
612
     *         "ID_OBSERVATION", "ID_OBSERVATION_2", etc.
613
     * @see #getLinkedRows(String)
614
     */
615
    public Set<SQLRow> getForeignRows(String destTable) {
616
        return this.getForeignRows(destTable, SQLRowMode.DATA);
617
    }
618
 
619
    public Set<SQLRow> getForeignRows(String destTable, SQLRowMode mode) {
620
        return new HashSet<SQLRow>(this.getForeignRowsMap(destTable, mode).values());
621
    }
622
 
623
    public Set<SQLRow> getForeignRows() {
624
        return this.getForeignRows(SQLRowMode.DATA);
625
    }
626
 
627
    public Set<SQLRow> getForeignRows(SQLRowMode mode) {
628
        return new HashSet<SQLRow>(this.getForeignRowsMap(mode).values());
629
    }
630
 
631
    /**
632
     * Retourne les lignes de destTable liées à cette ligne, indexées par les clefs externes.
633
     *
634
     * @param destTable la table dont on veut les lignes.
635
     * @return les lignes de destTable liées à cette ligne.
636
     */
637
    public Map<SQLField, SQLRow> getForeignRowsMap(String destTable) {
638
        return this.getForeignRowsMap(destTable, SQLRowMode.DATA);
639
    }
640
 
641
    public Map<SQLField, SQLRow> getForeignRowsMap(String destTable, SQLRowMode mode) {
642
        final Set<Link> links = this.getTable().getDBSystemRoot().getGraph().getForeignLinks(this.getTable(), this.getTable().getTable(destTable));
643
        return this.foreignLinksToMap(links, mode);
644
    }
645
 
646
    public Map<SQLField, SQLRow> getForeignRowsMap() {
647
        return this.getForeignRowsMap(SQLRowMode.DATA);
648
    }
649
 
650
    public Map<SQLField, SQLRow> getForeignRowsMap(SQLRowMode mode) {
132 ilm 651
        return this.foreignLinksToMap(this.getTable().getForeignLinks(), mode);
17 ilm 652
    }
653
 
654
    private Map<SQLField, SQLRow> foreignLinksToMap(Collection<Link> links, SQLRowMode mode) {
655
        final Map<SQLField, SQLRow> res = new HashMap<SQLField, SQLRow>();
656
        for (final Link l : links) {
657
            final SQLRow fr = this.getUncheckedForeignRow(l, mode);
658
            if (fr != null)
659
                res.put(l.getLabel(), fr);
660
        }
661
        return res;
662
    }
663
 
664
    /**
665
     * Fait la jointure entre cette ligne et les tables passées.
666
     *
667
     * @param path le chemin de la jointure.
668
     * @return la ligne correspondante.
669
     * @throws IllegalArgumentException si le path est mauvais.
670
     * @throws IllegalStateException si le path ne méne pas à une ligne unique.
671
     * @see #getDistantRows(List)
672
     */
673
    public SQLRow getDistantRow(List<String> path) {
80 ilm 674
        return this.getDistantRow(Path.get(this.getTable()).addTables(path));
65 ilm 675
    }
676
 
677
    public SQLRow getDistantRow(final Path path) {
19 ilm 678
        final Set<SQLRow> rows = this.getDistantRows(path);
17 ilm 679
        if (rows.size() != 1)
680
            throw new IllegalStateException("the path " + path + " does not lead to a unique row (" + rows.size() + ")");
65 ilm 681
        return rows.iterator().next();
17 ilm 682
    }
683
 
684
    /**
685
     * Fait la jointure entre cette ligne et les tables passées.
686
     *
687
     * @param path le chemin de la jointure.
688
     * @return un ensemble de lignes de la dernière table du chemin, dans l'ordre.
689
     * @throws IllegalArgumentException si le path est mauvais.
690
     */
691
    public Set<SQLRow> getDistantRows(List<String> path) {
80 ilm 692
        return this.getDistantRows(Path.get(this.getTable()).addTables(path));
65 ilm 693
    }
694
 
695
    public Set<SQLRow> getDistantRows(final Path path) {
132 ilm 696
        return this.getDistantRows(path, ArchiveMode.UNARCHIVED);
697
    }
698
 
699
    public Set<SQLRow> getDistantRows(final Path path, final ArchiveMode archiveMode) {
144 ilm 700
        return getDistantRows(path, archiveMode, true);
701
    }
702
 
703
    public Set<SQLRow> getDistantRows(final Path path, final ArchiveMode archiveMode, final boolean orderLast) {
704
        return (Set<SQLRow>) getDistantRows(path, archiveMode, orderLast, false);
705
    }
706
 
707
    public List<SQLRow> getDistantRowsList(final Path path, final ArchiveMode archiveMode) {
708
        // this method can return the same row multiple times, so don't use its order or the
709
        // duplicated rows will always be grouped together.
710
        return getDistantRowsList(path, archiveMode, false);
711
    }
712
 
713
    public List<SQLRow> getDistantRowsList(final Path path, final ArchiveMode archiveMode, final boolean orderLast) {
714
        return (List<SQLRow>) getDistantRows(path, archiveMode, orderLast, true);
715
    }
716
 
717
    private Collection<SQLRow> getDistantRows(final Path path, final ArchiveMode archiveMode, final boolean orderLast, final boolean list) {
718
        if (path.length() == 0) {
719
            if (SQLRowMode.check(archiveMode, this))
720
                return list ? Collections.singletonList(this) : Collections.singleton(this);
721
            else
722
                return list ? Collections.<SQLRow> emptyList() : Collections.<SQLRow> emptySet();
723
        } else {
724
            // on veut tous les champs de la derniere table et rien d'autre
725
            final List<List<String>> fields = new ArrayList<List<String>>(Collections.nCopies(path.length() - 1, Collections.<String> emptyList()));
726
            fields.add(null);
727
            final List<List<SQLRow>> s = this.getRowsOnPath(path, fields, archiveMode, orderLast);
728
            final List<SQLRow> resList = list ? new ArrayList<SQLRow>(s.size()) : null;
729
            final Set<SQLRow> resSet = list ? null : new LinkedHashSet<SQLRow>(s.size());
730
            final Collection<SQLRow> res = list ? resList : resSet;
731
            assert res != null;
732
            for (final List<SQLRow> l : s) {
733
                assert l.size() == 1 : "Too many rows were created : " + l;
734
                res.add(l.get(0));
735
            }
736
            return list ? Collections.unmodifiableList(resList) : Collections.unmodifiableSet(resSet);
17 ilm 737
        }
738
    }
739
 
740
    /**
741
     * Retourne les lignes distantes, plus les lignes intermédiaire du chemin. Par exemple
742
     * SITE[128].getRowsOnPath("BATIMENT,LOCAL", [null, "DESIGNATION"]) retourne tous les locaux du
743
     * site (seul DESIGNATION est chargé) avec tous les champs de leurs bâtiments.
744
     *
65 ilm 745
     * @param path le chemin dans le graphe de la base, see {@link Path#addTables(List)}.
17 ilm 746
     * @param fields un liste de des champs, chaque élément est :
747
     *        <ul>
748
     *        <li><code>null</code> pour tous les champs</li>
21 ilm 749
     *        <li>une Collection de nom de champs, e.g. ["DESIGNATION","NUMERO"]</li>
17 ilm 750
     *        </ul>
144 ilm 751
     * @return a list with one item per distant row, and each item has all the rows on the passed
752
     *         path.
17 ilm 753
     */
144 ilm 754
    public List<List<SQLRow>> getRowsOnPath(final List<String> path, final List<? extends Collection<String>> fields) {
80 ilm 755
        return this.getRowsOnPath(Path.get(this.getTable()).addTables(path), fields);
65 ilm 756
    }
757
 
144 ilm 758
    public List<List<SQLRow>> getRowsOnPath(final Path p, final List<? extends Collection<String>> fields) {
132 ilm 759
        return this.getRowsOnPath(p, fields, ArchiveMode.UNARCHIVED);
760
    }
761
 
144 ilm 762
    public List<List<SQLRow>> getRowsOnPath(final Path p, final List<? extends Collection<String>> fields, final ArchiveMode archiveMode) {
763
        return this.getRowsOnPath(p, fields, archiveMode, true);
764
    }
765
 
766
    // returns a List since the same row might be linked several times to another
767
    public List<List<SQLRow>> getRowsOnPath(final Path p, final List<? extends Collection<String>> fields, final ArchiveMode archiveMode, final boolean orderLast) {
65 ilm 768
        final int pathSize = p.length();
17 ilm 769
        if (pathSize == 0)
770
            throw new IllegalArgumentException("path is empty");
771
        if (pathSize != fields.size())
772
            throw new IllegalArgumentException("path and fields size mismatch : " + pathSize + " != " + fields.size());
65 ilm 773
        if (p.getFirst() != this.getTable())
774
            throw new IllegalArgumentException("path doesn't start with us : " + p.getFirst() + " != " + this.getTable());
144 ilm 775
        final List<List<SQLRow>> res = new ArrayList<List<SQLRow>>();
17 ilm 776
 
65 ilm 777
        final DBSystemRoot sysRoot = this.getTable().getDBSystemRoot();
778
        Where where = sysRoot.getGraph().getJointure(p);
17 ilm 779
        // ne pas oublier de sélectionner notre ligne
780
        where = where.and(this.getWhere());
781
 
80 ilm 782
        final SQLSelect select = new SQLSelect();
132 ilm 783
        select.setArchivedPolicy(archiveMode);
17 ilm 784
 
21 ilm 785
        final List<Collection<String>> fieldsCols = new ArrayList<Collection<String>>(pathSize);
17 ilm 786
        for (int i = 0; i < pathSize; i++) {
21 ilm 787
            final Collection<String> tableFields = fields.get(i);
17 ilm 788
            // +1 car p contient cette ligne
789
            final SQLTable t = p.getTable(i + 1);
21 ilm 790
            final Collection<String> fieldsCol;
791
            if (tableFields == null) {
792
                fieldsCol = t.getFieldsName();
17 ilm 793
            } else {
21 ilm 794
                fieldsCol = tableFields;
17 ilm 795
            }
796
            fieldsCols.add(fieldsCol);
797
 
798
            // les tables qui ne nous interessent pas
799
            if (fieldsCol.size() > 0) {
800
                // toujours mettre l'ID
801
                select.addSelect(t.getKey());
802
                // plus les champs demandés
21 ilm 803
                select.addAllSelect(t, fieldsCol);
17 ilm 804
            }
144 ilm 805
            if (!orderLast) {
806
                select.addOrder(t);
807
            }
17 ilm 808
        }
809
        // dans tous les cas mettre l'ID de la dernière table
810
        final SQLTable lastTable = p.getLast();
811
        select.addSelect(lastTable.getKey());
812
 
93 ilm 813
        select.setWhere(where);
144 ilm 814
        if (orderLast) {
815
            // determinist order even if there's no order field or invalid values in it
816
            select.addOrderSilent(lastTable.getName());
817
            select.addFieldOrder(lastTable.getKey());
818
        }
93 ilm 819
 
17 ilm 820
        // on ajoute une SQLRow pour chaque ID trouvé
65 ilm 821
        sysRoot.getDataSource().execute(select.asString(), new ResultSetHandler() {
17 ilm 822
 
823
            public Object handle(ResultSet rs) throws SQLException {
824
                final ResultSetMetaData rsmd = rs.getMetaData();
825
                while (rs.next()) {
826
                    final List<SQLRow> rows = new ArrayList<SQLRow>(pathSize);
827
                    for (int i = 0; i < pathSize; i++) {
828
                        // les tables qui ne nous interessent pas
829
                        if (fieldsCols.get(i).size() > 0) {
830
                            // +1 car p contient cette ligne
831
                            final SQLTable t = p.getTable(i + 1);
832
                            rows.add(SQLRow.createFromRS(t, rs, rsmd, pathSize == 1));
833
                        }
834
                    }
835
                    res.add(rows);
836
                }
837
                return null;
838
            }
839
        });
840
 
841
        return res;
842
    }
843
 
844
    /**
845
     * Retourne les lignes pointant sur celle ci.
846
     *
847
     * @return les lignes pointant sur celle ci.
848
     */
849
    public final List<SQLRow> getReferentRows() {
850
        return this.getReferentRows((Set<SQLTable>) null);
851
    }
852
 
853
    @Override
854
    public final List<SQLRow> getReferentRows(SQLTable refTable) {
855
        return this.getReferentRows(Collections.singleton(refTable));
856
    }
857
 
858
    /**
859
     * Retourne les lignes des tables spécifiées pointant sur celle ci.
860
     *
861
     * @param tables les tables voulues, <code>null</code> pour toutes.
862
     * @return les SQLRow pointant sur celle ci.
863
     */
864
    public final List<SQLRow> getReferentRows(Set<SQLTable> tables) {
865
        return this.getReferentRows(tables, SQLSelect.UNARCHIVED);
866
    }
867
 
93 ilm 868
    private final SetMap<SQLTable, Link> getReferentLinks(Set<SQLTable> tables) {
869
        final Set<Link> links = this.getTable().getBase().getGraph().getReferentLinks(this.getTable());
870
        final SetMap<SQLTable, Link> byTable = new SetMap<SQLTable, Link>();
871
        for (final Link l : links) {
872
            final SQLTable src = l.getSource();
873
            if (tables == null || tables != null && tables.contains(src)) {
874
                byTable.add(src, l);
875
            }
876
        }
877
        return byTable;
878
    }
879
 
17 ilm 880
    /**
881
     * Returns the rows of tables that points to this row.
882
     *
883
     * @param tables a Set of tables, or <code>null</code> for all of them.
884
     * @param archived <code>SQLSelect.UNARCHIVED</code>, <code>SQLSelect.ARCHIVED</code> or
885
     *        <code>SQLSelect.BOTH</code>.
886
     * @return a List of SQLRow that points to this.
887
     */
888
    public final List<SQLRow> getReferentRows(Set<SQLTable> tables, ArchiveMode archived) {
93 ilm 889
        final SetMap<SQLTable, Link> byTable = getReferentLinks(tables);
890
        final Set<SQLRow> res = new LinkedHashSet<SQLRow>();
891
        for (final Entry<SQLTable, Set<Link>> e : byTable.entrySet()) {
892
            res.addAll(this.getReferentRows(e.getValue(), archived, null));
893
        }
894
        return new ArrayList<SQLRow>(res);
17 ilm 895
    }
896
 
83 ilm 897
    public final ListMap<Link, SQLRow> getReferentRowsByLink() {
17 ilm 898
        return this.getReferentRowsByLink(null);
899
    }
900
 
83 ilm 901
    public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables) {
17 ilm 902
        return this.getReferentRowsByLink(tables, SQLSelect.UNARCHIVED);
903
    }
904
 
83 ilm 905
    public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables, ArchiveMode archived) {
906
        // List since getReferentRows() is ordered
907
        final ListMap<Link, SQLRow> res = new ListMap<Link, SQLRow>();
93 ilm 908
        final SetMap<SQLTable, Link> byTable = getReferentLinks(tables);
909
        for (final Entry<SQLTable, Set<Link>> e : byTable.entrySet()) {
910
            final Set<Link> links = e.getValue();
911
            final List<SQLRow> rows = this.getReferentRows(links, archived, null);
912
            for (final Link l : links) {
913
                // put all referent links, even if there's no referent row
914
                res.put(l, Collections.<SQLRow> emptyList());
915
                for (final SQLRow r : rows) {
916
                    if (r.getForeignID(l.getLabel().getName()) == this.getID())
917
                        res.add(l, r);
918
                }
17 ilm 919
            }
920
        }
921
        return res;
922
    }
923
 
924
    /**
925
     * Returns the rows that points to this row by the refField.
926
     *
927
     * @param refField a SQLField that points to the table of this row, eg BATIMENT.ID_SITE.
928
     * @return a List of SQLRow that points to this, eg [BATIMENT[123], BATIMENT[124]].
929
     */
930
    public List<SQLRow> getReferentRows(final SQLField refField) {
931
        return this.getReferentRows(refField, SQLSelect.UNARCHIVED);
932
    }
933
 
934
    public List<SQLRow> getReferentRows(final SQLField refField, final ArchiveMode archived) {
935
        return this.getReferentRows(refField, archived, null);
936
    }
937
 
938
    /**
939
     * Returns the rows that points to this row by <code>refField</code>.
940
     *
941
     * @param refField a SQLField that points to the table of this row, eg BATIMENT.ID_SITE.
942
     * @param archived specify which rows should be returned.
943
     * @param fields the list of fields the rows will have, <code>null</code> meaning all.
944
     * @return a List of SQLRow that points to this, eg [BATIMENT[123], BATIMENT[124]].
945
     */
946
    public List<SQLRow> getReferentRows(final SQLField refField, final ArchiveMode archived, final Collection<String> fields) {
93 ilm 947
        return getReferentRows(Collections.singleton(refField.getTable().getDBSystemRoot().getGraph().getForeignLink(refField)), archived, fields);
948
    }
949
 
950
    // fetch all rows from the same table at once : less requests than one per link and thus rows
951
    // are ordered across links.
952
    private List<SQLRow> getReferentRows(final Set<Link> links, final ArchiveMode archived, final Collection<String> fields) {
953
        if (links.isEmpty())
954
            return Collections.emptyList();
955
 
956
        SQLTable src = null;
957
        Where w = null;
958
        for (final Link l : links) {
959
            if (src == null) {
960
                src = l.getSource();
961
            } else if (!l.getSource().equals(src)) {
962
                throw new IllegalArgumentException(l + " doesn't come from " + src);
963
            }
964
            if (!l.getTarget().equals(this.getTable())) {
965
                throw new IllegalArgumentException(l + " doesn't point to " + this.getTable());
966
            }
967
            w = new Where(l.getLabel(), "=", this.getID()).or(w);
17 ilm 968
        }
969
 
80 ilm 970
        final SQLSelect sel = new SQLSelect();
93 ilm 971
        if (fields == null) {
17 ilm 972
            sel.addSelectStar(src);
93 ilm 973
        } else {
17 ilm 974
            sel.addSelect(src.getKey());
975
            for (final String f : fields)
976
                sel.addSelect(src.getField(f));
977
        }
93 ilm 978
        sel.setWhere(w);
17 ilm 979
        sel.setArchivedPolicy(archived);
980
        sel.addOrderSilent(src.getName());
981
        // - if some other criteria need to be applied, we could pass an SQLRowMode (instead of
982
        // just ArchiveMode) and modify the SQLSelect accordingly
983
 
21 ilm 984
        return SQLRowListRSH.execute(sel);
17 ilm 985
    }
986
 
987
    /**
988
     * Toutes les lignes qui touchent cette lignes. C'est à dire les lignes pointées par les clefs
989
     * externes plus lignes qui pointent sur cette ligne.
990
     *
991
     * @return les lignes qui touchent cette lignes.
992
     */
993
    private Set<SQLRow> getConnectedRows() {
994
        Set<SQLRow> res = new HashSet<SQLRow>();
995
        res.addAll(this.getReferentRows((Set<SQLTable>) null, SQLSelect.BOTH));
996
        res.addAll(this.getForeignRows(SQLRowMode.EXIST));
997
        return res;
998
    }
999
 
67 ilm 1000
    @Override
1001
    public Collection<SQLRow> followLink(Link l, Direction direction) {
1002
        // Path checks that one end of l is this table and that direction is valid (e.g. not ANY for
1003
        // self-reference links)
80 ilm 1004
        final boolean backwards = Path.get(getTable()).add(l, direction).isBackwards(0);
67 ilm 1005
        if (backwards)
1006
            return getReferentRows(l.getSingleField());
1007
        else
1008
            return Collections.singletonList(getForeign(l.getSingleField().getName()));
1009
    }
1010
 
17 ilm 1011
    /**
1012
     * Trouve les lignes archivées reliées à celle ci par moins de maxLength liens.
1013
     *
1014
     * @param maxLength la longeur maximale du chemin entre les lignes retournées et celle ci.
1015
     * @return les lignes archivées reliées à celle ci.
1016
     */
1017
    public Set<SQLRow> findDistantArchived(int maxLength) {
1018
        return this.findDistantArchived(maxLength, new HashSet<SQLRow>(), 0);
1019
    }
1020
 
1021
    private Set<SQLRow> findDistantArchived(final int maxLength, final Set<SQLRow> been, int length) {
1022
        final Set<SQLRow> res = new HashSet<SQLRow>();
1023
 
1024
        if (maxLength == length)
1025
            return res;
1026
 
1027
        // on avance d'un cran
1028
        been.add(this);
1029
        length++;
1030
 
1031
        // on garde les lignes à appeler récursivement pour la fin
1032
        // car on veut parcourir en largeur d'abord
1033
        final Set<SQLRow> rec = new HashSet<SQLRow>();
1034
        Iterator<SQLRow> iter = this.getConnectedRows().iterator();
1035
        while (iter.hasNext()) {
1036
            final SQLRow row = iter.next();
1037
            if (!been.contains(row)) {
1038
                if (row.isArchived()) {
1039
                    res.add(row);
1040
                } else {
1041
                    rec.add(row);
1042
                }
1043
            }
1044
        }
1045
        iter = rec.iterator();
1046
        while (iter.hasNext()) {
1047
            final SQLRow row = iter.next();
1048
            res.addAll(row.findDistantArchived(maxLength, been, length));
1049
        }
1050
        return res;
1051
    }
1052
 
132 ilm 1053
    @Override
17 ilm 1054
    public String toString() {
132 ilm 1055
        return fullToString(false);
1056
    }
1057
 
1058
    public String fullToString(final boolean allowDBAccess) {
17 ilm 1059
        String res = this.simpleToString();
132 ilm 1060
        final Boolean exists = allowDBAccess || this.isFilled() ? this.exists() : null;
1061
        if (exists == null) {
17 ilm 1062
            res = "?" + res + "?";
132 ilm 1063
        } else if (!exists) {
1064
            res = "-" + res + "-";
1065
        } else {
1066
            // the row exists
1067
 
1068
            Boolean archived = null;
1069
            try {
1070
                archived = this.isArchived(allowDBAccess);
1071
            } catch (Exception e) {
1072
                Log.get().log(Level.FINER, "Couldn't determine archive status", e);
1073
                assert archived == null;
1074
            }
1075
            if (archived == null) {
1076
                res = "?" + res + "?";
1077
            } else if (archived) {
1078
                res = "(" + res + ")";
1079
            }
17 ilm 1080
        }
1081
        return res;
1082
    }
1083
 
1084
    public String simpleToString() {
1085
        return this.getTable().getName() + "[" + this.ID + "]";
1086
    }
1087
 
151 ilm 1088
    @Override
1089
    public String mapToString() {
1090
        final String result = this.fullToString(false) + " : ";
1091
        return result + (this.values != null ? this.values : (this.isFilled() ? "not in DB" : "not filled"));
1092
    }
1093
 
17 ilm 1094
    /**
1095
     * Renvoie tous les champs de cette ligne, clef comprises. En général on ne veut pas les valeurs
1096
     * des clefs, voir getAllValues().
1097
     * <p>
1098
     * Les valeurs de cette map sont les valeurs retournées par getObject().
1099
     * </p>
1100
     *
1101
     * @return tous les champs de cette ligne.
1102
     * @see #getAllValues()
1103
     * @see #getObject(String)
1104
     */
1105
    @Override
1106
    public Map<String, Object> getAbsolutelyAll() {
182 ilm 1107
        return this.getValues();
17 ilm 1108
    }
1109
 
132 ilm 1110
    private static final VirtualFields ALL_VALUES_FIELDS = VirtualFields.ALL.difference(VirtualFields.KEYS, VirtualFields.ARCHIVE, VirtualFields.ORDER);
1111
 
17 ilm 1112
    /**
1113
     * Retourne toutes les valeurs de cette lignes, sans les clefs ni les champs d'ordre et
1114
     * d'archive.
1115
     *
1116
     * @return toutes les valeurs de cette lignes.
1117
     * @see #getAbsolutelyAll()
1118
     */
1119
    public Map<String, Object> getAllValues() {
132 ilm 1120
        return this.getValues(ALL_VALUES_FIELDS);
17 ilm 1121
    }
1122
 
1123
    /**
1124
     * Creates a SQLRowValues with absolutely all the values of this row. ATTN the values are as
1125
     * always the ones at the moment of the last fetching.
1126
     *
1127
     * <pre>
1128
     * SQLRow r = table.getRow(123); // [a=&gt;'26', b=&gt; '25']
1129
     * r.createUpdateRow().put(&quot;a&quot;, 1).update();
1130
     * r.createUpdateRow().put(&quot;b&quot;, 2).update();
1131
     * </pre>
1132
     *
1133
     * You could think that r now equals [a=>1, b=>2]. No, actually it's [a=>'26', b=>2], because
1134
     * the second line overwrote the first one. The best solution is to use only one SQLRowValues
1135
     * (hence only one access to the DB), otherwise use createEmptyUpdateRow().
1136
     *
1137
     * @see #createEmptyUpdateRow()
1138
     * @return a SQLRowValues on this SQLRow.
1139
     */
1140
    public SQLRowValues createUpdateRow() {
83 ilm 1141
        return new SQLRowValues(this.getTable(), this.getValues());
17 ilm 1142
    }
1143
 
1144
    /**
1145
     * Gets the unique (among this table at least) identifier of this row.
1146
     *
1147
     * @return an int greater than {@link #MIN_VALID_ID} if this is valid.
1148
     */
1149
    @Override
1150
    public int getID() {
1151
        return this.ID;
1152
    }
1153
 
1154
    @Override
1155
    public Number getIDNumber() {
1156
        return this.idNumber;
1157
    }
1158
 
1159
    @Override
182 ilm 1160
    public SQLRow asRow(final Boolean immutable) {
1161
        if (immutable == null || this.isFrozen() == immutable)
1162
            return this;
1163
        else
1164
            return this.copy(immutable);
17 ilm 1165
    }
1166
 
1167
    @Override
182 ilm 1168
    public final SQLRowValues asRowValues(final Boolean immutable) {
1169
        final SQLRowValues res = this.createUpdateRow();
1170
        if (Boolean.TRUE.equals(immutable))
1171
            res.getGraph().freeze();
1172
        return res;
17 ilm 1173
    }
1174
 
1175
    /**
1176
     * Note : ne compare pas les valeurs des champs de cette ligne.
1177
     *
1178
     * @see java.lang.Object#equals(java.lang.Object)
1179
     */
1180
    public boolean equals(Object other) {
1181
        if (!(other instanceof SQLRow))
1182
            return false;
1183
        SQLRow o = (SQLRow) other;
1184
        return this.equalsAsRow(o);
1185
    }
1186
 
1187
    public int hashCode() {
1188
        return this.hashCodeAsRow();
1189
    }
1190
 
1191
    /**
1192
     * Transforme un chemin en une liste de nom de table. Si path est "" alors retourne une liste
1193
     * vide.
1194
     *
1195
     * @param path le chemin, eg "BATIMENT,LOCAL".
1196
     * @return une liste de String, eg ["BATIMENT","LOCAL"].
1197
     */
1198
    static public List<String> toList(String path) {
1199
        return Arrays.asList(toArray(path));
1200
    }
1201
 
1202
    static private String[] toArray(String path) {
1203
        if (path.length() == 0)
1204
            return new String[0];
1205
        else
1206
            // ATTN ',' : no spaces
1207
            return path.split(",");
1208
    }
1209
 
25 ilm 1210
    @Override
1211
    public SQLTableModifiedListener createTableListener(SQLDataListener l) {
17 ilm 1212
        return new SQLTableListenerData<SQLRow>(this, l);
1213
    }
1214
 
1215
}