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
 package org.openconcerto.sql.model;
15
 
142 ilm 16
import org.openconcerto.sql.Log;
182 ilm 17
import org.openconcerto.sql.model.SQLTable.VirtualFields;
142 ilm 18
import org.openconcerto.sql.model.graph.DatabaseGraph;
67 ilm 19
import org.openconcerto.sql.model.graph.Link;
20
import org.openconcerto.sql.model.graph.Link.Direction;
21
import org.openconcerto.sql.model.graph.Step;
142 ilm 22
import org.openconcerto.utils.NumberUtils;
23
import org.openconcerto.utils.Value;
132 ilm 24
import org.openconcerto.utils.cc.HashingStrategy;
17 ilm 25
import org.openconcerto.utils.convertor.StringClobConvertor;
26
 
67 ilm 27
import java.math.BigDecimal;
17 ilm 28
import java.sql.Clob;
29
import java.text.DateFormat;
182 ilm 30
import java.util.ArrayList;
17 ilm 31
import java.util.Calendar;
32
import java.util.Collection;
142 ilm 33
import java.util.Collections;
17 ilm 34
import java.util.Date;
132 ilm 35
import java.util.HashSet;
36
import java.util.LinkedHashMap;
142 ilm 37
import java.util.List;
17 ilm 38
import java.util.Locale;
39
import java.util.Map;
40
import java.util.Set;
142 ilm 41
import java.util.logging.Level;
17 ilm 42
 
43
/**
44
 * A class that represent a row of a table. The row might not acutally exists in the database, and
45
 * it might not define all the fields.
46
 *
83 ilm 47
 * <table border="1">
48
 * <caption>Primary Key</caption> <thead>
49
 * <tr>
50
 * <th><code>ID</code> value</th>
51
 * <th>{@link #hasID()}</th>
52
 * <th>{@link #getIDNumber()}</th>
53
 * <th>{@link #isUndefined()}</th>
54
 * </tr>
55
 * </thead> <tbody>
56
 * <tr>
57
 * <th>∅</th>
58
 * <td><code>false</code></td>
59
 * <td><code>null</code></td>
60
 * <td><code>false</code></td>
61
 * </tr>
62
 * <tr>
63
 * <th><code>null</code></th>
64
 * <td><code>false</code> :<br/>
65
 * no row in the DB can have a <code>null</code> primary key</td>
66
 * <td><code>null</code></td>
67
 * <td><code>false</code><br/>
68
 * (even if getUndefinedIDNumber() is <code>null</code>, see method documentation)</td>
69
 * </tr>
70
 * <tr>
71
 * <th><code>instanceof Number</code></th>
72
 * <td><code>true</code></td>
73
 * <td><code>Number</code></td>
74
 * <td>if equals <code>getUndefinedID()</code></td>
75
 * </tr>
76
 * <tr>
77
 * <th><code>else</code></th>
78
 * <td><code>ClassCastException</code></td>
79
 * <td><code>ClassCastException</code></td>
80
 * <td><code>ClassCastException</code></td>
81
 * </tr>
82
 * </tbody>
83
 * </table>
84
 * <br/>
85
 * <table border="1">
86
 * <caption>Foreign Keys</caption> <thead>
87
 * <tr>
88
 * <th><code>ID</code> value</th>
89
 * <th>{@link #getForeignIDNumber(String)}</th>
90
 * <th>{@link #isForeignEmpty(String)}</th>
91
 * </tr>
92
 * </thead> <tbody>
93
 * <tr>
94
 * <th>∅</th>
95
 * <td><code>Exception</code></td>
96
 * <td><code>Exception</code></td>
97
 * </tr>
98
 * <tr>
99
 * <th><code>null</code></th>
100
 * <td><code>null</code></td>
101
 * <td>if equals <code>getUndefinedID()</code></td>
102
 * </tr>
103
 * <tr>
104
 * <th><code>instanceof Number</code></th>
105
 * <td><code>Number</code></td>
106
 * <td>if equals <code>getUndefinedID()</code></td>
107
 * </tr>
108
 * <tr>
109
 * <tr>
110
 * <th><code>instanceof SQLRowValues</code></th>
111
 * <td><code>getIDNumber()</code></td>
112
 * <td><code>isUndefined()</code></td>
113
 * </tr>
114
 * <th><code>else</code></th>
115
 * <td><code>ClassCastException</code></td>
116
 * <td><code>ClassCastException</code></td>
132 ilm 117
 * </tr>
118
 * </tbody>
83 ilm 119
 * </table>
120
 *
17 ilm 121
 * @author Sylvain CUAZ
122
 */
