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
 /*
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;
21
import org.openconcerto.sql.model.graph.Link;
67 ilm 22
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 23
import org.openconcerto.sql.model.graph.Path;
19 ilm 24
import org.openconcerto.sql.utils.ReOrder;
17 ilm 25
import org.openconcerto.utils.DecimalUtils;
83 ilm 26
import org.openconcerto.utils.ListMap;
17 ilm 27
 
28
import java.math.BigDecimal;
29
import java.sql.ResultSet;
30
import java.sql.ResultSetMetaData;
31
import java.sql.SQLException;
32
import java.util.ArrayList;
33
import java.util.Arrays;
34
import java.util.Collection;
35
import java.util.Collections;
36
import java.util.HashMap;
37
import java.util.HashSet;
38
import java.util.Iterator;
39
import java.util.LinkedHashSet;
40
import java.util.List;
41
import java.util.Map;
42
import java.util.Set;
43
 
44
import org.apache.commons.collections.CollectionUtils;
45
import org.apache.commons.collections.Predicate;
46
import org.apache.commons.dbutils.ResultSetHandler;
47
 
48
/**
49
 * Une ligne d'une table. Cette classe décrit une ligne et ne représente pas exactement une ligne
50
 * réelle, il n'y a pas unicité (cela reviendrait à recréer la base en Java !). Pour charger les
51
 * valeurs depuis la base manuellement à tout moment utiliser fetchValues(), cette méthode est
52
 * appelée automatiquement si nécessaire. Les valeurs des champs sont stockées, ainsi toutes les
53
 * méthodes renvoient l'état de la ligne réelle au moment du dernier fetchValues().
54
 * <p>
55
 * Une ligne peut ne pas exister ou être archivée, de plus elle peut ne pas contenir tous les champs
56
 * de la table. Pour accéder à la valeur des champs il existe getString() et getInt(), pour des
57
 * demandes plus complexes passer par getObject(). Si un champ qui n'est pas dans la ligne est
58
 * demandé, un fetchValues() est automatiquement fait.
59
 * </p>
60
 * <p>
61
 * On peut obtenir un ligne en la demandant à sa table, mais si l'on souhaite une SQLRow décrivant
62
 * une ligne n'existant pas dans la base il faut passer par le constructeur.
63
 * </p>
64
 *
65
 * @author ILM Informatique 20 mai 2004
66
 * @see #isValid()
67
 * @see #getObject(String)
68
 * @see org.openconcerto.sql.model.SQLTable#getRow(int)
69
 */
