OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

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