123
public abstract class SQLRowAccessor implements SQLData {
124
 
142 ilm 125
    @Deprecated
126
    static public final String ACCESS_DB_IF_NEEDED_PROP = "SQLRowAccessor.accessDBIfNeeded";
127
    static private final boolean ACCESS_DB_IF_NEEDED = Boolean.parseBoolean(System.getProperty(ACCESS_DB_IF_NEEDED_PROP, "false"));
128
 
129
    public static boolean getAccessDBIfNeeded() {
130
        return ACCESS_DB_IF_NEEDED;
131
    }
132
 
132 ilm 133
    static private final HashingStrategy<SQLRowAccessor> ROW_STRATEGY = new HashingStrategy<SQLRowAccessor>() {
134
        @Override
135
        public int computeHashCode(SQLRowAccessor object) {
136
            return object.hashCodeAsRow();
137
        }
138
 
139
        @Override
140
        public boolean equals(SQLRowAccessor object1, SQLRowAccessor object2) {
141
            return object1.equalsAsRow(object2);
142
        }
143
    };
144
 
145
    /**
146
     * A strategy to compare instances as {@link SQLRow}, i.e. only {@link #getTable() table} and
147
     * {@link #getID() id}.
148
     *
149
     * @return a strategy.
150
     * @see #equalsAsRow(SQLRowAccessor)
151
     */
152
    public static final HashingStrategy<SQLRowAccessor> getRowStrategy() {
153
        return ROW_STRATEGY;
154
    }
155
 
156
    static public final Set<Number> getIDs(final Collection<? extends SQLRowAccessor> rows) {
157
        return getIDs(rows, new HashSet<Number>());
158
    }
159
 
160
    static public final <C extends Collection<? super Number>> C getIDs(final Collection<? extends SQLRowAccessor> rows, final C res) {
161
        for (final SQLRowAccessor r : rows)
162
            res.add(r.getIDNumber());
163
        return res;
164
    }
165
 
17 ilm 166
    private final SQLTable table;
167
 
168
    protected SQLRowAccessor(SQLTable table) {
169
        super();
170
        if (table == null)
171
            throw new NullPointerException("null SQLTable");
172
        this.table = table;
173
    }
174
 
175
    public final SQLTable getTable() {
176
        return this.table;
177
    }
178
 
179
    /**
83 ilm 180
     * Whether this row has a Number for the primary key.
181
     *
182
     * @return <code>true</code> if the value of the primary key is specified and is a non
183
     *         <code>null</code> number, <code>false</code> if the value isn't specified or if it's
184
     *         <code>null</code>.
185
     * @throws ClassCastException if value is not <code>null</code> and not a {@link Number}.
186
     */
187
    public final boolean hasID() throws ClassCastException {
188
        return this.getIDNumber() != null;
189
    }
190
 
191
    /**
17 ilm 192
     * Returns the ID of the represented row.
193
     *
194
     * @return the ID, or {@link SQLRow#NONEXISTANT_ID} if this row is not linked to the DB.
195
     */
196
    public abstract int getID();
197
 
198
    public abstract Number getIDNumber();
199
 
83 ilm 200
    /**
182 ilm 201
     * Get the row reference.
202
     *
203
     * @return the row reference, <code>null</code> if some fields of the
204
     *         {@link SQLTable#getPrimaryKeyFields() primary key} are missing or <code>null</code>.
205
     */
206
    public final RowRef getRowRef() {
207
        final List<String> pkFields = this.getTable().getPKsNames();
208
        final List<Object> pk = new ArrayList<>(pkFields.size());
209
        for (final String f : pkFields) {
210
            final Object o = this.getObject(f);
211
            // Don't need contains() because a PK cannot contain NULL in the DB.
212
            if (o == null)
213
                return null;
214
            pk.add(o);
215
        }
216
        return new RowRef(this.getTable(), Collections.unmodifiableList(pk), true);
217
    }
218
 
219
    /**
83 ilm 220
     * Whether this row is the undefined row. Return <code>false</code> if both the
221
     * {@link #getIDNumber() ID} and {@link SQLTable#getUndefinedIDNumber()} are <code>null</code>
222
     * since no row can have <code>null</code> primary key in the database. IOW when
223
     * {@link SQLTable#getUndefinedIDNumber()} is <code>null</code> the empty
224
     * <strong>foreign</strong> keys are <code>null</code>.
225
     *
226
     * @return <code>true</code> if the ID is specified, not <code>null</code> and is equal to the
227
     *         {@link SQLTable#getUndefinedIDNumber() undefined} ID.
228
     */
17 ilm 229
    public final boolean isUndefined() {
83 ilm 230
        final Number id = this.getIDNumber();
231
        return id != null && id.intValue() == this.getTable().getUndefinedID();
17 ilm 232
    }
233
 
234
    /**
83 ilm 235
     * Est ce que cette ligne est archivée.
236
     *
237
     * @return <code>true</code> si la ligne était archivée lors de son instanciation.
238
     */
132 ilm 239
    public final boolean isArchived() {
240
        return this.isArchived(true);
241
    }
242
 
243
    protected final boolean isArchived(final boolean allowDBAccess) {
83 ilm 244
        // si il n'y a pas de champs archive, elle n'est pas archivée
132 ilm 245
        final SQLField archiveField = this.getTable().getArchiveField();
246
        if (archiveField == null)
83 ilm 247
            return false;
132 ilm 248
        final Object archiveVal = this.getRequiredObject(archiveField.getName(), allowDBAccess);
249
        if (archiveField.getType().getJavaType().equals(Boolean.class))
250
            return ((Boolean) archiveVal).booleanValue();
83 ilm 251
        else
132 ilm 252
            return ((Number) archiveVal).intValue() > 0;
83 ilm 253
    }
254
 
182 ilm 255
    public abstract SQLRowAccessor toImmutable();
256
 
257
    public abstract boolean isFrozen();
258
 
83 ilm 259
    /**
80 ilm 260
     * Creates an SQLRow from these values, without any DB access.
17 ilm 261
     *
262
     * @return an SQLRow with the same values as this.
263
     */
182 ilm 264
    public final SQLRow asRow() {
265
        return this.asRow(null);
266
    }
17 ilm 267
 
182 ilm 268
    public abstract SQLRow asRow(final Boolean immutable);
269
 
17 ilm 270
    /**
182 ilm 271
     * Return an immutable SQLRow with only the passed fields retained. I.e. the returned instance
272
     * can consume less memory.
273
     *
274
     * @param fields which fields to retain.
275
     * @return a {@link #isFrozen() frozen} SQLRow.
276
     */
277
    public final SQLRow trimmedRow(final VirtualFields fields) {
278
        final Set<String> fieldsNames = this.getTable().getFieldsNames(fields);
279
        if (this instanceof SQLRow && this.isFrozen() && fieldsNames.containsAll(this.getFields()))
280
            return (SQLRow) this;
281
        return SQLRow.trim(this, SQLRowAccessor::getValues, fieldsNames);
282
    }
283
 
284
    public final SQLRow fetchNewRow() {
285
        return this.fetchNewRow(true);
286
    }
287
 
288
    /**
289
     * Return a new instance with up-to-date values.
290
     *
291
     * @param useCache <code>true</code> to use the {@link SQLDataSource#isCacheEnabled() cache}.
292
     * @return a new instance.
293
     */
294
    public final SQLRow fetchNewRow(final boolean useCache) {
295
        return new SQLRow(this.getTable(), this.getID()).fetchValues(useCache);
296
    }
297
 
298
    /**
80 ilm 299
     * Creates an SQLRowValues from these values, without any DB access.
17 ilm 300
     *
301
     * @return an SQLRowValues with the same values as this.
302
     */
182 ilm 303
    public final SQLRowValues asRowValues() {
304
        return this.asRowValues(null);
305
    }
17 ilm 306
 
307
    /**
182 ilm 308
     * Creates an SQLRowValues from these values, without any DB access.
309
     *
310
     * @param immutable <code>true</code> if the result must be
311
     *        {@link SQLRowValuesCluster#isFrozen() frozen}, <code>false</code> if it must not,
312
     *        <code>null</code> if the caller doesn't care and just wants the fastest result.
313
     * @return an SQLRowValues with the same values as this.
314
     */
315
    public abstract SQLRowValues asRowValues(final Boolean immutable);
316
 
317
    /**
17 ilm 318
     * Creates an SQLRowValues with just this ID, and no other values.
319
     *
320
     * @return an empty SQLRowValues.
321
     */
132 ilm 322
    public final SQLRowValues createEmptyUpdateRow() {
323
        return new SQLRowValues(this.getTable()).setID(this.getIDNumber());
324
    }
17 ilm 325
 
326
    /**
327
     * Return the fields defined by this instance.
328
     *
329
     * @return a Set of field names.
330
     */
331
    public abstract Set<String> getFields();
332
 
151 ilm 333
    public boolean contains(final String fieldName) {
334
        return this.getFields().contains(fieldName);
335
    }
336
 
17 ilm 337
    public abstract Object getObject(String fieldName);
338
 
182 ilm 339
    public abstract Object getObjectNoCheck(String fieldName);
340
 
17 ilm 341
    /**
132 ilm 342
     * Return the value for the passed field only if already present in this instance.
343
     *
344
     * @param fieldName a field name.
345
     * @return the existing value for the passed field.
346
     * @throws IllegalArgumentException if there's no value for the passed field.
347
     */
348
    public final Object getContainedObject(String fieldName) throws IllegalArgumentException {
349
        return this.getObject(fieldName, true);
350
    }
351
 
352
    protected final Object getRequiredObject(String fieldName, final boolean allowDBAccess) throws IllegalArgumentException {
353
        // SQLRowValues cannot add a field value, so required means mustBePresent
354
        // SQLRow.getOject() can add and also checks whether the passed field is in its table, i.e.
355
        // fields are always required.
356
        return this.getObject(fieldName, this instanceof SQLRowValues || !allowDBAccess);
357
    }
358
 
359
    // MAYBE change paramter to enum MissingMode = THROW_EXCEPTION, ADD, RETURN_NULL
360
    public final Object getObject(String fieldName, final boolean mustBePresent) throws IllegalArgumentException {
151 ilm 361
        if (mustBePresent && !this.contains(fieldName)) {
362
            final String msg;
363
            if (this.getTable().contains(fieldName))
364
                msg = "Field " + fieldName + " not present in this : " + this.getFields() + " but exists in " + this.getTable();
365
            else
366
                msg = "Field " + fieldName + " neither present in this : " + this.getFields() + " nor " + this.getTable();
367
            throw new IllegalArgumentException(msg);
368
        }
132 ilm 369
        return this.getObject(fieldName);
370
    }
371
 
372
    /**
17 ilm 373
     * All objects in this row.
374
     *
375
     * @return an immutable map.
376
     */
377
    public abstract Map<String, Object> getAbsolutelyAll();
378
 
132 ilm 379
    public final Map<String, Object> getValues(final SQLTable.VirtualFields vFields) {
380
        return this.getValues(this.getTable().getFieldsNames(vFields));
381
    }
382
 
383
    public final Map<String, Object> getValues(final Collection<String> fields) {
384
        return this.getValues(fields, false);
385
    }
386
 
17 ilm 387
    /**
132 ilm 388
     * Return the values of this row for the passed fields.
389
     *
390
     * @param fields the keys.
391
     * @param includeMissingKeys <code>true</code> if a field only in the parameter should be
392
     *        returned with a <code>null</code> value (i.e. the result might contains fields not in
393
     *        {@link #getFields()}), <code>false</code> to not include it in the result (i.e. the
394
     *        fields of the result will be a subset of {@link #getFields()}).
395
     * @return the values of the passed fields.
396
     */
397
    public final Map<String, Object> getValues(final Collection<String> fields, final boolean includeMissingKeys) {
151 ilm 398
        this.initValues();
132 ilm 399
        final Map<String, Object> res = new LinkedHashMap<String, Object>();
400
        final Set<String> thisFields = this.getFields();
401
        for (final String f : fields) {
402
            if (includeMissingKeys || thisFields.contains(f))
403
                res.put(f, this.getObject(f));
404
        }
405
        return res;
406
    }
407
 
151 ilm 408
    protected void initValues() {
409
    }
410
 
132 ilm 411
    /**
17 ilm 412
     * Retourne le champ nommé <code>field</code> de cette ligne. Cette méthode formate la valeur en
413
     * fonction de son type, par exemple une date sera localisée.
414
     *
415
     * @param field le nom du champ que l'on veut.
416
     * @return la valeur du champ sous forme de chaine, ou <code>null</code> si la valeur est NULL.
417
     */
418
    public final String getString(String field) {
419
        String result = null;
420
        Object obj = this.getObject(field);
421
        if (obj == null) {
422
            result = null;
423
        } else if (obj instanceof Date) {
424
            DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault());
425
            result = df.format((Date) obj);
426
        } else if (obj instanceof Clob) {
427
            try {
428
                result = StringClobConvertor.INSTANCE.unconvert((Clob) obj);
429
            } catch (Exception e) {
430
                e.printStackTrace();
431
                result = obj.toString();
432
            }
433
        } else {
434
            result = obj.toString();
435
        }
436
        return result;
437
    }