70
public class SQLRow extends SQLRowAccessor {
71
 
72
    /**
73
     * Each table must have a row with this ID, that others refer to to indicate the absence of a
74
     * link.
75
     *
76
     * @deprecated use either {@link SQLRowAccessor#isForeignEmpty(String)} /
77
     *             {@link SQLRowValues#putEmptyLink(String)} or if you must
78
     *             {@link SQLTable#getUndefinedID()}
79
     */
80
    public static final int UNDEFINED_ID = 1;
81
    /**
82
     * No valid database rows should have an ID thats less than MIN_VALID_ID. But remember, you CAN
83
     * have a SQLRow with any ID.
84
     */
85
    public static final int MIN_VALID_ID = 0;
86
    /** Value representing no ID, no table can have a row with this ID. */
87
    public static final int NONEXISTANT_ID = MIN_VALID_ID - 1;
88
    /** <code>true</code> to print a stack trace when fetching missing values */
89
    public static final boolean printSTForMissingField = false;
90
 
91
    /**
92
     * Crée une ligne avec les valeurs du ResultSet.
93
     *
94
     * @param table la table de la ligne.
95
     * @param rs les valeurs.
96
     * @param onlyTable pass <code>true</code> if <code>rs</code> only contains columns from
97
     *        <code>table</code>, if unsure pass <code>false</code>. This allows to avoid calling
98
     *        {@link ResultSetMetaData#getTableName(int)} which is expensive on some systems.
99
     * @return la ligne correspondante.
100
     * @throws SQLException si problème lors de l'accès au ResultSet.
101
     * @see SQLRow#SQLRow(SQLTable, Map)
102
     * @deprecated use {@link SQLRowListRSH} or {@link SQLRowValuesListFetcher} instead or if you
103
     *             must use a {@link ResultSet} call
104
     *             {@link #createFromRS(SQLTable, ResultSet, ResultSetMetaData, boolean)} thus
105
     *             avoiding the potentially costly {@link ResultSet#getMetaData()}
106
     */
107
    public static final SQLRow createFromRS(SQLTable table, ResultSet rs, final boolean onlyTable) throws SQLException {
108
        return createFromRS(table, rs, rs.getMetaData(), onlyTable);
109
    }
110
 
111
    public static final SQLRow createFromRS(SQLTable table, ResultSet rs, final ResultSetMetaData rsmd, final boolean onlyTable) throws SQLException {
112
        return createFromRS(table, rs, getFieldNames(table, rsmd, onlyTable));
113
    }
114
 
115
    private static final List<String> getFieldNames(SQLTable table, final ResultSetMetaData rsmd, final boolean tableOnly) throws SQLException {
116
        final int colCount = rsmd.getColumnCount();
117
        final List<String> names = new ArrayList<String>(colCount);
118
        for (int i = 1; i <= colCount; i++) {
119
            // n'inclure que les colonnes de la table demandée
120
            // use a boolean since some systems (eg pg) require a request to the db to return the
121
            // table name
122
            if (tableOnly || rsmd.getTableName(i).equals(table.getName())) {
123
                names.add(rsmd.getColumnName(i));
124
            } else {
125
                names.add(null);
126
            }
127
        }
128
 
129
        return names;
130
    }
131
 
132
    // MAYBE create an opaque class holding names so that we can make this method, getFieldNames()
133
    // and createListFromRS() public
134
    static final SQLRow createFromRS(SQLTable table, ResultSet rs, final List<String> names) throws SQLException {
135
        final int indexCount = names.size();
136
 
137
        final Map<String, Object> m = new HashMap<String, Object>(indexCount);
138
        for (int i = 0; i < indexCount; i++) {
139
            final String colName = names.get(i);
140
            if (colName != null)
141
                m.put(colName, rs.getObject(i + 1));
142
        }
143
 
83 ilm 144
        final Number id = getID(m, table, true);
145
        // e.g. LEFT JOIN : missing values are null
146
        if (id == null)
147
            return null;
148
 
149
        // pass already found ID
150
        return new SQLRow(table, id, m);
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 {
164
        return createListFromRS(table, rs, getFieldNames(table, rs.getMetaData(), tableOnly));
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"].
174
     * @return the data of the result set as SQLRows.
175
     * @throws SQLException if an error occurs while reading <code>rs</code>.
176
     */
177
    static final List<SQLRow> createListFromRS(SQLTable table, ResultSet rs, final List<String> names) throws SQLException {
178
        final List<SQLRow> res = new ArrayList<SQLRow>();
179
        while (rs.next()) {
83 ilm 180
            final SQLRow row = createFromRS(table, rs, names);
181
            if (row != null)
182
                res.add(row);
17 ilm 183
        }
184
        return res;
185
    }
186
 
187
    private final int ID;
188
    private final Number idNumber;
189
    private Map<String, Object> values;
190
    private boolean fetched;
191
 
192
    private SQLRow(SQLTable table, Number id) {
193
        super(table);
194
        this.fetched = false;
195
        this.ID = id.intValue();
196
        this.idNumber = id;
197
        this.checkTable();
198
    }
199
 
200
    // public pour pouvoir créer une ligne n'exisant pas
201
    public SQLRow(SQLTable table, int ID) {
202
        // have to cast to Number, if you use Integer.valueOf() (or cast to Integer) the resulting
203
        // Integer is converted to Long
204
        this(table, table.getKey().getType().getJavaType() == Integer.class ? (Number) ID : Long.valueOf(ID));
205
    }
206
 
207
    private void checkTable() {
208
        if (!this.getTable().isRowable())
209
            throw new IllegalArgumentException(this.getTable() + " is not rowable");
210
    }
211
 
212
    /**
213
     * Crée une ligne avec les valeurs fournies. Evite une requête à la base.
214
     *
215
     * @param table la table.
216
     * @param values les valeurs de la lignes.
217
     * @throws IllegalArgumentException si values ne contient pas la clef de la table.
218
     */
19 ilm 219
    public SQLRow(SQLTable table, Map<String, ?> values) {
83 ilm 220
        this(table, null, values);
221
    }
222
 
223
    // allow to call getID() only once
224
    private SQLRow(SQLTable table, final Number id, Map<String, ?> values) {
225
        this(table, id == null ? getID(values, table, false) : id);
17 ilm 226
        // faire une copie, sinon backdoor pour changer les valeurs sans qu'on s'en aperçoive
227
        this.setValues(new HashMap<String, Object>(values));
228
    }
229
 
83 ilm 230
    // return ID, must always be present but may be null if <code>nullAllowed</code>
231
    private static Number getID(Map<String, ?> values, final SQLTable table, final boolean nullAllowed) {
17 ilm 232
        final String keyName = table.getKey().getName();
83 ilm 233
        if (!values.containsKey(keyName))
17 ilm 234
            throw new IllegalArgumentException(values + " does not contain the key of " + table);
83 ilm 235
        final Object keyValue = values.get(keyName);
236
        if (keyValue instanceof Number) {
237
            return (Number) keyValue;
238
        } else if (nullAllowed && keyValue == null) {
239
            return null;
240
        } else {
241
            final String valS = keyValue == null ? "' is null" : "' isn't a Number : " + keyValue.getClass() + " " + keyValue;
242
            throw new IllegalArgumentException("The value of '" + keyName + valS);
243
        }
17 ilm 244
    }
245
 
246
    private Map<String, Object> getValues() {
247
        if (!this.fetched)
248
            this.fetchValues();
249
        return this.values;
250
    }
251
 
252
    /**
253
     * Recharge les valeurs des champs depuis la base.
254
     */
255
    public void fetchValues() {
256
        this.fetchValues(true);
257
    }
258
 
259
    SQLRow fetchValues(final boolean useCache) {
260
        return this.fetchValues(useCache, useCache);
261
    }
262
 
263
    @SuppressWarnings("unchecked")
264
    SQLRow fetchValues(final boolean readCache, final boolean writeCache) {
265
        final IResultSetHandler handler = new IResultSetHandler(SQLDataSource.MAP_HANDLER) {
266
            @Override
267
            public boolean readCache() {
268
                return readCache;
269
            }
270
 
271
            @Override
272
            public boolean writeCache() {
273
                return writeCache;
274
            }
275
 
276
            public Set<SQLRow> getCacheModifiers() {
277
                return Collections.singleton(SQLRow.this);
278
            }
279
        };
280
        this.setValues((Map<String, Object>) this.getTable().getBase().getDataSource().execute(this.getQuery(), handler, false));
281
        return this;
282
    }
283
 
284
    // attention ne vérifie pas que tous les champs soient présents
285
    private final void setValues(Map<String, Object> values) {
286
        this.values = values;
287
        if (!this.fetched)
288
            this.fetched = true;
289
    }
290
 
291
    /**
292
     * Retourne les noms des champs qui ont été chargé depuis la base.
293
     *
294
     * @return les noms des champs qui ont été chargé depuis la base.
295
     */
296
    public Set<String> getFields() {
297
        return Collections.unmodifiableSet(this.getValues().keySet());
298
    }
299
 
300
    private String getQuery() {
301
        return "SELECT * FROM " + this.getTable().getSQLName().quote() + " WHERE " + this.getWhere().getClause();
302
    }
303
 
304
    public Where getWhere() {
305
        return new Where(this.getTable().getKey(), "=", this.getID());
306
    }
307
 
308
    /**
309
     * Est ce que cette ligne existe dans la base de donnée.
310
     *
311
     * @return <code>true</code> si la ligne existait lors de son instanciation.
312
     */
313
    public boolean exists() {
314
        return this.getValues() != null;
315
    }
316
 
317
    /**
318
     * Est ce que cette ligne existe et n'est pas archivée.
319
     *
320
     * @return <code>true</code> si cette ligne est valide.
321
     */
322
    public boolean isValid() {
323
        return this.exists() && this.getID() >= MIN_VALID_ID && !this.isArchived();
324
    }
325
 
326
    public boolean isData() {
327
        return this.isValid() && !this.isUndefined();
328
    }
329
 
330
    /**
331
     * Retourne le champ nommé <code>field</code> de cette ligne.
332
     *
333
     * @param field le nom du champ que l'on veut.
334
     * @return la valeur du champ sous forme d'objet Java, ou <code>null</code> si la valeur est
335
     *         NULL.
336
     * @throws IllegalStateException si cette ligne n'existe pas.
337
     * @throws IllegalArgumentException si cette ligne ne contient pas le champ demandé.
338
     */
339
    public final Object getObject(String field) {
340
        if (!this.exists())
341
            throw new IllegalStateException("The row " + this + "does not exist.");
342
        if (!this.getTable().contains(field))
343
            throw new IllegalArgumentException("The table of the row " + this + " doesn't contain the field '" + field + "'.");
344
        // pour différencier entre la valeur est NULL (SQL) et la ligne ne contient pas ce champ
345
        if (!this.getValues().containsKey(field)) {
346
            // on ne l'a pas fetché
347
            this.fetchValues();
348
            // MAYBE mettre un boolean pour choisir si on accède à la base ou pas
349
            // since we just made a trip to the db we can afford to print at least a message
350
            final String msg = "The row " + this.simpleToString() + " doesn't contain the field '" + field + "' ; refetching.";
351
            Log.get().warning(msg);
352
            if (printSTForMissingField)
353
                new IllegalArgumentException(msg).printStackTrace();
354
        }
355
        return this.getValues().get(field);
356
    }
357
 
73 ilm 358
    public final SQLRow getRow(boolean after) {
17 ilm 359
        final SQLTable t = this.getTable();
360
        final BigDecimal destOrder = this.getOrder();
361
        final int diff = (!after) ? -1 : 1;
362
 
73 ilm 363
        final SQLSelect sel = new SQLSelect();
19 ilm 364
        // undefined must not move
365
        sel.setExcludeUndefined(true);
17 ilm 366
        // unique index prend aussi en compte les archivés
367
        sel.setArchivedPolicy(SQLSelect.BOTH);
368
        sel.addSelect(t.getKey());
369
        sel.addSelect(t.getOrderField());
73 ilm 370
        if (t.isArchivable())
371
            sel.addSelect(t.getArchiveField());
17 ilm 372
        sel.setWhere(new Where(t.getOrderField(), diff < 0 ? "<" : ">", destOrder));
65 ilm 373
        sel.addFieldOrder(t.getOrderField(), diff < 0 ? Order.desc() : Order.asc());
19 ilm 374
        sel.setLimit(1);
17 ilm 375
 
376
        final SQLDataSource ds = t.getBase().getDataSource();
21 ilm 377
        @SuppressWarnings("unchecked")
19 ilm 378
        final Map<String, Object> otherMap = ds.execute1(sel.asString());
379
        if (otherMap != null) {
73 ilm 380
            return new SQLRow(t, otherMap);
381
        } else {
382
            return null;
383
        }
384
    }
385
 
386
    /**
387
     * The free order just after or before this row.
388
     *
389
     * @param after whether to look before or after this row.
390
     * @return a free order, or <code>null</code> if there's no room left.
391
     */
392
    public final BigDecimal getOrder(boolean after) {
393
        final BigDecimal destOrder = this.getOrder();
394
        final SQLRow otherRow = this.getRow(after);
395
        final BigDecimal otherOrder;
396
        if (otherRow != null) {
19 ilm 397
            otherOrder = otherRow.getOrder();
398
        } else if (after) {
17 ilm 399
            // dernière ligne de la table
19 ilm 400
            otherOrder = destOrder.add(ReOrder.DISTANCE);
17 ilm 401
        } else {
19 ilm 402
            // première ligne
403
            otherOrder = ReOrder.MIN_ORDER;
17 ilm 404
        }
405
 
19 ilm 406
        final int decDigits = this.getTable().getOrderDecimalDigits();
17 ilm 407
        final BigDecimal least = BigDecimal.ONE.scaleByPowerOfTen(-decDigits);
408
        final BigDecimal distance = destOrder.subtract(otherOrder).abs();
409
        if (distance.compareTo(least) <= 0)
410
            return null;
411
        else {
19 ilm 412
            final BigDecimal mean = destOrder.add(otherOrder).divide(BigDecimal.valueOf(2));
17 ilm 413
            return DecimalUtils.round(mean, decDigits);
414
        }
415
    }
416
 
25 ilm 417
    @Override
17 ilm 418
    public SQLRow getForeign(String fieldName) {
419
        return this.getForeignRow(fieldName);
420
    }
421
 
25 ilm 422
    @Override
83 ilm 423
    public Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
80 ilm 424
        final SQLRow foreignRow = this.getForeignRow(fieldName, SQLRowMode.NO_CHECK);
83 ilm 425
        return foreignRow == null ? null : foreignRow.getIDNumber();
80 ilm 426
    }
427
 
428
    @Override
17 ilm 429
    public boolean isForeignEmpty(String fieldName) {
430
        final SQLRow foreignRow = this.getForeignRow(fieldName, SQLRowMode.NO_CHECK);
431
        return foreignRow == null || foreignRow.isUndefined();
432
    }
433
 
434
    /**
435
     * Retourne la ligne sur laquelle pointe le champ passé. Elle peut être archivé ou indéfinie.
436
     *
437
     * @param field le nom de la clef externe.
25 ilm 438
     * @return la ligne sur laquelle pointe le champ passé.
17 ilm 439
     * @throws IllegalArgumentException si <code>field</code> n'est pas une clef étrangère de la
440
     *         table de cette ligne.
441
     * @throws IllegalStateException si <code>field</code> contient l'ID d'une ligne inexistante.
442
     */
443
    public SQLRow getForeignRow(String field) {
444
        return this.getForeignRow(field, SQLRowMode.EXIST);
445
    }
446
 
447
    /**
448
     * Retourne la ligne sur laquelle pointe le champ passé.
449
     *
450
     * @param field le nom de la clef externe.
451
     * @param mode quel type de ligne retourner.
452
     * @return la ligne sur laquelle pointe le champ passé, ou <code>null</code> si elle ne
453
     *         correspond pas au mode.
454
     * @throws IllegalArgumentException si <code>field</code> n'est pas une clef étrangère de la
455
     *         table de cette ligne.
456
     * @throws IllegalStateException si <code>field</code> contient l'ID d'une ligne inexistante et
457
     *         que l'on n'en veut pas (mode.wantExisting() == <code>true</code>).
458
     */
459
    public SQLRow getForeignRow(String field, SQLRowMode mode) {
460
        if (!this.getTable().contains(field)) {
461
            throw new IllegalArgumentException(field + " is not a field of " + this.getTable());
462
        }
463
        final SQLField f = this.getTable().getField(field);
464
        if (!this.getTable().getForeignKeys().contains(f)) {
465
            throw new IllegalArgumentException(field + " is not a foreign key of " + this.getTable());
466
        }
467
        return this.getUncheckedForeignRow(this.getTable().getBase().getGraph().getForeignLink(f), mode);
468
    }
469
 
470
    private SQLRow getUncheckedForeignRow(Link foreignLink, SQLRowMode mode) {
471
        final SQLField field = foreignLink.getLabel();
472
        final SQLTable foreignTable = foreignLink.getTarget();
473
        if (this.getObject(field.getName()) == null) {
474
            return null;
475
        } else {
476
            final int foreignID = this.getInt(field.getName());
477
            final SQLRow foreignRow = new SQLRow(foreignTable, foreignID);
478
            // we used to check coherence here before all our dbs had real foreign keys
479
            return mode.filter(foreignRow);
480
        }
481
    }
482
 
483
    /**
484
     * Retourne l'ensemble des lignes de destTable liées à cette ligne.
485
     *
486
     * @param destTable la table dont on veut les lignes, eg "CPI_BT".
487
     * @return l'ensemble des lignes liées à cette ligne, eg les cpis de LOCAL[5822].
488
     * @see #getLinkedRows(String)
489
     */
490
    public Set<SQLRow> getLinkedRows(String destTable) {
491
        return this.getDistantRows(Collections.singletonList(destTable));
492
    }
493
 
494
    /**
495
     * Retourne l'ensemble des lignes de destTable qui sont pointées par celle-ci.
496
     *
497
     * @param destTable la table dont on veut les lignes, eg "OBSERVATION".
498
     * @return l'ensemble des lignes liées à cette ligne, eg les lignes pointées par
499
     *         "ID_OBSERVATION", "ID_OBSERVATION_2", etc.
500
     * @see #getLinkedRows(String)
501
     */
502
    public Set<SQLRow> getForeignRows(String destTable) {
503
        return this.getForeignRows(destTable, SQLRowMode.DATA);
504
    }
505
 
506
    public Set<SQLRow> getForeignRows(String destTable, SQLRowMode mode) {
507
        return new HashSet<SQLRow>(this.getForeignRowsMap(destTable, mode).values());
508
    }
509
 
510
    public Set<SQLRow> getForeignRows() {
511
        return this.getForeignRows(SQLRowMode.DATA);
512
    }
513
 
514
    public Set<SQLRow> getForeignRows(SQLRowMode mode) {
515
        return new HashSet<SQLRow>(this.getForeignRowsMap(mode).values());
516
    }
517
 
518
    /**
519
     * Retourne les lignes de destTable liées à cette ligne, indexées par les clefs externes.
520
     *
521
     * @param destTable la table dont on veut les lignes.
522
     * @return les lignes de destTable liées à cette ligne.
523
     */
524
    public Map<SQLField, SQLRow> getForeignRowsMap(String destTable) {
525
        return this.getForeignRowsMap(destTable, SQLRowMode.DATA);
526
    }
527
 
528
    public Map<SQLField, SQLRow> getForeignRowsMap(String destTable, SQLRowMode mode) {
529
        final Set<Link> links = this.getTable().getDBSystemRoot().getGraph().getForeignLinks(this.getTable(), this.getTable().getTable(destTable));
530
        return this.foreignLinksToMap(links, mode);
531
    }
532
 
533
    public Map<SQLField, SQLRow> getForeignRowsMap() {
534
        return this.getForeignRowsMap(SQLRowMode.DATA);
535
    }
536
 
537
    public Map<SQLField, SQLRow> getForeignRowsMap(SQLRowMode mode) {
538
        final Set<Link> links = this.getTable().getBase().getGraph().getForeignLinks(this.getTable());
539
        return this.foreignLinksToMap(links, mode);
540
    }
541
 
542
    private Map<SQLField, SQLRow> foreignLinksToMap(Collection<Link> links, SQLRowMode mode) {
543
        final Map<SQLField, SQLRow> res = new HashMap<SQLField, SQLRow>();
544
        for (final Link l : links) {
545
            final SQLRow fr = this.getUncheckedForeignRow(l, mode);
546
            if (fr != null)
547
                res.put(l.getLabel(), fr);
548
        }
549
        return res;
550
    }
551
 
552
    /**
553
     * Fait la jointure entre cette ligne et les tables passées.
554
     *
555
     * @param path le chemin de la jointure.
556
     * @return la ligne correspondante.
557
     * @throws IllegalArgumentException si le path est mauvais.
558
     * @throws IllegalStateException si le path ne méne pas à une ligne unique.
559
     * @see #getDistantRows(List)
560
     */
561
    public SQLRow getDistantRow(List<String> path) {
80 ilm 562
        return this.getDistantRow(Path.get(this.getTable()).addTables(path));
65 ilm 563
    }
564
 
565
    public SQLRow getDistantRow(final Path path) {
19 ilm 566
        final Set<SQLRow> rows = this.getDistantRows(path);
17 ilm 567
        if (rows.size() != 1)
568
            throw new IllegalStateException("the path " + path + " does not lead to a unique row (" + rows.size() + ")");
65 ilm 569
        return rows.iterator().next();
17 ilm 570
    }
571
 
572
    /**
573
     * Fait la jointure entre cette ligne et les tables passées.
574
     *
575
     * @param path le chemin de la jointure.
576
     * @return un ensemble de lignes de la dernière table du chemin, dans l'ordre.
577
     * @throws IllegalArgumentException si le path est mauvais.
578
     */
579
    public Set<SQLRow> getDistantRows(List<String> path) {
80 ilm 580
        return this.getDistantRows(Path.get(this.getTable()).addTables(path));
65 ilm 581
    }
582
 
583
    public Set<SQLRow> getDistantRows(final Path path) {
17 ilm 584
        // on veut tous les champs de la derniere table et rien d'autre
65 ilm 585
        final List<List<String>> fields = new ArrayList<List<String>>(Collections.nCopies(path.length() - 1, Collections.<String> emptyList()));
17 ilm 586
        fields.add(null);
587
        final Set<List<SQLRow>> s = this.getRowsOnPath(path, fields);
588
        final Set<SQLRow> res = new LinkedHashSet<SQLRow>(s.size());
589
        for (final List<SQLRow> l : s) {
590
            res.add(l.get(0));
591
        }
592
        return res;
593
    }
594
 
595
    /**
596
     * Retourne les lignes distantes, plus les lignes intermédiaire du chemin. Par exemple
597
     * SITE[128].getRowsOnPath("BATIMENT,LOCAL", [null, "DESIGNATION"]) retourne tous les locaux du
598
     * site (seul DESIGNATION est chargé) avec tous les champs de leurs bâtiments.
599
     *
65 ilm 600
     * @param path le chemin dans le graphe de la base, see {@link Path#addTables(List)}.
17 ilm 601
     * @param fields un liste de des champs, chaque élément est :
602
     *        <ul>
603
     *        <li><code>null</code> pour tous les champs</li>
21 ilm 604
     *        <li>une Collection de nom de champs, e.g. ["DESIGNATION","NUMERO"]</li>
17 ilm 605
     *        </ul>
606
     * @return un ensemble de List de SQLRow.
607
     */
21 ilm 608
    public Set<List<SQLRow>> getRowsOnPath(final List<String> path, final List<? extends Collection<String>> fields) {
80 ilm 609
        return this.getRowsOnPath(Path.get(this.getTable()).addTables(path), fields);
65 ilm 610
    }
611
 
612
    public Set<List<SQLRow>> getRowsOnPath(final Path p, final List<? extends Collection<String>> fields) {
613
        final int pathSize = p.length();
17 ilm 614
        if (pathSize == 0)
615
            throw new IllegalArgumentException("path is empty");
616
        if (pathSize != fields.size())
617
            throw new IllegalArgumentException("path and fields size mismatch : " + pathSize + " != " + fields.size());
65 ilm 618
        if (p.getFirst() != this.getTable())
619
            throw new IllegalArgumentException("path doesn't start with us : " + p.getFirst() + " != " + this.getTable());
17 ilm 620
        final Set<List<SQLRow>> res = new LinkedHashSet<List<SQLRow>>();
621
 
65 ilm 622
        final DBSystemRoot sysRoot = this.getTable().getDBSystemRoot();
623
        Where where = sysRoot.getGraph().getJointure(p);
17 ilm 624
        // ne pas oublier de sélectionner notre ligne
625
        where = where.and(this.getWhere());
626
 
80 ilm 627
        final SQLSelect select = new SQLSelect();
17 ilm 628
 
21 ilm 629
        final List<Collection<String>> fieldsCols = new ArrayList<Collection<String>>(pathSize);
17 ilm 630
        for (int i = 0; i < pathSize; i++) {
21 ilm 631
            final Collection<String> tableFields = fields.get(i);
17 ilm 632
            // +1 car p contient cette ligne
633
            final SQLTable t = p.getTable(i + 1);
21 ilm 634
            final Collection<String> fieldsCol;
635
            if (tableFields == null) {
636
                fieldsCol = t.getFieldsName();
17 ilm 637
            } else {
21 ilm 638
                fieldsCol = tableFields;
17 ilm 639
            }
640
            fieldsCols.add(fieldsCol);
641
 
642
            // les tables qui ne nous interessent pas
643
            if (fieldsCol.size() > 0) {
644
                // toujours mettre l'ID
645
                select.addSelect(t.getKey());
646
                // plus les champs demandés
21 ilm 647
                select.addAllSelect(t, fieldsCol);
17 ilm 648
            }
649
        }
650
        // dans tous les cas mettre l'ID de la dernière table
651
        final SQLTable lastTable = p.getLast();
652
        select.addSelect(lastTable.getKey());
653
 
654
        // on ajoute une SQLRow pour chaque ID trouvé
655
        select.setWhere(where).addOrderSilent(lastTable.getName());
65 ilm 656
        sysRoot.getDataSource().execute(select.asString(), new ResultSetHandler() {
17 ilm 657
 
658
            public Object handle(ResultSet rs) throws SQLException {
659
                final ResultSetMetaData rsmd = rs.getMetaData();
660
                while (rs.next()) {
661
                    final List<SQLRow> rows = new ArrayList<SQLRow>(pathSize);
662
                    for (int i = 0; i < pathSize; i++) {
663
                        // les tables qui ne nous interessent pas
664
                        if (fieldsCols.get(i).size() > 0) {
665
                            // +1 car p contient cette ligne
666
                            final SQLTable t = p.getTable(i + 1);
667
                            rows.add(SQLRow.createFromRS(t, rs, rsmd, pathSize == 1));
668
                        }
669
                    }
670
                    res.add(rows);
671
                }
672
                return null;
673
            }
674
        });
675
 
676
        return res;
677
    }
678
 
679
    /**
680
     * Retourne les lignes pointant sur celle ci.
681
     *
682
     * @return les lignes pointant sur celle ci.
683
     */
684
    public final List<SQLRow> getReferentRows() {
685
        return this.getReferentRows((Set<SQLTable>) null);
686
    }
687
 
688
    @Override
689
    public final List<SQLRow> getReferentRows(SQLTable refTable) {
690
        return this.getReferentRows(Collections.singleton(refTable));
691
    }
692
 
693
    /**
694
     * Retourne les lignes des tables spécifiées pointant sur celle ci.
695
     *
696
     * @param tables les tables voulues, <code>null</code> pour toutes.
697
     * @return les SQLRow pointant sur celle ci.
698
     */
699
    public final List<SQLRow> getReferentRows(Set<SQLTable> tables) {
700
        return this.getReferentRows(tables, SQLSelect.UNARCHIVED);
701
    }
702
 
703
    /**
704
     * Returns the rows of tables that points to this row.
705
     *
706
     * @param tables a Set of tables, or <code>null</code> for all of them.
707
     * @param archived <code>SQLSelect.UNARCHIVED</code>, <code>SQLSelect.ARCHIVED</code> or
708
     *        <code>SQLSelect.BOTH</code>.
709
     * @return a List of SQLRow that points to this.
710
     */
711
    public final List<SQLRow> getReferentRows(Set<SQLTable> tables, ArchiveMode archived) {
83 ilm 712
        return new ArrayList<SQLRow>(this.getReferentRowsByLink(tables, archived).allValues());
17 ilm 713
    }
714
 
83 ilm 715
    public final ListMap<Link, SQLRow> getReferentRowsByLink() {
17 ilm 716
        return this.getReferentRowsByLink(null);
717
    }
718
 
83 ilm 719
    public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables) {
17 ilm 720
        return this.getReferentRowsByLink(tables, SQLSelect.UNARCHIVED);
721
    }
722
 
83 ilm 723
    public final ListMap<Link, SQLRow> getReferentRowsByLink(Set<SQLTable> tables, ArchiveMode archived) {
724
        // List since getReferentRows() is ordered
725
        final ListMap<Link, SQLRow> res = new ListMap<Link, SQLRow>();
17 ilm 726
        final Set<Link> links = this.getTable().getBase().getGraph().getReferentLinks(this.getTable());
727
        for (final Link l : links) {
728
            final SQLTable src = l.getSource();
729
            if (tables == null || tables != null && tables.contains(src)) {
83 ilm 730
                res.addAll(l, this.getReferentRows(l.getLabel(), archived));
17 ilm 731
            }
732
        }
733
        return res;
734
    }
735
 
736
    /**
737
     * Returns the rows that points to this row by the refField.
738
     *
739
     * @param refField a SQLField that points to the table of this row, eg BATIMENT.ID_SITE.
740
     * @return a List of SQLRow that points to this, eg [BATIMENT[123], BATIMENT[124]].
741
     */
742
    public List<SQLRow> getReferentRows(final SQLField refField) {
743
        return this.getReferentRows(refField, SQLSelect.UNARCHIVED);
744
    }
745
 
746
    public List<SQLRow> getReferentRows(final SQLField refField, final ArchiveMode archived) {
747
        return this.getReferentRows(refField, archived, null);
748
    }
749
 
750
    /**
751
     * Returns the rows that points to this row by <code>refField</code>.
752
     *
753
     * @param refField a SQLField that points to the table of this row, eg BATIMENT.ID_SITE.
754
     * @param archived specify which rows should be returned.
755
     * @param fields the list of fields the rows will have, <code>null</code> meaning all.
756
     * @return a List of SQLRow that points to this, eg [BATIMENT[123], BATIMENT[124]].
757
     */
758
    public List<SQLRow> getReferentRows(final SQLField refField, final ArchiveMode archived, final Collection<String> fields) {
759
        final SQLTable foreignTable = refField.getTable().getBase().getGraph().getForeignTable(refField);
760
        if (!foreignTable.equals(this.getTable())) {
761
            throw new IllegalArgumentException(refField + " doesn't point to " + this.getTable());
762
        }
763
 
764
        final SQLTable src = refField.getTable();
80 ilm 765
        final SQLSelect sel = new SQLSelect();
17 ilm 766
        if (fields == null)
767
            sel.addSelectStar(src);
768
        else {
769
            sel.addSelect(src.getKey());
770
            for (final String f : fields)
771
                sel.addSelect(src.getField(f));
772
        }
773
        sel.setWhere(new Where(refField, "=", this.getID()));
774
        sel.setArchivedPolicy(archived);
775
        sel.addOrderSilent(src.getName());
776
        // - if some other criteria need to be applied, we could pass an SQLRowMode (instead of
777
        // just ArchiveMode) and modify the SQLSelect accordingly
778
 
21 ilm 779
        return SQLRowListRSH.execute(sel);
17 ilm 780
    }
781
 
782
    /**
783
     * Toutes les lignes qui touchent cette lignes. C'est à dire les lignes pointées par les clefs
784
     * externes plus lignes qui pointent sur cette ligne.
785
     *
786
     * @return les lignes qui touchent cette lignes.
787
     */
788
    private Set<SQLRow> getConnectedRows() {
789
        Set<SQLRow> res = new HashSet<SQLRow>();
790
        res.addAll(this.getReferentRows((Set<SQLTable>) null, SQLSelect.BOTH));
791
        res.addAll(this.getForeignRows(SQLRowMode.EXIST));
792
        return res;
793
    }
794
 
67 ilm 795
    @Override
796
    public Collection<SQLRow> followLink(Link l, Direction direction) {
797
        // Path checks that one end of l is this table and that direction is valid (e.g. not ANY for
798
        // self-reference links)
80 ilm 799
        final boolean backwards = Path.get(getTable()).add(l, direction).isBackwards(0);
67 ilm 800
        if (backwards)
801
            return getReferentRows(l.getSingleField());
802
        else
803
            return Collections.singletonList(getForeign(l.getSingleField().getName()));
804
    }
805
 
17 ilm 806
    /**
807
     * Trouve les lignes archivées reliées à celle ci par moins de maxLength liens.
808
     *
809
     * @param maxLength la longeur maximale du chemin entre les lignes retournées et celle ci.
810
     * @return les lignes archivées reliées à celle ci.
811
     */
812
    public Set<SQLRow> findDistantArchived(int maxLength) {
813
        return this.findDistantArchived(maxLength, new HashSet<SQLRow>(), 0);
814
    }
815
 
816
    private Set<SQLRow> findDistantArchived(final int maxLength, final Set<SQLRow> been, int length) {
817
        final Set<SQLRow> res = new HashSet<SQLRow>();
818
 
819
        if (maxLength == length)
820
            return res;
821
 
822
        // on avance d'un cran
823
        been.add(this);
824
        length++;
825
 
826
        // on garde les lignes à appeler récursivement pour la fin
827
        // car on veut parcourir en largeur d'abord
828
        final Set<SQLRow> rec = new HashSet<SQLRow>();
829
        Iterator<SQLRow> iter = this.getConnectedRows().iterator();
830
        while (iter.hasNext()) {
831
            final SQLRow row = iter.next();
832
            if (!been.contains(row)) {
833
                if (row.isArchived()) {
834
                    res.add(row);
835
                } else {
836
                    rec.add(row);
837
                }
838
            }
839
        }
840
        iter = rec.iterator();
841
        while (iter.hasNext()) {
842
            final SQLRow row = iter.next();
843
            res.addAll(row.findDistantArchived(maxLength, been, length));
844
        }
845
        return res;
846
    }
847
 
848
    // ATTN peut faire une requête si archive n'est pas chargé
849
    public String toString() {
850
        String res = this.simpleToString();
851
        if (!this.exists()) {
852
            res = "?" + res + "?";
853
        } else if (this.isArchived()) {
854
            res = "(" + res + ")";
855
        }
856
        return res;
857
    }
858
 
859
    public String simpleToString() {
860
        return this.getTable().getName() + "[" + this.ID + "]";
861
    }
862
 
863
    /**
864
     * Renvoie tous les champs de cette ligne, clef comprises. En général on ne veut pas les valeurs
865
     * des clefs, voir getAllValues().
866
     * <p>
867
     * Les valeurs de cette map sont les valeurs retournées par getObject().
868
     * </p>
869
     *
870
     * @return tous les champs de cette ligne.
871
     * @see #getAllValues()
872
     * @see #getObject(String)
873
     */
874
    @Override
875
    public Map<String, Object> getAbsolutelyAll() {
876
        return Collections.unmodifiableMap(this.getValues());
877
    }
878
 
879
    /**
880
     * Retourne toutes les valeurs de cette lignes, sans les clefs ni les champs d'ordre et
881
     * d'archive.
882
     *
883
     * @return toutes les valeurs de cette lignes.
884
     * @see #getAbsolutelyAll()
885
     */
886
    public Map<String, Object> getAllValues() {
887
        // commence par tout copier
888
        final Map<String, Object> res = new HashMap<String, Object>(this.getValues());
19 ilm 889
        final Set<SQLField> keys = this.getTable().getKeys();
17 ilm 890
        // puis on enlève les clefs, l'ordre et l'archive
891
        CollectionUtils.filter(res.keySet(), new Predicate() {
892
            public boolean evaluate(Object object) {
893
                final SQLField field = getTable().getField((String) object);
894
                return !keys.contains(field) && field != getTable().getOrderField() && field != getTable().getArchiveField();
895
            }
896
        });
897
        return res;
898
    }
899
 
900
    /**
901
     * Creates a SQLRowValues with absolutely all the values of this row. ATTN the values are as
902
     * always the ones at the moment of the last fetching.
903
     *
904
     * <pre>
905
     * SQLRow r = table.getRow(123); // [a=&gt;'26', b=&gt; '25']
906
     * r.createUpdateRow().put(&quot;a&quot;, 1).update();
907
     * r.createUpdateRow().put(&quot;b&quot;, 2).update();
908
     * </pre>
909
     *
910
     * You could think that r now equals [a=>1, b=>2]. No, actually it's [a=>'26', b=>2], because
911
     * the second line overwrote the first one. The best solution is to use only one SQLRowValues
912
     * (hence only one access to the DB), otherwise use createEmptyUpdateRow().
913
     *
914
     * @see #createEmptyUpdateRow()
915
     * @return a SQLRowValues on this SQLRow.
916
     */
917
    public SQLRowValues createUpdateRow() {
83 ilm 918
        return new SQLRowValues(this.getTable(), this.getValues());
17 ilm 919
    }
920
 
921
    /**
922
     * Creates a SQLRowValues with just this ID, and no other values.
923
     *
924
     * @return a SQLRowValues on this SQLRow.
925
     */
926
    @Override
927
    public SQLRowValues createEmptyUpdateRow() {
928
        final SQLRowValues res = new SQLRowValues(this.getTable());
929
        res.put(this.getTable().getKey().getName(), this.getIDNumber());
930
        return res;
931
    }
932
 
933
    /**
934
     * Gets the unique (among this table at least) identifier of this row.
935
     *
936
     * @return an int greater than {@link #MIN_VALID_ID} if this is valid.
937
     */
938
    @Override
939
    public int getID() {
940
        return this.ID;
941
    }
942
 
943
    @Override
944
    public Number getIDNumber() {
945
        return this.idNumber;
946
    }
947
 
948
    @Override
949
    public SQLRow asRow() {
950
        return this;
951
    }
952
 
953
    @Override
954
    public final SQLRowValues asRowValues() {
955
        return this.createUpdateRow();
956
    }
957
 
958
    /**
959
     * Note : ne compare pas les valeurs des champs de cette ligne.
960
     *
961
     * @see java.lang.Object#equals(java.lang.Object)
962
     */
963
    public boolean equals(Object other) {
964
        if (!(other instanceof SQLRow))
965
            return false;
966
        SQLRow o = (SQLRow) other;
967
        return this.equalsAsRow(o);
968
    }
969
 
970
    public int hashCode() {
971
        return this.hashCodeAsRow();
972
    }
973
 
974
    /**
975
     * Transforme un chemin en une liste de nom de table. Si path est "" alors retourne une liste
976
     * vide.
977
     *
978
     * @param path le chemin, eg "BATIMENT,LOCAL".
979
     * @return une liste de String, eg ["BATIMENT","LOCAL"].
980
     */
981
    static public List<String> toList(String path) {
982
        return Arrays.asList(toArray(path));
983
    }
984
 
985
    static private String[] toArray(String path) {
986
        if (path.length() == 0)
987
            return new String[0];
988
        else
989
            // ATTN ',' : no spaces
990
            return path.split(",");
991
    }
992
 
25 ilm 993
    @Override
994
    public SQLTableModifiedListener createTableListener(SQLDataListener l) {
17 ilm 995
        return new SQLTableListenerData<SQLRow>(this, l);
996
    }
997
 
998
}