438
 
439
    /**
440
     * Retourne le champ nommé <code>field</code> de cette ligne.
441
     *
442
     * @param field le nom du champ que l'on veut.
443
     * @return la valeur du champ sous forme d'int, ou <code>0</code> si la valeur est NULL.
444
     */
445
    public final int getInt(String field) {
446
        return getObjectAs(field, Number.class).intValue();
447
    }
448
 
449
    public final long getLong(String field) {
450
        return getObjectAs(field, Number.class).longValue();
451
    }
452
 
453
    public final float getFloat(String field) {
454
        return getObjectAs(field, Number.class).floatValue();
455
    }
456
 
457
    public final Boolean getBoolean(String field) {
458
        return getObjectAs(field, Boolean.class);
459
    }
460
 
67 ilm 461
    public final BigDecimal getBigDecimal(String field) {
462
        return getObjectAs(field, BigDecimal.class);
463
    }
464
 
17 ilm 465
    public final Calendar getDate(String field) {
466
        final Date d = this.getObjectAs(field, Date.class);
467
        if (d == null)
468
            return null;
469
 
470
        final Calendar cal = Calendar.getInstance();
471
        cal.setTime(d);
472
        return cal;
473
    }
474
 
475
    public final <T> T getObjectAs(String field, Class<T> clazz) {
156 ilm 476
        return this.getObjectAs(field, false, clazz);
477
    }
478
 
479
    public final <T> T getObjectAs(final String field, final boolean mustBePresent, final Class<T> clazz) {
17 ilm 480
        T res = null;
481
        try {
156 ilm 482
            res = clazz.cast(this.getObject(field, mustBePresent));
17 ilm 483
        } catch (ClassCastException e) {
484
            throw new IllegalArgumentException("Impossible d'accéder au champ " + field + " de la ligne " + this + " en tant que " + clazz.getSimpleName(), e);
485
        }
486
        return res;
487
    }
488
 
25 ilm 489
    /**
142 ilm 490
     * Returns the foreign table of <i>fieldName</i>.
491
     *
492
     * @param fieldName the name of a foreign field, e.g. "ID_ARTICLE_2".
493
     * @return the table the field points to (never <code>null</code>), e.g. |ARTICLE|.
494
     * @throws IllegalArgumentException if <i>fieldName</i> is not a foreign field.
495
     */
496
    protected final SQLTable getForeignTable(String fieldName) throws IllegalArgumentException {
497
        return this.getForeignLink(Collections.singletonList(fieldName)).getTarget();
498
    }
499
 
500
    protected final Link getForeignLink(final List<String> fieldsNames) throws IllegalArgumentException {
501
        final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph();
502
        final Link foreignLink = graph.getForeignLink(this.getTable(), fieldsNames);
503
        if (foreignLink == null)
504
            throw new IllegalArgumentException(fieldsNames + " are not a foreign key of " + this.getTable());
505
        return foreignLink;
506
    }
507
 
508
    /**
25 ilm 509
     * Return the foreign row, if any, for the passed field.
510
     *
511
     * @param fieldName name of the foreign field.
512
     * @return <code>null</code> if the value of <code>fieldName</code> is <code>null</code>,
513
     *         otherwise a SQLRowAccessor with the value of <code>fieldName</code> as its ID.
514
     * @throws IllegalArgumentException if fieldName is not a foreign field.
515
     */
17 ilm 516
    public abstract SQLRowAccessor getForeign(String fieldName);
517
 
80 ilm 518
    /**
142 ilm 519
     * Return the non empty foreign row, if any, for the passed field.
520
     *
521
     * @param fieldName name of the foreign field.
522
     * @return <code>null</code> if the value of <code>fieldName</code> is
523
     *         {@link #isForeignEmpty(String) empty}, otherwise a SQLRowAccessor with the value of
524
     *         <code>fieldName</code> as its ID.
525
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
526
     *         specified.
144 ilm 527
     * @see #getNonEmptyForeignIDNumber(String)
142 ilm 528
     */
529
    public final SQLRowAccessor getNonEmptyForeign(String fieldName) {
530
        if (this.isForeignEmpty(fieldName)) {
531
            return null;
532
        } else {
533
            final SQLRowAccessor res = this.getForeign(fieldName);
534
            assert res != null;
535
            return res;
536
        }
537
    }
538
 
539
    /**
80 ilm 540
     * Return the ID of a foreign row.
541
     *
542
     * @param fieldName name of the foreign field.
543
     * @return the value of <code>fieldName</code>, {@link SQLRow#NONEXISTANT_ID} if
544
     *         <code>null</code>.
545
     * @throws IllegalArgumentException if fieldName is not a foreign field.
546
     */
83 ilm 547
    public final int getForeignID(String fieldName) throws IllegalArgumentException {
548
        final Number res = this.getForeignIDNumber(fieldName);
549
        return res == null ? SQLRow.NONEXISTANT_ID : res.intValue();
550
    }
80 ilm 551
 
83 ilm 552
    /**
142 ilm 553
     * Return the ID of a foreign row. NOTE : there's two cases when the result can be
554
     * <code>null</code> :
555
     * <ol>
556
     * <li><code>field</code> is defined and has the value <code>null</code></li>
151 ilm 557
     * <li><code>field</code> is defined and has an SQLRowValues value {@link #hasID() without an
558
     * ID} (i.e. field not defined or <code>null</code>)</li>
142 ilm 559
     * </ol>
560
     * In the second case, <code>field</code> is *not* {@link #isForeignEmpty(String) empty}, an ID
561
     * is just missing.
83 ilm 562
     *
563
     * @param fieldName name of the foreign field.
564
     * @return the value of <code>fieldName</code> or {@link #getIDNumber()} if the value is a
142 ilm 565
     *         {@link SQLRowValues}, <code>null</code> if the actual value is.
83 ilm 566
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
567
     *         specified.
568
     */
142 ilm 569
    public final Number getForeignIDNumber(String fieldName) throws IllegalArgumentException {
570
        final Value<Number> res = getForeignIDNumberValue(fieldName);
571
        return res.hasValue() ? res.getValue() : null;
572
    }
83 ilm 573
 
574
    /**
144 ilm 575
     * Return the non empty foreign ID, if any, for the passed field.
576
     *
577
     * @param fieldName name of the foreign field.
578
     * @return <code>null</code> if the value of <code>fieldName</code> is
579
     *         {@link #isForeignEmpty(String) empty}, otherwise the foreign ID for the passed field.
580
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
581
     *         specified.
151 ilm 582
     * @throws IllegalStateException if fieldName is not empty but lacks an ID (i.e. has a
583
     *         SQLRowValues without an ID) as by definition a <code>null</code> result means empty.
584
     * @see #getNonEmptyForeign(String)
144 ilm 585
     */
586
    public final Number getNonEmptyForeignIDNumber(String fieldName) {
587
        if (this.isForeignEmpty(fieldName)) {
588
            return null;
589
        } else {
151 ilm 590
            final Value<Number> res = this.getForeignIDNumberValue(fieldName);
591
            if (!res.hasValue())
592
                throw new IllegalStateException("Foreign row has no ID");
593
            assert res.getValue() != null;
594
            return res.getValue();
144 ilm 595
        }
596
    }
597
 
598
    /**
142 ilm 599
     * Return the ID of a foreign row.
600
     *
601
     * @param fieldName name of the foreign field.
602
     * @return {@link Value#getNone()} if there's a {@link SQLRowValues} without
603
     *         {@link SQLRowValues#hasID() ID}, otherwise the value of <code>fieldName</code> or
604
     *         {@link #getIDNumber()} if the value is a {@link SQLRowValues}, never
605
     *         <code>null</code> (the {@link Value#getValue()} is <code>null</code> when
606
     *         <code>fieldName</code> is).
607
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
608
     *         specified.
609
     */
610
    public final Value<Number> getForeignIDNumberValue(final String fieldName) throws IllegalArgumentException {
611
        fetchIfNeeded(fieldName);
612
        // don't use getForeign() to avoid creating a SQLRow
613
        final Object val = this.getContainedObject(fieldName);
614
        if (val instanceof SQLRowValues) {
615
            final SQLRowValues vals = (SQLRowValues) val;
616
            return vals.hasID() ? Value.getSome(vals.getIDNumber()) : Value.<Number> getNone();
617
        } else {
618
            if (!this.getTable().getField(fieldName).isForeignKey())
619
                throw new IllegalArgumentException(fieldName + "is not a foreign key of " + this.getTable());
620
            return Value.getSome((Number) val);
621
        }
622
    }
623
 
624
    private void fetchIfNeeded(String fieldName) {
151 ilm 625
        if (getAccessDBIfNeeded() && (this instanceof SQLRow) && !contains(fieldName)) {
142 ilm 626
            assert false : "Missing " + fieldName + " in " + this;
627
            Log.get().log(Level.WARNING, "Missing " + fieldName + " in " + this, new IllegalStateException());
628
            ((SQLRow) this).fetchValues();
629
        }
630
    }
631
 
632
    /**
83 ilm 633
     * Whether the passed field is empty.
634
     *
635
     * @param fieldName name of the foreign field.
636
     * @return <code>true</code> if {@link #getForeignIDNumber(String)} is the
637
     *         {@link SQLTable#getUndefinedIDNumber()}.
151 ilm 638
     * @throws IllegalArgumentException if fieldName is not a foreign field or if the field isn't
639
     *         specified.
640
     * @throws IllegalStateException if <code>fieldName</code> is <code>null</code> but the foreign
641
     *         table has an undefined ID.
83 ilm 642
     */
142 ilm 643
    public final boolean isForeignEmpty(String fieldName) {
644
        final Value<Number> fID = this.getForeignIDNumberValue(fieldName);
645
        if (!fID.hasValue()) {
646
            // a foreign row values without ID is *not* undefined
647
            return false;
648
        } else {
649
            // keep getForeignTable at the 1st line since it does the check
650
            final SQLTable foreignTable = this.getForeignTable(fieldName);
651
            final Number undefID = foreignTable.getUndefinedIDNumber();
151 ilm 652
            if (undefID != null && fID.getValue() == null) {
653
                // since a foreign row with ID=null !hasID() and thus getForeignIDNumberValue() is
654
                // none and this method returns false above
655
                assert this.getObject(fieldName) == null;
656
                throw new IllegalStateException("Null isn't a valid foreign key value when pointing to a table with undefined ID : " + undefID);
657
            }
142 ilm 658
            return NumberUtils.areNumericallyEqual(fID.getValue(), undefID);
659
        }
660
    }
17 ilm 661
 
662
    public abstract Collection<? extends SQLRowAccessor> getReferentRows();
663
 
664
    public abstract Collection<? extends SQLRowAccessor> getReferentRows(final SQLField refField);
665
 
666
    public abstract Collection<? extends SQLRowAccessor> getReferentRows(final SQLTable refTable);
667
 
67 ilm 668
    public final Collection<? extends SQLRowAccessor> followLink(final Link l) {
669
        return this.followLink(l, Direction.ANY);
670
    }
671
 
17 ilm 672
    /**
67 ilm 673
     * Return the rows linked to this one by <code>l</code>.
674
     *
675
     * @param l the link to follow.
676
     * @param direction which way, one can pass {@link Direction#ANY} to infer it except for self
677
     *        references.
678
     * @return the rows linked to this one.
679
     * @see Step#create(SQLTable, SQLField, Direction)
680
     */
681
    public abstract Collection<? extends SQLRowAccessor> followLink(final Link l, final Direction direction);
682
 
73 ilm 683
    public final BigDecimal getOrder() {
684
        return (BigDecimal) this.getObject(this.getTable().getOrderField().getName());
685
    }
686
 
17 ilm 687
    public final Calendar getCreationDate() {
688
        final SQLField f = getTable().getCreationDateField();
689
        return f == null ? null : this.getDate(f.getName());
690
    }
691
 
692
    public final Calendar getModificationDate() {
693
        final SQLField f = getTable().getModifDateField();
694
        return f == null ? null : this.getDate(f.getName());
695
    }
696
 
697
    // avoid costly asRow()
698
    public final boolean equalsAsRow(SQLRowAccessor o) {
699
        return this.getTable() == o.getTable() && this.getID() == o.getID();
700
    }
701
 
702
    // avoid costly asRow()
703
    public final int hashCodeAsRow() {
704
        return this.getTable().hashCode() + this.getID();
705
    }
142 ilm 706
 
151 ilm 707
    // return the all current field values
708
    public abstract String mapToString();
17 ilm 709
}