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
 
16
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
17
import org.openconcerto.sql.model.SQLRowValuesCluster.ValueChangeListener;
93 ilm 18
import org.openconcerto.sql.model.SQLTable.FieldGroup;
17 ilm 19
import org.openconcerto.sql.model.SQLTableEvent.Mode;
67 ilm 20
import org.openconcerto.sql.model.graph.Link;
21
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 22
import org.openconcerto.sql.model.graph.Path;
93 ilm 23
import org.openconcerto.sql.model.graph.SQLKey.Type;
67 ilm 24
import org.openconcerto.sql.model.graph.Step;
80 ilm 25
import org.openconcerto.sql.request.Inserter;
26
import org.openconcerto.sql.request.Inserter.Insertion;
27
import org.openconcerto.sql.request.Inserter.ReturnMode;
17 ilm 28
import org.openconcerto.sql.users.UserManager;
29
import org.openconcerto.sql.utils.ReOrder;
83 ilm 30
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
17 ilm 31
import org.openconcerto.utils.CollectionUtils;
93 ilm 32
import org.openconcerto.utils.CompareUtils;
17 ilm 33
import org.openconcerto.utils.CopyUtils;
34
import org.openconcerto.utils.ExceptionUtils;
81 ilm 35
import org.openconcerto.utils.ListMap;
17 ilm 36
import org.openconcerto.utils.RecursionType;
81 ilm 37
import org.openconcerto.utils.SetMap;
17 ilm 38
import org.openconcerto.utils.Tuple2;
39
import org.openconcerto.utils.cc.IClosure;
40
import org.openconcerto.utils.cc.ITransformer;
73 ilm 41
import org.openconcerto.utils.cc.IdentitySet;
17 ilm 42
import org.openconcerto.utils.cc.LinkedIdentitySet;
43
import org.openconcerto.utils.cc.TransformedMap;
44
import org.openconcerto.utils.convertor.NumberConvertor;
142 ilm 45
import org.openconcerto.utils.convertor.ValueConvertor;
46
import org.openconcerto.utils.convertor.ValueConvertorFactory;
17 ilm 47
 
48
import java.math.BigDecimal;
49
import java.sql.Connection;
50
import java.sql.PreparedStatement;
51
import java.sql.ResultSet;
52
import java.sql.SQLException;
53
import java.sql.Statement;
54
import java.sql.Timestamp;
55
import java.util.ArrayList;
56
import java.util.Arrays;
57
import java.util.Collection;
58
import java.util.Collections;
59
import java.util.EventListener;
60
import java.util.EventObject;
61
import java.util.HashMap;
62
import java.util.HashSet;
63
import java.util.Iterator;
64
import java.util.LinkedHashMap;
65
import java.util.LinkedHashSet;
66
import java.util.List;
67
import java.util.ListIterator;
68
import java.util.Map;
69
import java.util.Map.Entry;
70
import java.util.Set;
71
 
142 ilm 72
import net.jcip.annotations.GuardedBy;
73
 
17 ilm 74
/**
75
 * A class that represent a row of a table that can be modified before being inserted or updated.
76
 * The row might not actually exists in the database, and it might not define all the fields. One
77
 * can put SQLRowValues as a foreign key value, so that it will be inserted as well.
78
 *
79
 * @author Sylvain CUAZ
80
 * @see #load(SQLRowAccessor, Set)
81
 * @see #put(String, Object)
82
 * @see #insert()
83
 * @see #update(int)
84
 */
85
public final class SQLRowValues extends SQLRowAccessor {
86
 
87
    public static enum ForeignCopyMode {
88
        /**
89
         * Copy no SQLRowValues.
90
         */
91
        NO_COPY,
92
        /**
93 ilm 93
         * Put <code>null</code> instead of the SQLRowValues. This keeps all fields.
94
         */
95
        COPY_NULL,
96
        /**
17 ilm 97
         * Copy the id of SQLRowValues if any, otherwise don't copy anything. This keeps the maximum
98
         * of information without any foreign rowValues.
99
         */
100
        COPY_ID_OR_RM,
101
        /**
102
         * Copy the id of SQLRowValues if any, otherwise copy the row. This keeps all the
103
         * information.
104
         */
105
        COPY_ID_OR_ROW,
106
        /**
107
         * Copy every SQLRowValues.
108
         */
109
        COPY_ROW
110
    }
111
 
67 ilm 112
    static public enum CreateMode {
113
        /**
114
         * Never create rows.
115
         */
116
        CREATE_NONE,
117
        /**
93 ilm 118
         * For non-full step, create one row with all links. For example with a step of 3 links :
119
         * <ul>
120
         * <li>if they are all filled, do nothing</li>
121
         * <li>if they are all empty, create one row</li>
122
         * <li>if one link is filled with a row, add all empty links to it</li>
123
         * <li>if more than one link is filled (but not all of them), error out as it would leave a
124
         * hybrid state : neither 2 rows joined by all the links, nor one row per link</li>
125
         * </ul>
126
         * Then follow all existing plus created rows.
67 ilm 127
         */
128
        CREATE_ONE,
129
        /**
93 ilm 130
         * Create one row for each empty link, then follow all existing plus created rows.
67 ilm 131
         */
132
        CREATE_MANY
133
    }
134
 
41 ilm 135
    public static final Object SQL_DEFAULT = new Object() {
136
        @Override
137
        public String toString() {
138
            return SQLRowValues.class.getSimpleName() + ".SQL_DEFAULT";
139
        }
140
    };
17 ilm 141
    /**
142
     * Empty foreign field value.
143
     *
144
     * @see #putEmptyLink(String)
145
     */
41 ilm 146
    public static final Object SQL_EMPTY_LINK = new Object() {
147
        @Override
148
        public String toString() {
149
            return SQLRowValues.class.getSimpleName() + ".SQL_EMPTY_LINK";
150
        }
151
    };
17 ilm 152
 
142 ilm 153
    static public enum ValidityCheck {
154
        /**
155
         * The check is never performed.
156
         */
157
        FORBIDDEN {
158
            @Override
159
            public boolean shouldCheck(final Boolean asked) {
160
                return false;
161
            }
162
        },
163
        /**
164
         * The check is only performed if requested.
165
         */
166
        FALSE_BY_DEFAULT {
167
            @Override
168
            public boolean shouldCheck(Boolean asked) {
169
                return asked == null ? false : asked.booleanValue();
170
            }
171
        },
172
        /**
173
         * The check is performed unless specified.
174
         */
175
        TRUE_BY_DEFAULT {
176
            @Override
177
            public boolean shouldCheck(Boolean asked) {
178
                return asked == null ? true : asked.booleanValue();
179
            }
180
        },
181
        /**
182
         * The check is always performed. This is not generally recommended as some methods of the
183
         * framework will fail.
184
         */
185
        FORCED {
186
            @Override
187
            public boolean shouldCheck(Boolean asked) {
188
                return true;
189
            }
190
        };
17 ilm 191
 
142 ilm 192
        public abstract boolean shouldCheck(final Boolean asked);
17 ilm 193
    }
194
 
142 ilm 195
    @GuardedBy("this")
196
    private static ValidityCheck checkValidity;
197
 
17 ilm 198
    /**
142 ilm 199
     * Set how {@link #getInvalid()} should be called before each data modification. Initially set
144 ilm 200
     * to {@link ValidityCheck#TRUE_BY_DEFAULT}. NOTE : the check is performed outside the
201
     * transaction and thus not always accurate. Only the DB can make sure of the validity
202
     * efficiently : with foreign keys and a trigger to check that unarchived rows reference
203
     * unarchived rows.
17 ilm 204
     *
142 ilm 205
     * @param vc the new mode, <code>null</code> to set the default.
206
     */
207
    public synchronized static void setValidityChecked(final ValidityCheck vc) {
208
        checkValidity = vc == null ? ValidityCheck.TRUE_BY_DEFAULT : vc;
209
    }
210
 
211
    /**
212
     * Whether or not {@link #getInvalid()} should be called.
213
     *
214
     * @param asked what the caller requested.
17 ilm 215
     * @return <code>true</code> if the validity is checked.
216
     */
142 ilm 217
    public synchronized static boolean isValidityChecked(final Boolean asked) {
218
        return checkValidity.shouldCheck(asked);
17 ilm 219
    }
220
 
142 ilm 221
    static {
222
        setValidityChecked(null);
223
    }
224
 
73 ilm 225
    private static final boolean DEFAULT_ALLOW_BACKTRACK = true;
226
 
83 ilm 227
    // i.e. no re-hash for up to 6 entries (8*0.8=6.4)
228
    private static final int DEFAULT_VALUES_CAPACITY = 8;
229
    private static final float DEFAULT_LOAD_FACTOR = 0.8f;
230
 
231
    // Assure there's no copy. Don't just return plannedSize : e.g. for HashMap if it's 15
232
    // the initial capacity will be 16 (the nearest power of 2) and threshold will be 12.8 (with
233
    // our load of 0.8) so there would be a rehash at the 13th items.
234
    private static final int getCapacity(final int plannedSize, final int defaultCapacity) {
235
        return plannedSize < 0 ? defaultCapacity : Math.max((int) (plannedSize / DEFAULT_LOAD_FACTOR) + 1, 4);
236
    }
237
 
93 ilm 238
    private static final LinkedHashMap<String, Object> createLinkedHashMap(final int plannedSize) {
239
        if (plannedSize < 0)
240
            throw new IllegalArgumentException("Negative capacity");
241
        return createLinkedHashMap(plannedSize, -1);
242
    }
243
 
244
    private static final <K, V> LinkedHashMap<K, V> createLinkedHashMap(final int plannedSize, final int defaultCapacity) {
245
        return new LinkedHashMap<K, V>(getCapacity(plannedSize, defaultCapacity), DEFAULT_LOAD_FACTOR);
246
    }
247
 
248
    private static final <K> SetMap<K, SQLRowValues> createSetMap(final int plannedSize, final int defaultCapacity) {
249
        return new SetMap<K, SQLRowValues>(new HashMap<K, Set<SQLRowValues>>(getCapacity(plannedSize, defaultCapacity), DEFAULT_LOAD_FACTOR), org.openconcerto.utils.CollectionMap2.Mode.NULL_FORBIDDEN, false) {
250
            @Override
251
            public Set<SQLRowValues> createCollection(Collection<? extends SQLRowValues> coll) {
252
                // use LinkedHashSet so that the order is preserved, eg we can iterate over LOCALs
253
                // pointing to a BATIMENT with consistent and predictable (insertion-based) order.
254
                // use IdentitySet to be able to put two equal instances
255
                return coll == null ? new LinkedIdentitySet<SQLRowValues>() : new LinkedIdentitySet<SQLRowValues>(coll);
256
            }
257
        };
258
    }
259
 
17 ilm 260
    private final Map<String, Object> values;
261
    private final Map<String, SQLRowValues> foreigns;
81 ilm 262
    private final SetMap<SQLField, SQLRowValues> referents;
17 ilm 263
    private SQLRowValuesCluster graph;
81 ilm 264
    private ListMap<SQLField, ReferentChangeListener> referentsListener;
17 ilm 265
 
266
    public SQLRowValues(SQLTable t) {
83 ilm 267
        this(t, -1, -1, -1);
268
    }
269
 
270
    /**
271
     * Create a new instance.
272
     *
273
     * @param t the table.
274
     * @param valuesPlannedSize no further allocations will be made until that number of
275
     *        {@link #getAbsolutelyAll() values}, pass a negative value to use a default.
276
     * @param foreignsPlannedSize no further allocations will be made until that number of
277
     *        {@link #getForeigns() foreigns}, pass a negative value to use a default.
278
     * @param referentsPlannedSize no further allocations will be made until that number of
279
     *        {@link #getReferentsMap() referents}, pass a negative value to use a default.
280
     *
281
     */
282
    public SQLRowValues(SQLTable t, final int valuesPlannedSize, final int foreignsPlannedSize, final int referentsPlannedSize) {
17 ilm 283
        super(t);
284
        // use LinkedHashSet so that the order is preserved, see #walkFields()
93 ilm 285
        this.values = createLinkedHashMap(valuesPlannedSize, DEFAULT_VALUES_CAPACITY);
83 ilm 286
        // foreigns order should be coherent with values
93 ilm 287
        this.foreigns = createLinkedHashMap(foreignsPlannedSize, 4);
288
        this.referents = createSetMap(referentsPlannedSize, 4);
17 ilm 289
        // no used much so lazy init
290
        this.referentsListener = null;
83 ilm 291
        // Allow to reduce memory for lonely rows, and even for linked rows since before :
292
        // 1. create a row, create a cluster
293
        // 2. create a second row, create a second cluster
294
        // 3. put, the second row uses the first cluster, the second one can be collected
295
        // Now the second cluster is never created, see SQLRowValuesCluster.add().
296
        this.graph = null;
17 ilm 297
    }
298
 
299
    public SQLRowValues(SQLTable t, Map<String, ?> values) {
83 ilm 300
        this(t, values.size(), -1, -1);
17 ilm 301
        this.setAll(values);
302
    }
303
 
304
    public SQLRowValues(SQLRowValues vals) {
305
        this(vals, ForeignCopyMode.COPY_ROW);
306
    }
307
 
308
    /**
309
     * Create a new instance with the same values. If <code>copyForeigns</code> is <code>true</code>
310
     * the new instance will have exactly the same values, ie it will point to the same
311
     * SQLRowValues. If <code>copyForeigns</code> is <code>false</code> all SQLRowValues will be
312
     * left out.
313
     *
314
     * @param vals the instance to copy.
315
     * @param copyForeigns whether to copy foreign SQLRowValues.
316
     */
317
    public SQLRowValues(SQLRowValues vals, ForeignCopyMode copyForeigns) {
318
        // setAll() takes care of foreigns and referents
83 ilm 319
        this(vals.getTable(), vals.getAllValues(copyForeigns));
17 ilm 320
    }
321
 
322
    /**
323
     * Copy this rowValues and all others connected to it. Ie contrary to
324
     * {@link #SQLRowValues(SQLRowValues)} the result will not point to the same rowValues, but to
325
     * copy of them.
326
     *
327
     * @return a copy of this.
328
     */
329
    public final SQLRowValues deepCopy() {
132 ilm 330
        return this.getGraph().deepCopy(this, false);
17 ilm 331
    }
332
 
151 ilm 333
    public final SQLRowValues copy(final SQLRowValues graph) {
334
        return this.getGraph().copy(this, graph).get(this);
335
    }
336
 
93 ilm 337
    /**
338
     * Get a frozen version of this. If not already {@link #isFrozen() frozen}, copy this rowValues
339
     * and all others connected to it and {@link SQLRowValuesCluster#freeze()} the copy. I.e. if the
340
     * result is to be shared among threads, it still needs to be safely published.
341
     *
342
     * @return this if already frozen, otherwise a frozen copy of this.
343
     */
344
    public final SQLRowValues toImmutable() {
345
        if (this.isFrozen())
346
            return this;
347
        return this.getGraph().deepCopy(this, true);
348
    }
349
 
17 ilm 350
    // *** graph
351
 
83 ilm 352
    private void updateLinks(String fieldName, Object old, Object value) {
17 ilm 353
        // try to avoid getTable().getField() (which takes 1/3 of put() for nothing when there is no
354
        // rowvalues)
355
        final boolean oldRowVals = old instanceof SQLRowValues;
356
        final boolean newRowVals = value instanceof SQLRowValues;
357
        if (!oldRowVals && !newRowVals)
358
            return;
359
 
360
        final SQLField f = this.getTable().getField(fieldName);
361
 
362
        if (oldRowVals) {
363
            final SQLRowValues vals = (SQLRowValues) old;
144 ilm 364
            vals.referents.removeOne(f, this);
17 ilm 365
            this.foreigns.remove(fieldName);
366
            assert this.graph == vals.graph;
367
            this.graph.remove(this, f, vals);
368
            vals.fireRefChange(f, false, this);
369
        }
370
        if (newRowVals) {
371
            final SQLRowValues vals = (SQLRowValues) value;
81 ilm 372
            vals.referents.add(f, this);
17 ilm 373
            this.foreigns.put(fieldName, vals);
83 ilm 374
            // prefer vals' graph as add() is faster that way
375
            final SQLRowValuesCluster usedGraph = this.graph != null && vals.graph == null ? this.graph : vals.getGraph();
376
            usedGraph.add(this, f, vals);
17 ilm 377
            assert this.graph == vals.graph;
378
            vals.fireRefChange(f, true, this);
379
        }
380
    }
381
 
93 ilm 382
    /**
383
     * Return the graph for this instance. NOTE: for single row values the graph is only created on
384
     * demand.
385
     *
386
     * @return the graph.
387
     */
83 ilm 388
    public final SQLRowValuesCluster getGraph() {
389
        return this.getGraph(true);
390
    }
391
 
392
    final SQLRowValuesCluster getGraph(final boolean create) {
393
        if (create && this.graph == null)
394
            this.graph = new SQLRowValuesCluster(this);
17 ilm 395
        return this.graph;
396
    }
397
 
93 ilm 398
    /**
399
     * The number of items in our graph. NOTE: this method doesn't allocate a graph.
400
     *
401
     * @return the number of items in our graph.
402
     * @see SQLRowValuesCluster#size()
403
     */
404
    public final int getGraphSize() {
405
        final SQLRowValuesCluster g = this.getGraph(false);
406
        return g == null ? 1 : g.size();
407
    }
408
 
144 ilm 409
    public final Set<SQLTable> getGraphTables() {
410
        final SQLRowValuesCluster g = this.getGraph(false);
411
        if (g == null)
412
            return Collections.singleton(this.getTable());
413
        else
414
            return g.getTables();
415
    }
416
 
17 ilm 417
    public final <T> void walkGraph(T acc, ITransformer<State<T>, T> closure) {
418
        this.getGraph().walk(this, acc, closure);
419
    }
420
 
421
    /**
422
     * Walk through the fields of the rowValues in order. Eg if you added DESIGNATION, ID_BATIMENT
423
     * pointing to {DESIGNATION}, then INCLURE, <code>closure</code> would be called with
424
     * LOCAL.DESIGNATION, LOCAL.ID_BATIMENT.DESIGNATION, LOCAL.INCLURE. This can't be done using
425
     * {@link SQLRowValuesCluster#walk(SQLRowValues, Object, ITransformer, RecursionType)} since it
426
     * walks through rowValues so if you use {@link RecursionType#BREADTH_FIRST} you'll be passed
427
     * LOCAL, then BATIMENT and the reverse if you use {@link RecursionType#DEPTH_FIRST}.
428
     *
429
     * @param closure what to do on each field.
430
     */
431
    public final void walkFields(IClosure<FieldPath> closure) {
432
        this.walkFields(closure, false);
433
    }
434
 
435
    public final void walkFields(IClosure<FieldPath> closure, final boolean includeFK) {
436
        this.getGraph().walkFields(this, closure, includeFK);
437
    }
438
 
439
    public final SQLRowValues prune(SQLRowValues graph) {
151 ilm 440
        return this.prune(graph, true);
17 ilm 441
    }
442
 
443
    /**
151 ilm 444
     * Prune this graph.
445
     *
446
     * @param graph the rows and fields to keep.
447
     * @param copy <code>true</code> if a pruned copy should be returned, <code>false</code> to
448
     *        modify this instance in-place.
449
     * @return a graph no bigger than the passed parameter.
450
     * @see SQLRowValuesCluster#prune(SQLRowValues, SQLRowValues, boolean)
451
     * @see SQLRowValuesCluster#pruneWithoutCopy(SQLRowValues, SQLRowValues, boolean)
452
     */
453
    public final SQLRowValues prune(SQLRowValues graph, final boolean copy) {
454
        return copy ? this.getGraph().prune(this, graph) : this.getGraph().pruneWithoutCopy(this, graph);
455
    }
456
 
457
    /**
17 ilm 458
     * Fetch if necessary and store in this the foreign row.
459
     *
460
     * @param fk a foreign key, eg "ID_FAMILLE_2".
461
     * @return the foreign row, eg FAMILLE[1].
462
     */
463
    public final SQLRowValues grow(String fk) {
25 ilm 464
        final Object val = this.getContainedObject(fk);
465
        // if fk is in our map with a null value, nothing to grow
466
        if (val != null && !(val instanceof SQLRowValues)) {
17 ilm 467
            final SQLRowValues vals = new SQLRowValues(this.getTable());
468
            vals.putRowValues(fk).setAllToNull();
469
            this.grow(vals, true);
470
        }
471
        return (SQLRowValues) this.getForeign(fk);
472
    }
473
 
474
    public final SQLRowValues grow(SQLRowValues graph) {
475
        return this.grow(graph, true);
476
    }
477
 
478
    /**
479
     * Grow this rowValues to match the passed graph. If this was /RECEPTEUR/ : {DESIGNATION="des";
480
     * ID_LOCAL=2} and <code>graph</code> is /RECEPTEUR/ : {DESIGNATION=null; ID_LOCAL:
481
     * /LOCAL/:{DESIGNATION=null}}, then now this is /RECEPTEUR/ : {DESIGNATION="des"; ID_LOCAL:
482
     * /LOCAL/:{ID=2, DESIGNATION="local"}}
483
     *
484
     * @param graph the target graph.
485
     * @param checkFields <code>true</code> if missing fields should be fetched.
486
     * @return this.
487
     * @throws IllegalArgumentException if this couldn't be grown.
488
     */
489
    public final SQLRowValues grow(SQLRowValues graph, final boolean checkFields) {
151 ilm 490
        return this.grow(graph, checkFields, false);
491
    }
492
 
493
    public final SQLRowValues grow(SQLRowValues graph, final boolean checkFields, final boolean growUndefined) {
494
        graph.getGraph().grow(graph, this, checkFields, growUndefined);
17 ilm 495
        return this;
496
    }
497
 
498
    public final boolean graphContains(SQLRowValues graph) {
499
        return this.getGraph().contains(this, graph) == null;
500
    }
501
 
502
    void setGraph(SQLRowValuesCluster g) {
503
        assert g != null;
504
        this.graph = g;
505
    }
506
 
83 ilm 507
    public final boolean hasForeigns() {
508
        // OK since updateLinks() removes empty map entries
509
        return !this.foreigns.isEmpty();
510
    }
511
 
512
    public final Map<String, SQLRowValues> getForeigns() {
17 ilm 513
        return Collections.unmodifiableMap(this.foreigns);
514
    }
515
 
93 ilm 516
    final int getForeignsSize() {
517
        return this.foreigns.size();
518
    }
519
 
17 ilm 520
    final Map<SQLField, SQLRowValues> getForeignsBySQLField() {
521
        return new TransformedMap<String, SQLField, SQLRowValues>(this.getForeigns(), new ITransformer<String, SQLField>() {
522
            @Override
523
            public SQLField transformChecked(String input) {
524
                return getTable().getField(input);
525
            }
526
        }, new ITransformer<SQLField, String>() {
527
            @Override
528
            public String transformChecked(SQLField input) {
529
                return input.getName();
530
            }
531
        });
532
    }
533
 
93 ilm 534
    // package private since the result is modifiable, see below for the public version
81 ilm 535
    final SetMap<SQLField, SQLRowValues> getReferents() {
17 ilm 536
        return this.referents;
537
    }
538
 
83 ilm 539
    public final SetMapItf<SQLField, SQLRowValues> getReferentsMap() {
540
        return SetMap.unmodifiableMap(this.referents);
541
    }
542
 
543
    public final boolean hasReferents() {
544
        // OK since updateLinks() removes empty map entries
545
        return !this.referents.isEmpty();
546
    }
547
 
17 ilm 548
    @Override
549
    public Collection<SQLRowValues> getReferentRows() {
550
        // remove the backdoor since values() returns a view
551
        // remove duplicates (e.g. this is a CONTACT referenced by ID_CONTACT_RAPPORT &
552
        // ID_CONTACT_RDV from the same site)
81 ilm 553
        return this.referents.createCollection(this.referents.allValues());
17 ilm 554
    }
555
 
556
    @Override
557
    public Set<SQLRowValues> getReferentRows(SQLField refField) {
93 ilm 558
        return Collections.unmodifiableSet(this.referents.getNonNull(refField));
17 ilm 559
    }
560
 
561
    @Override
562
    public Collection<SQLRowValues> getReferentRows(SQLTable refTable) {
563
        // remove duplicates
564
        final Collection<SQLRowValues> res = this.referents.createCollection(null);
565
        assert res.isEmpty();
81 ilm 566
        for (final Map.Entry<SQLField, Set<SQLRowValues>> e : this.referents.entrySet()) {
17 ilm 567
            if (e.getKey().getTable().equals(refTable))
568
                res.addAll(e.getValue());
569
        }
570
        return res;
571
    }
572
 
573
    /**
83 ilm 574
     * Remove all links pointing to this from the referent rows.
17 ilm 575
     *
576
     * @return this.
577
     */
578
    public final SQLRowValues clearReferents() {
132 ilm 579
        return this.changeReferents(ForeignCopyMode.NO_COPY);
61 ilm 580
    }
581
 
132 ilm 582
    public final SQLRowValues changeReferents(final ForeignCopyMode mode) {
583
        return this.changeReferents(null, false, mode);
584
    }
585
 
61 ilm 586
    public final SQLRowValues removeReferents(final SQLField f) {
587
        // don't use changeReferents() as it's less optimal
588
        for (final SQLRowValues ref : new ArrayList<SQLRowValues>(this.getReferentRows(f))) {
589
            ref.remove(f.getName());
590
        }
591
        return this;
592
    }
593
 
132 ilm 594
    public final SQLRowValues removeReferentFields(final Collection<SQLField> fields) {
595
        return this.changeReferents(fields, false);
61 ilm 596
    }
597
 
132 ilm 598
    public final SQLRowValues retainReferentFields(final Collection<SQLField> fields) {
599
        return this.changeReferents(fields, true);
93 ilm 600
    }
601
 
132 ilm 602
    private final SQLRowValues changeReferents(final Collection<SQLField> fields, final boolean retain) {
603
        return this.changeReferents(fields, retain, ForeignCopyMode.NO_COPY);
604
    }
605
 
606
    /**
607
     * Change referents. NOTE : depending on the {@link ForeignCopyMode mode} this method may detach
608
     * this row from some of its referents.
609
     *
610
     * @param fields the fields to change or to exclude from change.
611
     * @param exclude <code>true</code> if fields passed to this method must be excluded from the
612
     *        change, <code>false</code> to only change fields passed to this method.
613
     * @param mode how the referent row will be changed.
614
     * @return this.
615
     */
616
    public final SQLRowValues changeReferents(final Collection<SQLField> fields, final boolean exclude, final ForeignCopyMode mode) {
617
        if (!isEmpty(fields, exclude) && mode != ForeignCopyMode.COPY_ROW) {
61 ilm 618
            // copy otherwise ConcurrentModificationException
81 ilm 619
            for (final Entry<SQLField, Set<SQLRowValues>> e : CopyUtils.copy(this.getReferents()).entrySet()) {
132 ilm 620
                // fields == null means !retain thanks to the above if
621
                if (fields == null || fields.contains(e.getKey()) != exclude) {
61 ilm 622
                    for (final SQLRowValues ref : e.getValue()) {
93 ilm 623
                        ref.flatten(e.getKey().getName(), mode);
61 ilm 624
                    }
625
                }
17 ilm 626
            }
627
        }
628
        return this;
629
    }
630
 
65 ilm 631
    public SQLRowValues retainReferent(SQLRowValues toRetain) {
632
        return this.retainReferents(Collections.singleton(toRetain));
633
    }
634
 
635
    public SQLRowValues retainReferents(Collection<SQLRowValues> toRetain) {
67 ilm 636
        toRetain = CollectionUtils.toIdentitySet(toRetain);
65 ilm 637
        // copy otherwise ConcurrentModificationException
81 ilm 638
        for (final Entry<SQLField, Set<SQLRowValues>> e : CopyUtils.copy(this.getReferents()).entrySet()) {
65 ilm 639
            for (final SQLRowValues ref : e.getValue()) {
640
                if (!toRetain.contains(ref))
83 ilm 641
                    ref.remove(e.getKey().getName());
65 ilm 642
            }
643
        }
644
        return this;
645
    }
646
 
17 ilm 647
    // *** get
648
 
649
    public int size() {
650
        return this.values.size();
651
    }
652
 
653
    @Override
654
    public final int getID() {
83 ilm 655
        final Number res = this.getIDNumber(false);
17 ilm 656
        if (res != null)
657
            return res.intValue();
658
        else
659
            return SQLRow.NONEXISTANT_ID;
660
    }
661
 
662
    @Override
83 ilm 663
    public Number getIDNumber() {
664
        // We never have rows in the DB with NULL primary key, so a null result means no value was
665
        // specified (or null was programmatically specified)
666
        return this.getIDNumber(false);
667
    }
668
 
669
    public final Number getIDNumber(final boolean mustBePresent) {
670
        final Object res = this.getObject(this.getTable().getKey().getName(), mustBePresent);
671
        if (res == null) {
672
            return null;
673
        } else {
17 ilm 674
            return (Number) res;
83 ilm 675
        }
17 ilm 676
    }
677
 
678
    @Override
679
    public final Object getObject(String fieldName) {
680
        return this.values.get(fieldName);
681
    }
682
 
683
    @Override
684
    public Map<String, Object> getAbsolutelyAll() {
25 ilm 685
        return getAllValues(ForeignCopyMode.COPY_ROW);
17 ilm 686
    }
687
 
25 ilm 688
    protected final Map<String, Object> getAllValues(ForeignCopyMode copyForeigns) {
132 ilm 689
        return this.getAllValues(copyForeigns, false);
690
    }
691
 
692
    private final Map<String, Object> getAllValues(ForeignCopyMode copyForeigns, final boolean copy) {
25 ilm 693
        final Map<String, Object> toAdd;
694
        if (copyForeigns == ForeignCopyMode.COPY_ROW || this.foreigns.size() == 0) {
132 ilm 695
            if (copy) {
696
                toAdd = createLinkedHashMap(this.size());
697
                toAdd.putAll(this.values);
698
            } else {
699
                toAdd = this.values;
700
            }
25 ilm 701
        } else {
702
            final Set<Entry<String, Object>> entrySet = this.values.entrySet();
93 ilm 703
            toAdd = createLinkedHashMap(entrySet.size());
25 ilm 704
            for (final Map.Entry<String, Object> e : entrySet) {
705
                if (!(e.getValue() instanceof SQLRowValues)) {
706
                    toAdd.put(e.getKey(), e.getValue());
93 ilm 707
                } else if (copyForeigns == ForeignCopyMode.COPY_NULL) {
708
                    toAdd.put(e.getKey(), null);
25 ilm 709
                } else if (copyForeigns != ForeignCopyMode.NO_COPY) {
710
                    final SQLRowValues foreign = (SQLRowValues) e.getValue();
711
                    if (foreign.hasID())
712
                        toAdd.put(e.getKey(), foreign.getIDNumber());
713
                    else if (copyForeigns == ForeignCopyMode.COPY_ID_OR_ROW)
714
                        toAdd.put(e.getKey(), foreign);
715
                }
716
            }
717
        }
132 ilm 718
        return copy ? toAdd : Collections.unmodifiableMap(toAdd);
25 ilm 719
    }
720
 
721
    /**
93 ilm 722
     * All current groups of this row.
723
     *
724
     * @return the ordered groups.
725
     * @throws IllegalStateException if a group is incomplete (e.g. a primary key has only one of
726
     *         its two values).
727
     */
728
    public final Set<FieldGroup> getFieldGroups() throws IllegalStateException {
729
        final Set<String> fields = this.getFields();
730
        // keep order
731
        final LinkedHashSet<FieldGroup> set = new LinkedHashSet<FieldGroup>();
732
        final Map<String, FieldGroup> tableGroups = this.getTable().getFieldGroups();
733
        for (final String fieldName : fields) {
734
            final FieldGroup group = tableGroups.get(fieldName);
735
            // check that groups are complete
736
            if (set.add(group)) {
737
                if (!fields.containsAll(group.getFields()))
738
                    throw new IllegalStateException("Missing fields for " + group + ", current fields : " + fields);
739
            }
740
        }
741
        return set;
742
    }
743
 
744
    /**
25 ilm 745
     * Return the foreign row, if any, for the passed field.
746
     *
747
     * @param fieldName name of the foreign field.
748
     * @return if <code>null</code> or a SQLRowValues one was put at <code>fieldName</code>, return
749
     *         it ; else assume that an ID was put at <code>fieldName</code> and return a new SQLRow
750
     *         with it.
751
     * @throws IllegalArgumentException if fieldName is not a foreign field or if it isn't contained
752
     *         in this instance.
753
     * @throws ClassCastException if the value is neither a SQLRowValues, nor <code>null</code> nor
754
     *         a Number.
755
     */
17 ilm 756
    @Override
25 ilm 757
    public final SQLRowAccessor getForeign(String fieldName) throws IllegalArgumentException, ClassCastException {
132 ilm 758
        return this.getForeign(this.getForeignLink(Collections.singletonList(fieldName)));
759
    }
760
 
761
    public final SQLRowAccessor getForeign(final Link l) throws IllegalArgumentException {
762
        if (!l.getSource().equals(this.getTable()))
763
            throw new IllegalArgumentException(l + " not from " + this);
764
        final String fieldName = l.getSingleField().getName();
25 ilm 765
        final Object val = this.getContainedObject(fieldName);
766
        if (val instanceof SQLRowAccessor) {
767
            return (SQLRowAccessor) val;
768
        } else if (val == null) {
769
            // since we used getContainedObject(), it means that a null was put in our map, not that
770
            // fieldName wasn't there
17 ilm 771
            return null;
772
        } else if (this.isDefault(fieldName)) {
773
            throw new IllegalStateException(fieldName + " is DEFAULT");
774
        } else {
132 ilm 775
            return new SQLRow(l.getTarget(), this.getInt(fieldName));
17 ilm 776
        }
777
    }
778
 
779
    public boolean isDefault(String fieldName) {
780
        return SQL_DEFAULT.equals(this.getObject(fieldName));
781
    }
782
 
783
    /**
784
     * Retourne les champs spécifiés par cette instance.
785
     *
786
     * @return l'ensemble des noms des champs.
787
     */
788
    @Override
789
    public Set<String> getFields() {
790
        return Collections.unmodifiableSet(this.values.keySet());
791
    }
792
 
151 ilm 793
    // avoid Collections.unmodifiableSet() allocation
17 ilm 794
    @Override
151 ilm 795
    public boolean contains(String fieldName) {
796
        return this.values.containsKey(fieldName);
797
    }
798
 
799
    @Override
17 ilm 800
    public final SQLRow asRow() {
801
        if (!this.hasID())
802
            throw new IllegalStateException(this + " has no ID");
25 ilm 803
        return new SQLRow(this.getTable(), this.getAllValues(ForeignCopyMode.COPY_ID_OR_RM));
17 ilm 804
    }
805
 
806
    @Override
807
    public final SQLRowValues asRowValues() {
808
        return this;
809
    }
810
 
811
    // *** set
812
 
813
    /**
93 ilm 814
     * Whether this can be modified.
815
     *
816
     * @return <code>true</code> if this (and its graph) is not modifiable.
817
     */
818
    public final boolean isFrozen() {
819
        final SQLRowValuesCluster g = this.getGraph(false);
820
        return g != null && g.isFrozen();
821
    }
822
 
823
    private void checkFrozen() {
824
        if (this.isFrozen())
825
            throw new IllegalStateException("Graph is not modifiable");
826
    }
827
 
828
    /**
17 ilm 829
     * Retains only the fields in this that are contained in the specified collection. In other
830
     * words, removes all of its elements that are not contained in the specified collection.
831
     *
832
     * @param fields collection containing elements to be retained, <code>null</code> meaning all.
833
     * @return this.
834
     */
835
    public final SQLRowValues retainAll(Collection<String> fields) {
836
        return this.changeFields(fields, true);
837
    }
838
 
839
    private final SQLRowValues changeFields(Collection<String> fields, final boolean retain) {
132 ilm 840
        return this.changeFields(fields, retain, false);
841
    }
842
 
843
    public final SQLRowValues changeFields(Collection<String> fields, final boolean retain, final boolean protectGraph) {
844
        if (isEmpty(fields, retain))
17 ilm 845
            return this;
41 ilm 846
        // clear all on an empty values == no-op
847
        if (!retain && fields == null && this.size() == 0)
848
            return this;
17 ilm 849
 
850
        final Set<String> toRm = new HashSet<String>(this.values.keySet());
132 ilm 851
        if (protectGraph)
852
            toRm.removeAll(this.foreigns.keySet());
17 ilm 853
        // fields == null => !retain => clear()
854
        if (fields != null) {
855
            if (retain) {
856
                toRm.removeAll(fields);
41 ilm 857
            } else {
17 ilm 858
                toRm.retainAll(fields);
41 ilm 859
            }
17 ilm 860
        }
41 ilm 861
        // nothing to change
862
        if (toRm.isEmpty())
863
            return this;
17 ilm 864
        // handle links
93 ilm 865
        final Map<String, FieldGroup> fieldGroups = getTable().getFieldGroups();
17 ilm 866
        for (final String fieldName : toRm) {
93 ilm 867
            if (fieldGroups.get(fieldName).getKeyType() == Type.FOREIGN_KEY)
142 ilm 868
                // name is OK since it is a foreign key
869
                // value null is also OK
870
                this._put(fieldName, null, false, ValueOperation.CHECK);
17 ilm 871
        }
132 ilm 872
        if (fields == null && !protectGraph) {
17 ilm 873
            assert !retain && toRm.equals(this.values.keySet());
874
            this.values.clear();
875
        } else {
876
            this.values.keySet().removeAll(toRm);
877
        }
83 ilm 878
        // if there's no graph, there can't be any listeners
879
        final SQLRowValuesCluster graph = this.getGraph(false);
880
        if (graph != null)
881
            graph.fireModification(this, toRm);
17 ilm 882
        return this;
883
    }
884
 
885
    /**
886
     * Removes from this all fields that are contained in the specified collection.
887
     *
888
     * @param fields collection containing elements to be removed, <code>null</code> meaning all.
889
     * @return this.
890
     */
891
    public final SQLRowValues removeAll(Collection<String> fields) {
892
        return this.changeFields(fields, false);
893
    }
894
 
895
    public final void remove(String field) {
896
        // check arg & handle links
897
        this.put(field, null);
898
        // really remove
93 ilm 899
        assert !this.isFrozen() : "Should already be checked by put(null)";
17 ilm 900
        this.values.remove(field);
901
    }
902
 
903
    public final void clear() {
904
        this.removeAll(null);
905
    }
906
 
907
    public final void clearPrimaryKeys() {
93 ilm 908
        checkFrozen();
17 ilm 909
        this.clearPrimaryKeys(this.values);
910
        // by definition primary keys are not foreign keys, so no need to updateLinks()
911
    }
912
 
913
    private Map<String, Object> clearPrimaryKeys(final Map<String, Object> values) {
914
        return clearFields(values, this.getTable().getPrimaryKeys());
915
    }
916
 
917
    private Map<String, Object> clearFields(final Map<String, Object> values, final Set<SQLField> fields) {
918
        return changeFields(values, fields, false);
919
    }
920
 
921
    private Map<String, Object> changeFields(final Map<String, Object> values, final Set<SQLField> fields, final boolean retain) {
922
        final Iterator<String> iter = values.keySet().iterator();
923
        while (iter.hasNext()) {
924
            final String fieldName = iter.next();
925
            if (fields.contains(this.getTable().getField(fieldName)) ^ retain)
926
                iter.remove();
927
        }
928
        return values;
929
    }
930
 
132 ilm 931
    /**
932
     * Change foreign and referent rows. NOTE : this doesn't change all foreign keys, only those
933
     * that contain an {@link SQLRowValues}.
934
     *
935
     * @param paths the first steps are to be changed or to be excluded from change,
936
     *        <code>null</code> meaning all.
937
     * @param exclude <code>true</code> if steps passed to this method must be excluded from the
938
     *        change, <code>false</code> to only change steps passed to this method.
939
     * @param mode how the rows will be changed.
940
     * @return this.
941
     */
942
    public final SQLRowValues changeGraph(final Collection<Path> paths, final boolean exclude, ForeignCopyMode mode) {
943
        if (this.getGraphSize() == 1)
944
            return this;
945
 
946
        final Set<SQLField> refFields;
947
        final Set<String> foreignFields;
948
        if (paths == null) {
949
            refFields = null;
950
            foreignFields = null;
951
        } else {
952
            refFields = new HashSet<SQLField>();
953
            foreignFields = new HashSet<String>();
954
            for (final Path p : paths) {
955
                if (p.getFirst() != this.getTable())
956
                    throw new IllegalArgumentException("Path not from this : " + p);
957
                if (p.length() > 0) {
958
                    final Step step = p.getStep(0);
959
                    for (final Link l : step.getLinks()) {
960
                        if (step.getDirection(l) == Direction.REFERENT)
961
                            refFields.addAll(l.getFields());
962
                        else
963
                            foreignFields.addAll(l.getCols());
964
                    }
965
                }
966
            }
967
        }
968
        changeForeigns(foreignFields, exclude, mode);
969
        changeReferents(refFields, exclude, mode);
970
        return this;
971
    }
972
 
973
    public final void detach() {
974
        // keep the most information
975
        this.detach(ForeignCopyMode.COPY_ID_OR_RM);
976
    }
977
 
978
    public final void detach(final ForeignCopyMode mode) {
979
        if (mode.compareTo(ForeignCopyMode.COPY_ID_OR_ROW) >= 0)
980
            throw new IllegalArgumentException("Might keep row and not detach : " + mode);
981
        this.changeGraph(null, false, mode);
982
        assert this.getGraphSize() == 1;
983
    }
984
 
17 ilm 985
    // puts
986
 
987
    public SQLRowValues put(String fieldName, Object value) {
988
        return this.put(fieldName, value, true);
989
    }
990
 
142 ilm 991
    SQLRowValues put(String fieldName, Object value, final boolean check) {
992
        return this.put(fieldName, value, check, check ? ValueOperation.CONVERT : ValueOperation.PASS);
993
    }
994
 
995
    SQLRowValues put(String fieldName, Object value, final boolean checkName, final ValueOperation checkValue) {
996
        _put(fieldName, value, checkName, checkValue);
83 ilm 997
        // if there's no graph, there can't be any listeners
998
        final SQLRowValuesCluster graph = this.getGraph(false);
999
        if (graph != null)
1000
            graph.fireModification(this, fieldName, value);
17 ilm 1001
        return this;
1002
    }
1003
 
142 ilm 1004
    static public enum ValueOperation {
1005
        CONVERT, CHECK, PASS
17 ilm 1006
    }
1007
 
142 ilm 1008
    // TODO support java.time.LocalDateTime in Java 8
1009
    static private <T, U> U convert(final Class<T> source, final Object value, final Class<U> dest) {
1010
        final ValueConvertor<T, U> conv = ValueConvertorFactory.find(source, dest);
1011
        if (conv == null)
1012
            throw new IllegalArgumentException("No convertor to " + dest + " from " + source);
1013
        assert source == value.getClass();
1014
        @SuppressWarnings("unchecked")
1015
        final T tVal = (T) value;
1016
        return conv.convert(tVal);
17 ilm 1017
    }
1018
 
142 ilm 1019
    private void _put(String fieldName, Object value, final boolean checkName, final ValueOperation checkValue) {
1020
        // table.contains() can take up to 35% of this method
1021
        if (checkName && !this.getTable().contains(fieldName))
1022
            throw new IllegalArgumentException(fieldName + " is not in table " + this.getTable());
1023
        if (value == SQL_EMPTY_LINK) {
1024
            // keep getForeignTable since it does the check
1025
            value = this.getForeignTable(fieldName).getUndefinedIDNumber();
1026
        } else if (value != null && value != SQL_DEFAULT && checkValue != ValueOperation.PASS) {
1027
            final SQLField field = this.getTable().getField(fieldName);
1028
            if (value instanceof SQLRowValues) {
1029
                if (!field.isForeignKey())
1030
                    throw new IllegalArgumentException("Since value is a SQLRowValues, expected a foreign key but got " + field);
1031
            } else {
1032
                final Class<?> javaType = field.getType().getJavaType();
1033
                if (!javaType.isInstance(value)) {
1034
                    if (checkValue == ValueOperation.CONVERT) {
1035
                        value = convert(value.getClass(), value, javaType);
1036
                    } else {
1037
                        throw new IllegalArgumentException("Wrong type for " + fieldName + ", expected " + javaType + " but got " + value.getClass());
1038
                    }
1039
                }
1040
            }
17 ilm 1041
        }
142 ilm 1042
        checkFrozen();
1043
        this.updateLinks(fieldName, this.values.put(fieldName, value), value);
17 ilm 1044
    }
1045
 
1046
    public SQLRowValues put(String fieldName, int value) {
1047
        return this.put(fieldName, Integer.valueOf(value));
1048
    }
1049
 
1050
    public SQLRowValues putDefault(String fieldName) {
1051
        return this.put(fieldName, SQL_DEFAULT);
1052
    }
1053
 
1054
    /**
1055
     * To empty a foreign key.
1056
     *
1057
     * @param fieldName the name of the foreign key to empty.
1058
     * @return this.
1059
     */
1060
    public SQLRowValues putEmptyLink(String fieldName) {
1061
        return this.put(fieldName, SQL_EMPTY_LINK);
1062
    }
1063
 
1064
    /**
1065
     * Set a new {@link SQLRowValues} as the value of <code>fieldName</code>. ATTN contrary to many
1066
     * methods this one do not return <code>this</code>.
1067
     *
1068
     * @param fieldName the name of a foreign field.
1069
     * @return the newly created values.
1070
     * @throws IllegalArgumentException if <code>fieldName</code> is not a foreign field.
1071
     */
1072
    public final SQLRowValues putRowValues(String fieldName) throws IllegalArgumentException {
1073
        // getForeignTable checks
1074
        final SQLRowValues vals = new SQLRowValues(this.getForeignTable(fieldName));
1075
        this.put(fieldName, vals);
1076
        return vals;
1077
    }
1078
 
93 ilm 1079
    public final SQLRowValues putRowValues(final Path p, final boolean createPath) throws IllegalArgumentException {
1080
        return this.put(p, createPath, null);
1081
    }
1082
 
17 ilm 1083
    /**
93 ilm 1084
     * Create or follow the passed path and put the passed row at the end.
1085
     *
1086
     * @param p the {@link Path#isSingleLink() single link} path.
1087
     * @param createPath <code>true</code> if new rows must {@link #createPathToOne(Path) always be
1088
     *        created}, <code>false</code> if existing rows can be {@link #assurePath(Path) used}.
1089
     * @param vals the row to {@link #put(Step, SQLRowValues) put}, <code>null</code> to create a
1090
     *        new one.
1091
     * @return the row that was added.
1092
     * @throws IllegalArgumentException if the path is invalid.
1093
     */
1094
    public final SQLRowValues put(final Path p, final boolean createPath, final SQLRowValues vals) throws IllegalArgumentException {
1095
        if (p.length() == 0)
1096
            throw new IllegalArgumentException("Empty path");
1097
        if (!p.isSingleLink())
1098
            throw new IllegalArgumentException("Multi-link path " + p);
1099
        // checks first table
1100
        final SQLRowValues beforeLast = createPath ? this.createPathToOne(p.minusLast()) : this.assurePath(p.minusLast());
1101
        // checks last table
1102
        return beforeLast.put(p.getStep(-1), vals);
1103
    }
1104
 
1105
    public final SQLRowValues putRowValues(final Step step) throws IllegalArgumentException {
1106
        return this.put(step, null);
1107
    }
1108
 
1109
    /**
1110
     * Add all links of the passed step from this to the passed row.
1111
     *
1112
     * @param step a step.
1113
     * @param vals a row, <code>null</code> to create a new one.
1114
     * @return the row that was linked.
1115
     * @throws IllegalArgumentException if the step is not from <code>this</code> to
1116
     *         <code>vals</code>.
1117
     */
1118
    public final SQLRowValues put(final Step step, SQLRowValues vals) throws IllegalArgumentException {
1119
        if (!step.getFrom().equals(this.getTable()))
1120
            throw new IllegalArgumentException(step + " not from " + this);
1121
        if (vals == null)
1122
            vals = new SQLRowValues(step.getTo());
1123
        else if (!step.getTo().equals(vals.getTable()))
1124
            throw new IllegalArgumentException(step + " not to " + vals);
1125
        for (final Link l : step.getLinks()) {
1126
            final Direction dir = step.getDirection(l);
1127
            if (dir == Direction.REFERENT) {
132 ilm 1128
                vals._putForeign(l, this);
93 ilm 1129
            } else {
1130
                assert dir == Direction.FOREIGN;
132 ilm 1131
                this._putForeign(l, vals);
93 ilm 1132
            }
1133
        }
1134
        return vals;
1135
    }
1136
 
132 ilm 1137
    private final SQLRowValues _putForeign(final Link l, SQLRowValues vals) {
1138
        this.put(l.getSingleField().getName(), vals);
1139
        return vals;
1140
    }
1141
 
1142
    public final SQLRowValues putForeign(final Link l, SQLRowValues vals) throws IllegalArgumentException {
1143
        if (!l.getSource().equals(this.getTable()))
1144
            throw new IllegalArgumentException(l + " not from " + this);
1145
        return _putForeign(l, vals);
1146
    }
1147
 
93 ilm 1148
    public final void remove(final Step step) {
1149
        for (final Link l : step.getLinks()) {
1150
            if (step.getDirection(l) == Direction.FOREIGN)
1151
                this.removeForeignKey(l);
1152
            else
132 ilm 1153
                this.removeReferentFields(l.getFields());
93 ilm 1154
        }
1155
    }
1156
 
1157
    /**
83 ilm 1158
     * Safely set the passed field to the value of the primary key of <code>r</code>.
1159
     *
1160
     * @param fk the field to change.
1161
     * @param r the row, <code>null</code> meaning {@link #SQL_EMPTY_LINK empty} foreign key.
1162
     * @return this.
1163
     * @throws IllegalArgumentException if <code>fk</code> doesn't point to the table of
1164
     *         <code>r</code>.
1165
     */
1166
    public final SQLRowValues putForeignID(final String fk, final SQLRowAccessor r) throws IllegalArgumentException {
1167
        return this.putForeignKey(Collections.singletonList(fk), r);
1168
    }
1169
 
1170
    public final SQLRowValues putForeignKey(final List<String> cols, final SQLRowAccessor r) throws IllegalArgumentException {
1171
        // first check that cols are indeed a foreign key
93 ilm 1172
        return this.putForeignKey(this.getForeignLink(cols), r);
1173
    }
1174
 
1175
    public final SQLRowValues putForeignKey(final Link foreignLink, final SQLRowAccessor r) throws IllegalArgumentException {
1176
        checkForeignLink(foreignLink);
1177
        final List<String> cols = foreignLink.getCols();
83 ilm 1178
        if (r == null) {
1179
            if (cols.size() == 1) {
1180
                return this.putEmptyLink(cols.get(0));
1181
            } else {
93 ilm 1182
                return this.putNulls(cols);
83 ilm 1183
            }
1184
        } else {
93 ilm 1185
            checkSameTable(r, foreignLink.getTarget());
1186
            final Iterator<String> iter = cols.iterator();
1187
            final Iterator<String> refIter = foreignLink.getRefCols().iterator();
1188
            while (iter.hasNext()) {
1189
                final String col = iter.next();
1190
                final String refCol = refIter.next();
1191
                this.put(col, r.getObject(refCol));
1192
            }
1193
            return this;
83 ilm 1194
        }
1195
    }
1196
 
93 ilm 1197
    private void checkForeignLink(final Link foreignLink) {
1198
        if (foreignLink.getSource() != this.getTable())
1199
            throw new IllegalArgumentException("Link not from " + this.getTable() + " : " + foreignLink);
1200
    }
1201
 
1202
    public final void removeForeignKey(final Link foreignLink) {
1203
        checkForeignLink(foreignLink);
1204
        this.removeAll(foreignLink.getCols());
1205
    }
1206
 
83 ilm 1207
    private void checkSameTable(final SQLRowAccessor r, final SQLTable t) {
1208
        if (r.getTable() != t)
1209
            throw new IllegalArgumentException("Table mismatch : " + r.getTable().getSQLName() + " != " + t.getSQLName());
1210
    }
1211
 
1212
    /**
17 ilm 1213
     * Set the order of this row so that it will be just after/before <code>r</code>. NOTE: this may
1214
     * reorder the table to make room.
1215
     *
1216
     * @param r the row to be next to.
1217
     * @param after whether this row will be before or after <code>r</code>.
1218
     * @return this.
1219
     */
1220
    public SQLRowValues setOrder(SQLRow r, boolean after) {
156 ilm 1221
        setOrder(Collections.singletonList(this), r, after);
1222
        return this;
17 ilm 1223
    }
1224
 
156 ilm 1225
    public static void setOrder(final List<SQLRowValues> values, final SQLRow r, boolean after) {
1226
        final int valuesCount = values.size();
1227
        final List<BigDecimal> orders;
1228
        try {
1229
            orders = ReOrder.getFreeOrderValuesFor(valuesCount, after, r).get0();
1230
        } catch (SQLException e) {
1231
            throw ExceptionUtils.createExn(IllegalStateException.class, "reorder failed for " + r.getTable() + " at " + r.getOrder(), e);
17 ilm 1232
        }
156 ilm 1233
        final String orderName = r.getTable().getOrderField().getName();
1234
        for (int i = 0; i < valuesCount; i++) {
1235
            values.get(i).put(orderName, orders.get(i));
1236
        }
17 ilm 1237
    }
1238
 
1239
    public final SQLRowValues setID(Number id) {
1240
        // faster
1241
        return this.setID(id, false);
1242
    }
1243
 
1244
    /***
1245
     * Set the {@link #getIDNumber() ID} of this row. Convert is useful to compare a row created in
1246
     * Java and a row returned from the database, since in Java the ID will be an integer whereas
1247
     * the DB can return anything.
1248
     *
1249
     * @param id the new ID.
1250
     * @param convert <code>true</code> if <code>id</code> should be converted to type of the
1251
     *        primary key.
1252
     * @return this.
1253
     */
1254
    public final SQLRowValues setID(Number id, final boolean convert) {
1255
        final SQLField key = this.getTable().getKey();
1256
        if (convert)
1257
            id = NumberConvertor.convert(id, key.getType().getJavaType().asSubclass(Number.class));
1258
 
1259
        return this.put(key.getName(), id);
1260
    }
1261
 
83 ilm 1262
    public final SQLRowValues setPrimaryKey(final SQLRowAccessor r) {
1263
        if (r == null) {
1264
            return this.putNulls(this.getTable().getPKsNames(), false);
1265
        } else {
1266
            checkSameTable(r, this.getTable());
1267
            // required since we don't want only half of the fields of the primary key
93 ilm 1268
            return this.loadAll(r.getAbsolutelyAll(), this.getTable().getPKsNames(new HashSet<String>()), true, FillMode.OVERWRITE);
83 ilm 1269
        }
1270
    }
1271
 
17 ilm 1272
    public final SQLRowValues setAll(Map<String, ?> m) {
93 ilm 1273
        return this.loadAll(m, FillMode.CLEAR);
17 ilm 1274
    }
1275
 
1276
    public final SQLRowValues putAll(Map<String, ?> m) {
93 ilm 1277
        return this.putAll(m, null);
17 ilm 1278
    }
1279
 
93 ilm 1280
    public final SQLRowValues putAll(Map<String, ?> m, final Collection<String> keys) {
144 ilm 1281
        return this.putAll(m, keys, FillMode.OVERWRITE);
83 ilm 1282
    }
1283
 
144 ilm 1284
    final SQLRowValues putAll(Map<String, ?> m, final Collection<String> keys, final FillMode fillMode) {
1285
        return this.loadAll(m, keys, false, fillMode);
1286
    }
1287
 
1288
    static enum FillMode {
93 ilm 1289
        CLEAR, OVERWRITE, DONT_OVERWRITE
1290
    }
1291
 
1292
    private final SQLRowValues loadAll(Map<String, ?> m, final FillMode fillMode) {
1293
        return this.loadAll(m, null, false, fillMode);
1294
    }
1295
 
1296
    private final SQLRowValues loadAll(Map<String, ?> m, final Collection<String> keys, final boolean required, final FillMode fillMode) {
83 ilm 1297
        final Collection<String> keySet = keys == null ? m.keySet() : keys;
132 ilm 1298
        if (!this.getTable().getFieldsName().containsAll(keySet)) {
1299
            final List<String> l1 = new ArrayList<String>(keySet);
1300
            final List<String> l2 = new ArrayList<String>(this.getTable().getFieldsName());
1301
            Collections.sort(l1);
1302
            Collections.sort(l2);
1303
            throw new IllegalArgumentException("fields " + l1 + " are not a subset of " + this.getTable() + " : " + l2);
1304
        }
83 ilm 1305
        // copy before passing to fire()
93 ilm 1306
        final Map<String, Object> toLoad = new LinkedHashMap<String, Object>(m);
83 ilm 1307
        if (keys != null) {
1308
            if (required && !m.keySet().containsAll(keys))
1309
                throw new IllegalArgumentException("Not all are keys " + keys + " are in " + m);
1310
            toLoad.keySet().retainAll(keys);
1311
        }
93 ilm 1312
        if (fillMode == FillMode.CLEAR) {
17 ilm 1313
            clear();
93 ilm 1314
        } else if (fillMode == FillMode.DONT_OVERWRITE) {
1315
            toLoad.keySet().removeAll(this.getFields());
1316
        }
83 ilm 1317
        for (final Map.Entry<String, ?> e : toLoad.entrySet()) {
142 ilm 1318
            // names are checked at the start
1319
            this._put(e.getKey(), e.getValue(), false, ValueOperation.CONVERT);
17 ilm 1320
        }
83 ilm 1321
        // if there's no graph, there can't be any listeners
1322
        final SQLRowValuesCluster graph = this.getGraph(false);
1323
        if (graph != null)
1324
            graph.fireModification(this, toLoad);
17 ilm 1325
        return this;
1326
    }
1327
 
1328
    public final SQLRowValues putNulls(String... fields) {
83 ilm 1329
        return this.putNulls(Arrays.asList(fields));
17 ilm 1330
    }
1331
 
83 ilm 1332
    public final SQLRowValues putNulls(Collection<String> fields) {
1333
        return this.putNulls(fields, false);
1334
    }
1335
 
17 ilm 1336
    /**
1337
     * Set the passed fields to <code>null</code>.
1338
     *
1339
     * @param fields which fields to put.
1340
     * @param ignoreInexistant <code>true</code> if non existing field should be ignored,
1341
     *        <code>false</code> will throw an exception if a field doesn't exist.
1342
     * @return this.
1343
     */
1344
    public final SQLRowValues putNulls(Collection<String> fields, final boolean ignoreInexistant) {
93 ilm 1345
        return this.fill(fields, null, ignoreInexistant, false);
1346
    }
1347
 
1348
    /**
1349
     * Put the same value in all the passed fields.
1350
     *
1351
     * @param fields fields to change, <code>null</code> meaning all the fields of the table.
1352
     * @param val the value to put, can be <code>null</code>.
1353
     * @param ignoreInexistant if <code>fields</code> that aren't in the table should be ignored
1354
     *        (not used if <code>fields</code> is <code>null</code>).
1355
     * @param ignoreExisting <code>true</code> if no value should be overwritten.
1356
     * @return this.
1357
     * @throws IllegalArgumentException if <code>!ignoreInexistant</code> and some fields aren't in
1358
     *         the table.
1359
     */
1360
    public final SQLRowValues fill(final Collection<String> fields, final Object val, final boolean ignoreInexistant, final boolean ignoreExisting) throws IllegalArgumentException {
1361
        final Set<String> tableFieldsNames = getTable().getFieldsName();
17 ilm 1362
        // keep order
93 ilm 1363
        final Set<String> actualFields = fields == null ? tableFieldsNames : new LinkedHashSet<String>(fields);
1364
        final Map<String, Object> m = createLinkedHashMap(actualFields.size());
1365
        for (final String fn : actualFields) {
1366
            if (fields == null || !ignoreInexistant || tableFieldsNames.contains(fn))
1367
                m.put(fn, val);
17 ilm 1368
        }
93 ilm 1369
        return this.loadAll(m, ignoreExisting ? FillMode.DONT_OVERWRITE : FillMode.OVERWRITE);
17 ilm 1370
    }
1371
 
1372
    /**
93 ilm 1373
     * Fill all fields with the passed value.
1374
     *
1375
     * @param val the value to put, can be <code>null</code>.
1376
     * @param overwrite <code>true</code> if existing values must be replaced.
1377
     * @return this.
1378
     */
1379
    public final SQLRowValues fillWith(final Object val, final boolean overwrite) {
1380
        return this.fill(null, val, false, !overwrite);
1381
    }
1382
 
1383
    /**
17 ilm 1384
     * Set all the fields (including primary and foreign keys) of this row to <code>null</code>.
1385
     *
1386
     * @return this.
1387
     */
1388
    public final SQLRowValues setAllToNull() {
93 ilm 1389
        return this.fillWith(null, true);
17 ilm 1390
    }
1391
 
1392
    // listener
1393
 
1394
    public class ReferentChangeEvent extends EventObject {
1395
 
1396
        private final SQLField f;
1397
        private final SQLRowValues vals;
1398
        private final boolean put;
1399
 
1400
        public ReferentChangeEvent(SQLField f, boolean put, SQLRowValues vals) {
1401
            super(SQLRowValues.this);
1402
            assert f != null && f.getDBSystemRoot().getGraph().getForeignTable(f) == getSource().getTable() && f.getTable() == vals.getTable();
1403
            this.f = f;
1404
            this.put = put;
1405
            this.vals = vals;
1406
        }
1407
 
1408
        // eg SITE[2]
1409
        @Override
1410
        public SQLRowValues getSource() {
1411
            return (SQLRowValues) super.getSource();
1412
        }
1413
 
1414
        // eg ID_SITE
1415
        public final SQLField getField() {
1416
            return this.f;
1417
        }
1418
 
1419
        // eg BATIMENT[3]
1420
        public final SQLRowValues getChangedReferent() {
1421
            return this.vals;
1422
        }
1423
 
1424
        // true if getChangedReferent() is a new referent of getSource(), false if it has been
1425
        // removed from getSource()
1426
        public final boolean isAddition() {
1427
            return this.put;
1428
        }
1429
 
1430
        public final boolean isRemoval() {
1431
            return !this.isAddition();
1432
        }
1433
 
1434
        @Override
1435
        public String toString() {
1436
            return this.getClass().getSimpleName() + (this.isAddition() ? " added" : " removed") + " on field " + getField() + " from " + this.getSource().asRow() + " : " + getChangedReferent();
1437
        }
1438
    }
1439
 
1440
    public static interface ReferentChangeListener extends EventListener {
1441
 
1442
        void referentChange(ReferentChangeEvent evt);
1443
 
1444
    }
1445
 
1446
    /**
1447
     * Adds a listener to referent rows.
1448
     *
1449
     * @param field the referent field to listen to, <code>null</code> meaning all.
1450
     * @param l the listener.
1451
     */
1452
    public final void addReferentListener(SQLField field, ReferentChangeListener l) {
1453
        if (this.referentsListener == null)
81 ilm 1454
            this.referentsListener = new ListMap<SQLField, ReferentChangeListener>();
1455
        this.referentsListener.add(field, l);
17 ilm 1456
    }
1457
 
1458
    public final void removeReferentListener(SQLField field, ReferentChangeListener l) {
1459
        if (this.referentsListener != null) {
144 ilm 1460
            this.referentsListener.removeOne(field, l);
17 ilm 1461
        }
1462
    }
1463
 
1464
    private void fireRefChange(SQLField f, boolean put, SQLRowValues vals) {
1465
        // only create event if needed
1466
        if (this.referentsListener != null || this.getGraph().referentFireNeeded(put)) {
1467
            final ReferentChangeEvent evt = new ReferentChangeEvent(f, put, vals);
1468
            if (this.referentsListener != null) {
1469
                for (final ReferentChangeListener l : this.referentsListener.getNonNull(f))
1470
                    l.referentChange(evt);
1471
                for (final ReferentChangeListener l : this.referentsListener.getNonNull(null))
1472
                    l.referentChange(evt);
1473
            }
83 ilm 1474
            // no need to avoid creating graph, as this is called when the graph change
1475
            assert this.graph != null;
17 ilm 1476
            this.getGraph().fireModification(evt);
1477
        }
1478
    }
1479
 
1480
    public final void addValueListener(ValueChangeListener l) {
1481
        this.getGraph().addValueListener(this, l);
1482
    }
1483
 
1484
    public final void removeValueListener(ValueChangeListener l) {
1485
        this.getGraph().removeValueListener(this, l);
1486
    }
1487
 
67 ilm 1488
    @Override
1489
    public final Collection<SQLRowValues> followLink(final Link l, final Direction direction) {
80 ilm 1490
        return this.followPath(Path.get(getTable()).add(l, direction), CreateMode.CREATE_NONE, false);
67 ilm 1491
    }
1492
 
17 ilm 1493
    /**
1494
     * Create the necessary SQLRowValues so that the graph of this row goes along the passed path.
1495
     *
1496
     * @param p the path of SQLRowValues, eg "LOCAL.ID_BATIMENT,BATIMENT.ID_SITE".
1497
     * @return the SQLRowValues at the end of the path, eg a SQLRowValues on /SITE/.
1498
     */
1499
    public final SQLRowValues assurePath(final Path p) {
1500
        return this.followPath(p, true);
1501
    }
1502
 
1503
    /**
1504
     * Return the row at the end of passed path.
1505
     *
73 ilm 1506
     * @param p the path to follow, e.g. SITE,SITE.ID_CONTACT_CHEF.
1507
     * @return the row at the end or <code>null</code> if none exists, e.g. SQLRowValues on
1508
     *         /CONTACT/.
17 ilm 1509
     */
1510
    public final SQLRowValues followPath(final Path p) {
1511
        return this.followPath(p, false);
1512
    }
1513
 
1514
    private final SQLRowValues followPath(final Path p, final boolean create) {
73 ilm 1515
        return followPathToOne(p, create ? CreateMode.CREATE_ONE : CreateMode.CREATE_NONE, DEFAULT_ALLOW_BACKTRACK);
1516
    }
1517
 
1518
    /**
1519
     * Follow path to at most one row.
1520
     *
1521
     * @param p the path to follow.
1522
     * @param create if and how to create new rows.
1523
     * @param allowBackTrack <code>true</code> to allow encountering the same row more than once.
1524
     * @return the destination row or <code>null</code> if none exists and <code>create</code> was
1525
     *         {@link CreateMode#CREATE_NONE}
1526
     * @see #followPath(Path, CreateMode, boolean, boolean)
1527
     */
1528
    public final SQLRowValues followPathToOne(final Path p, final CreateMode create, final boolean allowBackTrack) {
1529
        final Collection<SQLRowValues> res = this.followPath(p, create, true, allowBackTrack);
67 ilm 1530
        // since we passed onlyOne=true
1531
        assert res.size() <= 1;
1532
        return CollectionUtils.getSole(res);
1533
    }
1534
 
1535
    /**
1536
     * Return the rows at the end of the passed path.
1537
     *
1538
     * @param path a path, e.g. SITE, BATIMENT, LOCAL.
1539
     * @return the existing rows at the end of <code>path</code>, never <code>null</code>, e.g.
1540
     *         [LOCAL[3], LOCAL[5]].
1541
     */
1542
    public final Collection<SQLRowValues> getDistantRows(final Path path) {
1543
        return followPath(path, CreateMode.CREATE_NONE, false);
1544
    }
1545
 
93 ilm 1546
    /**
132 ilm 1547
     * Create all rows on the passed path and add them to this. There's {@link CreateMode#CREATE_ONE
1548
     * one row} per step.
93 ilm 1549
     *
1550
     * @param p the path.
1551
     * @return the row at the end of the path.
1552
     * @throws IllegalStateException if the first step is a non-empty foreign link.
1553
     */
1554
    public final SQLRowValues createPathToOne(final Path p) {
1555
        final Collection<SQLRowValues> res = this.createPath(p, true);
1556
        assert res.size() == 1;
1557
        return res.iterator().next();
1558
    }
1559
 
1560
    /**
1561
     * Create all rows on the passed path and add them to this.
1562
     *
1563
     * @param p the path.
1564
     * @param createOne <code>true</code> to {@link CreateMode#CREATE_ONE create one} row per step,
1565
     *        <code>false</code> to {@link CreateMode#CREATE_MANY create} one row per link.
1566
     * @return the rows at the end of the path.
1567
     * @throws IllegalStateException if the first step is a non-empty foreign link.
1568
     */
1569
    public final Collection<SQLRowValues> createPath(final Path p, final boolean createOne) {
1570
        return this.followPath(p, createOne ? CreateMode.CREATE_ONE : CreateMode.CREATE_MANY, true, false, null);
1571
    }
1572
 
67 ilm 1573
    public final Collection<SQLRowValues> followPath(final Path p, final CreateMode create, final boolean onlyOne) {
73 ilm 1574
        return followPath(p, create, onlyOne, DEFAULT_ALLOW_BACKTRACK);
1575
    }
1576
 
1577
    /**
1578
     * Follow path through the graph.
1579
     *
1580
     * @param p the path to follow.
1581
     * @param create if and how to create new rows.
1582
     * @param onlyOne <code>true</code> if this method should return at most one row.
1583
     * @param allowBackTrack <code>true</code> to allow encountering the same row more than once.
1584
     * @return the destination rows, can be empty.
1585
     * @throws IllegalArgumentException if <code>p</code> doesn't start with this table.
1586
     * @throws IllegalStateException if <code>onlyOne</code> and there's more than one row on the
1587
     *         path.
1588
     */
132 ilm 1589
    public final Collection<SQLRowValues> followPath(final Path p, final CreateMode create, final boolean onlyOne, final boolean allowBackTrack)
1590
            throws IllegalArgumentException, IllegalStateException {
93 ilm 1591
        return followPath(p, create, false, onlyOne, allowBackTrack ? null : new LinkedIdentitySet<SQLRowValues>());
73 ilm 1592
    }
1593
 
93 ilm 1594
    // if alwaysCreate : CREATE_NONE is invalid and existing rows are ignored (i.e. rows are always
1595
    // created and an exception is thrown if there's a non-empty foreign link (perhaps add a force
1596
    // mode to replace it))
1597
    private final IdentitySet<SQLRowValues> followPath(final Path p, final CreateMode create, final boolean alwaysCreate, final boolean onlyOne, final IdentitySet<SQLRowValues> beenThere) {
17 ilm 1598
        if (p.getFirst() != this.getTable())
1599
            throw new IllegalArgumentException("path " + p + " doesn't start with us " + this);
93 ilm 1600
        final boolean neverCreate = create == CreateMode.CREATE_NONE;
1601
        if (alwaysCreate && neverCreate)
1602
            throw new IllegalArgumentException("If alwaysCreate, don't pass " + create);
1603
        if (alwaysCreate && beenThere != null)
1604
            throw new IllegalArgumentException("If alwaysCreate, existing rows are ignored so the same row can never be visited more than once");
17 ilm 1605
        if (p.length() > 0) {
67 ilm 1606
            // fail-fast : avoid creating rows
1607
            if (onlyOne && create == CreateMode.CREATE_MANY && !p.isSingleLink())
1608
                throw new IllegalStateException("more than one link with " + create + " and onlyOne : " + p);
1609
 
1610
            final Step firstStep = p.getStep(0);
73 ilm 1611
            final Set<Link> ffs = firstStep.getLinks();
93 ilm 1612
            final SetMap<Link, SQLRowValues> existingRows = createSetMap(-1, 6);
1613
            final Set<Link> linksToCreate = neverCreate ? Collections.<Link> emptySet() : new HashSet<Link>();
73 ilm 1614
            for (final Link l : ffs) {
1615
                final SQLField ff = l.getLabel();
1616
                if (firstStep.isForeign(l)) {
67 ilm 1617
                    final Object fkValue = this.getObject(ff.getName());
73 ilm 1618
                    if (fkValue instanceof SQLRowValues && (beenThere == null || !beenThere.contains(fkValue))) {
93 ilm 1619
                        if (alwaysCreate)
1620
                            throw new IllegalStateException("alwaysCreate=true but foreign link is not empty : " + l);
1621
                        existingRows.add(l, (SQLRowValues) fkValue);
1622
                    } else if (!neverCreate) {
1623
                        linksToCreate.add(l);
67 ilm 1624
                    }
1625
                } else {
1626
                    final Set<SQLRowValues> referentRows = this.getReferentRows(ff);
73 ilm 1627
                    final Set<SQLRowValues> validReferentRows;
1628
                    if (beenThere == null || beenThere.size() == 0) {
1629
                        validReferentRows = referentRows;
1630
                    } else {
1631
                        validReferentRows = new LinkedIdentitySet<SQLRowValues>(referentRows);
1632
                        validReferentRows.removeAll(beenThere);
1633
                    }
93 ilm 1634
                    final boolean hasRef = validReferentRows.size() > 0;
1635
                    if (hasRef) {
1636
                        existingRows.addAll(l, validReferentRows);
67 ilm 1637
                    }
93 ilm 1638
                    if (alwaysCreate || !neverCreate && !hasRef) {
1639
                        linksToCreate.add(l);
1640
                    }
67 ilm 1641
                }
17 ilm 1642
            }
93 ilm 1643
            assert !alwaysCreate || linksToCreate.size() > 0;
1644
            // Set is needed when a row is multi-linked to another (to avoid calling recursively
1645
            // followPath() on the same instance)
1646
            // IdentitySet is needed since multiple rows can be equal, e.g. empty rows :
1647
            // SITE -- chef -> CONTACT
1648
            // _____-- rapport -> CONTACT
1649
            final Set<SQLRowValues> next = new LinkedIdentitySet<SQLRowValues>();
1650
            // by definition alwaysCreate implies ignoring existing rows
1651
            if (!alwaysCreate)
1652
                next.addAll(existingRows.allValues());
1653
            final int existingCount = next.size();
1654
            if (onlyOne && existingCount > 1)
1655
                throw new IllegalStateException("more than one row exist and onlyOne=true : " + existingRows);
67 ilm 1656
 
93 ilm 1657
            final int newCount;
1658
            if (create == CreateMode.CREATE_MANY) {
1659
                newCount = existingCount + linksToCreate.size();
1660
            } else if (create == CreateMode.CREATE_ONE) {
1661
                // only enforce if we're creating rows, otherwise use "onlyOne"
1662
                if (linksToCreate.size() > 0 && existingCount > 1)
1663
                    throw new IllegalStateException("more than one row exist and " + create + ", this step won't be between two rows : " + existingRows);
1664
                newCount = Math.max(existingCount, 1);
1665
            } else {
1666
                assert neverCreate;
1667
                newCount = existingCount;
1668
            }
1669
            if (onlyOne && newCount > 1)
1670
                throw new IllegalStateException("Will have more than one row and onlyOne=true : " + existingRows + " to create : " + linksToCreate);
1671
 
1672
            for (final Link l : linksToCreate) {
1673
                final SQLField ff = l.getLabel();
1674
                final boolean isForeign = firstStep.isForeign(l);
1675
 
1676
                final SQLRowValues nextOne;
1677
                if (create == CreateMode.CREATE_ONE && next.size() == 1) {
1678
                    nextOne = next.iterator().next();
1679
                } else {
1680
                    assert create == CreateMode.CREATE_MANY || (create == CreateMode.CREATE_ONE && next.size() == 0) : "Creating more than one, already " + next.size();
1681
                    nextOne = new SQLRowValues(firstStep.getTo());
1682
                    if (isForeign) {
1683
                        // keep the id, if present
1684
                        final Object fkValue = this.getObject(ff.getName());
1685
                        if (fkValue instanceof Number)
1686
                            nextOne.setID((Number) fkValue);
1687
                    }
1688
                    next.add(nextOne);
1689
                }
1690
                if (isForeign) {
1691
                    this.put(ff.getName(), nextOne);
1692
                } else {
1693
                    nextOne.put(ff.getName(), this);
1694
                }
1695
            }
1696
            // already checked above
1697
            assert !(onlyOne && next.size() > 1);
1698
 
67 ilm 1699
            // see comment above for IdentitySet
73 ilm 1700
            final IdentitySet<SQLRowValues> res = new LinkedIdentitySet<SQLRowValues>();
67 ilm 1701
            for (final SQLRowValues n : next) {
73 ilm 1702
                final IdentitySet<SQLRowValues> newBeenThere;
1703
                if (beenThere == null) {
1704
                    newBeenThere = null;
1705
                } else {
1706
                    newBeenThere = new LinkedIdentitySet<SQLRowValues>(beenThere);
1707
                    final boolean added = newBeenThere.add(this);
1708
                    assert added;
1709
                }
93 ilm 1710
                res.addAll(n.followPath(p.minusFirst(), create, alwaysCreate, onlyOne, newBeenThere));
67 ilm 1711
            }
1712
            return res;
1713
        } else {
73 ilm 1714
            return CollectionUtils.createIdentitySet(this);
67 ilm 1715
        }
17 ilm 1716
    }
1717
 
132 ilm 1718
    public final SQLRowValues changeForeigns(ForeignCopyMode mode) {
1719
        return this.changeForeigns(null, false, mode);
17 ilm 1720
    }
1721
 
132 ilm 1722
    static private final boolean isEmpty(final Collection<?> coll, final boolean exclude) {
1723
        if (exclude) {
1724
            return coll == null;
1725
        } else {
1726
            return coll != null && coll.isEmpty();
17 ilm 1727
        }
132 ilm 1728
    }
1729
 
1730
    public final SQLRowValues changeForeigns(final Collection<String> fields, final boolean exclude, final ForeignCopyMode mode) {
1731
        if (!isEmpty(fields, exclude) && mode != ForeignCopyMode.COPY_ROW) {
1732
            // copy otherwise ConcurrentModificationException
1733
            for (final String ff : new ArrayList<String>(this.getForeigns().keySet())) {
1734
                // fields == null means include all thanks to the above if
1735
                if (fields == null || fields.contains(ff) != exclude) {
1736
                    this.flatten(ff, mode);
1737
                }
1738
            }
1739
        }
17 ilm 1740
        return this;
1741
    }
1742
 
93 ilm 1743
    /**
1744
     * Flatten a foreign row values. NOTE : if there's no foreign row in <code>ff</code>, this
1745
     * method does nothing.
1746
     *
1747
     * @param ff a foreign field.
1748
     * @param mode how to flatten.
1749
     * @return this.
1750
     */
1751
    public final SQLRowValues flatten(final String ff, final ForeignCopyMode mode) {
1752
        if (mode != ForeignCopyMode.COPY_ROW) {
1753
            final SQLRowValues foreign = this.foreigns.get(ff);
1754
            if (foreign != null) {
1755
                if (mode == ForeignCopyMode.COPY_NULL) {
1756
                    this.put(ff, null);
1757
                } else if (mode == ForeignCopyMode.NO_COPY) {
1758
                    this.remove(ff);
1759
                } else if (foreign.hasID()) {
1760
                    assert mode == ForeignCopyMode.COPY_ID_OR_ROW || mode == ForeignCopyMode.COPY_ID_OR_RM;
1761
                    this.put(ff, foreign.getIDNumber());
1762
                } else if (mode == ForeignCopyMode.COPY_ID_OR_RM) {
1763
                    this.remove(ff);
1764
                } else {
1765
                    assert mode == ForeignCopyMode.COPY_ID_OR_ROW && !foreign.hasID();
1766
                }
1767
            }
1768
        }
1769
        return this;
1770
    }
1771
 
17 ilm 1772
    // *** load
1773
 
1774
    public void loadAbsolutelyAll(SQLRow row) {
1775
        this.setAll(row.getAbsolutelyAll());
1776
    }
1777
 
1778
    /**
1779
     * Load values from the passed row (and remove them if possible).
1780
     *
1781
     * @param row the row to load values from.
1782
     * @param fieldsNames what fields to load, <code>null</code> meaning all.
1783
     */
132 ilm 1784
    public void load(SQLRowAccessor row, final Collection<String> fieldsNames) {
17 ilm 1785
        // make sure we only define keys that row has
1786
        // allow load( {'A':a, 'B':b}, {'A', 'B', 'C' } ) to not define 'C' to null
93 ilm 1787
        final Map<String, Object> m = new LinkedHashMap<String, Object>(row.getAbsolutelyAll());
17 ilm 1788
        if (fieldsNames != null)
1789
            m.keySet().retainAll(fieldsNames);
1790
 
1791
        // rm the added fields otherwise this and row will be linked
1792
        // eg load LOCAL->BATIMENT into a LOCAL will result in the BATIMENT
1793
        // being pointed to by both LOCAL
1794
        if (row instanceof SQLRowValues)
1795
            ((SQLRowValues) row).removeAll(m.keySet());
1796
 
1797
        // put after remove so that this graph never contains row (and thus avoids unneeded events)
1798
        this.putAll(m);
1799
    }
1800
 
144 ilm 1801
    public void merge(final SQLRowValues v) {
1802
        this.getGraph().merge(this, v);
1803
    }
1804
 
17 ilm 1805
    // *** modify
1806
 
1807
    void checkValidity() {
1808
        // this checks archived which the DB doesn't with just foreign constraints
142 ilm 1809
        // it also locks foreign rows so that they don't *become* archived
17 ilm 1810
        final Object[] pb = this.getInvalid();
1811
        if (pb != null)
1812
            throw new IllegalStateException("can't update " + this + " : the field " + pb[0] + " points to " + pb[1]);
1813
    }
1814
 
1815
    /**
144 ilm 1816
     * Return the first problem with a foreign key.
17 ilm 1817
     *
1818
     * @return <code>null</code> si pas de pb, sinon un Object[] :
1819
     *         <ol>
1820
     *         <li>en 0 le nom du champ posant pb, eg "ID_OBSERVATION_2"</li>
1821
     *         <li>en 1 une SQLRow décrivant le pb, eg "(OBSERVATION[123])"</li>
1822
     *         </ol>
1823
     */
83 ilm 1824
    public Object[] getInvalid() {
142 ilm 1825
        final Map<String, Link> foreignLinks = new HashMap<String, Link>();
132 ilm 1826
        for (final Link foreignLink : this.getTable().getForeignLinks()) {
142 ilm 1827
            for (final String f : foreignLink.getCols()) {
93 ilm 1828
                foreignLinks.put(f, foreignLink);
1829
            }
1830
        }
1831
 
17 ilm 1832
        for (final String fieldName : this.values.keySet()) {
142 ilm 1833
            final Link foreignLink = foreignLinks.remove(fieldName);
93 ilm 1834
            if (foreignLink != null) {
1835
                final SQLTable foreignTable = foreignLink.getTarget();
1836
                if (foreignTable.isRowable()) {
1837
                    // otherwise would have to check more than field
1838
                    assert foreignLink.getCols().size() == 1;
1839
                    // verifie l'intégrité (a rowValues is obviously correct, as is EMPTY,
1840
                    // DEFAULT is the responsability of the DB)
1841
                    final Object fieldVal = this.getObject(fieldName);
1842
                    if (fieldVal != null && fieldVal != SQL_DEFAULT && !(fieldVal instanceof SQLRowValues)) {
1843
                        final SQLRow pb = foreignTable.checkValidity(((Number) fieldVal).intValue());
1844
                        if (pb != null)
1845
                            return new Object[] { fieldName, pb };
1846
                    }
1847
                } else {
1848
                    // check that the foreign key is complete
1849
                    for (final String ff : foreignLink.getCols()) {
151 ilm 1850
                        if (!this.contains(ff))
93 ilm 1851
                            return new Object[] { ff, null };
1852
                    }
144 ilm 1853
                    foreignLinks.keySet().removeAll(foreignLink.getCols());
93 ilm 1854
                    // MAYBE also check foreign row is valid
1855
                }
1856
            } // else not a foreign key or already checked
17 ilm 1857
        }
1858
        return null;
1859
    }
1860
 
1861
    // * insert
1862
 
1863
    /**
1864
     * Insert a new line (strips the primary key, it must be db generated and strips order, added at
1865
     * the end).
1866
     *
1867
     * @return the newly inserted line, or <code>null</code> if the table has not exactly one
1868
     *         primary key.
1869
     * @throws SQLException if an error occurs while inserting.
1870
     * @throws IllegalStateException if the ID of the new line cannot be retrieved.
1871
     */
83 ilm 1872
    public SQLRow insert() throws SQLException {
17 ilm 1873
        // remove unwanted fields, keep ARCHIVE
142 ilm 1874
        return this.store(SQLRowValuesCluster.StoreMode.INSERT);
17 ilm 1875
    }
1876
 
1877
    /**
1878
     * Insert a new line verbatim. ATTN the primary key must not exist.
1879
     *
1880
     * @return the newly inserted line, or <code>null</code> if the table has not exactly one
1881
     *         primary key.
1882
     * @throws SQLException if an error occurs while inserting.
1883
     * @throws IllegalStateException if the ID of the new line cannot be retrieved.
1884
     */
83 ilm 1885
    public SQLRow insertVerbatim() throws SQLException {
142 ilm 1886
        return this.store(SQLRowValuesCluster.StoreMode.INSERT_VERBATIM);
17 ilm 1887
    }
1888
 
83 ilm 1889
    public SQLRow insert(final boolean insertPK, final boolean insertOrder) throws SQLException {
142 ilm 1890
        return this.store(new SQLRowValuesCluster.Insert(insertPK, insertOrder));
17 ilm 1891
    }
1892
 
142 ilm 1893
    public SQLRow store(final SQLRowValuesCluster.StoreMode mode) throws SQLException {
1894
        return this.getGraph().store(mode).getStoredRow(this);
1895
    }
1896
 
1897
    SQLTableEvent insertJustThis(final boolean fetchStoredRow, final Set<SQLField> autoFields) throws SQLException {
17 ilm 1898
        final Map<String, Object> copy = this.clearFields(new HashMap<String, Object>(this.values), autoFields);
1899
 
1900
        try {
1901
            final Tuple2<List<String>, Number> fieldsAndID = this.getTable().getBase().getDataSource().useConnection(new ConnectionHandlerNoSetup<Tuple2<List<String>, Number>, SQLException>() {
1902
                @Override
1903
                public Tuple2<List<String>, Number> handle(SQLDataSource ds) throws SQLException {
1904
                    final Tuple2<PreparedStatement, List<String>> pStmt = createInsertStatement(getTable(), copy);
1905
                    try {
1906
                        final Number newID = insert(pStmt.get0(), getTable());
1907
                        // MAYBE keep the pStmt around while values.keySet() doesn't change
1908
                        pStmt.get0().close();
1909
                        return Tuple2.create(pStmt.get1(), newID);
1910
                    } catch (Exception e) {
1911
                        throw new SQLException("Unable to insert " + pStmt.get0(), e);
1912
                    }
1913
                }
1914
            });
1915
 
1916
            assert this.getTable().isRowable() == (fieldsAndID.get1() != null);
1917
            if (this.getTable().isRowable()) {
1918
                // pour pouvoir avoir les valeurs des champs non précisés
142 ilm 1919
                return new SQLTableEvent(getEventRow(fieldsAndID.get1().intValue(), fetchStoredRow), Mode.ROW_ADDED, fieldsAndID.get0());
17 ilm 1920
            } else
1921
                return new SQLTableEvent(getTable(), SQLRow.NONEXISTANT_ID, Mode.ROW_ADDED, fieldsAndID.get0());
1922
        } catch (SQLException e) {
1923
            throw new SQLException("unable to insert " + this + " using " + copy, e);
1924
        }
1925
    }
1926
 
142 ilm 1927
    private SQLRow getEventRow(final int newID, final boolean fetch) {
1928
        final SQLRow res;
1929
        if (fetch) {
1930
            // don't read the cache since no event has been fired yet
1931
            // don't write to it since the transaction isn't committed yet, so other threads
1932
            // should not see the new values.
1933
            res = new SQLRow(getTable(), newID).fetchValues(false);
1934
        } else {
1935
            res = SQLRow.createEmpty(getTable(), newID);
1936
        }
1937
        assert res.isFilled();
1938
        return res;
17 ilm 1939
    }
1940
 
1941
    // * update
1942
 
1943
    public SQLRow update() throws SQLException {
1944
        if (!hasID()) {
83 ilm 1945
            throw new IllegalStateException("can't update : no ID specified, use update(int) or set ID for " + this);
17 ilm 1946
        }
83 ilm 1947
        return this.commit();
17 ilm 1948
    }
1949
 
1950
    public SQLRow update(final int id) throws SQLException {
1951
        this.put(this.getTable().getKey().getName(), id);
1952
        return this.commit();
1953
    }
1954
 
1955
    /**
1956
     * Permet de mettre à jour une ligne existante avec les valeurs courantes.
1957
     *
142 ilm 1958
     * @param fetchStoredRow <code>true</code> to fetch the just stored row.
17 ilm 1959
     * @param id l'id à mettre à jour.
1960
     * @return the updated row.
1961
     * @throws SQLException si pb lors de la maj.
1962
     */
142 ilm 1963
    SQLTableEvent updateJustThis(boolean fetchStoredRow, final int id) throws SQLException {
17 ilm 1964
        if (id == this.getTable().getUndefinedID()) {
83 ilm 1965
            throw new IllegalArgumentException("can't update undefined with " + this);
17 ilm 1966
        }
1967
        // clear primary key, otherwise we might end up with :
1968
        // UPDATE TABLE SET ID=123,DESIGNATION='aa' WHERE id=456
1969
        // which will delete ID 456, and possibly cause a conflict with preexisting ID 123
1970
        final Map<String, Object> updatedValues = this.clearPrimaryKeys(new HashMap<String, Object>(this.values));
1971
 
83 ilm 1972
        final List<String> updatedCols;
1973
        if (updatedValues.isEmpty()) {
1974
            updatedCols = Collections.emptyList();
1975
        } else {
1976
            updatedCols = this.getTable().getDBSystemRoot().getDataSource().useConnection(new ConnectionHandlerNoSetup<List<String>, SQLException>() {
1977
                @Override
1978
                public List<String> handle(SQLDataSource ds) throws SQLException {
1979
                    final Tuple2<PreparedStatement, List<String>> pStmt = createUpdateStatement(getTable(), updatedValues, id);
1980
                    final long timeMs = System.currentTimeMillis();
1981
                    final long time = System.nanoTime();
142 ilm 1982
                    final int updateCount = pStmt.get0().executeUpdate();
83 ilm 1983
                    final long afterExecute = System.nanoTime();
1984
                    // logging after closing fails to get the connection info
1985
                    SQLRequestLog.log(pStmt.get0(), "rowValues.update()", timeMs, time, afterExecute, afterExecute, afterExecute, afterExecute, System.nanoTime());
1986
                    pStmt.get0().close();
142 ilm 1987
                    if (updateCount > 1)
1988
                        throw new IllegalStateException(updateCount + " rows updated with ID " + id);
1989
                    return updateCount == 0 ? null : pStmt.get1();
83 ilm 1990
                }
1991
            });
1992
        }
17 ilm 1993
 
142 ilm 1994
        return updatedCols == null ? null : new SQLTableEvent(getEventRow(id, fetchStoredRow), Mode.ROW_UPDATED, updatedCols);
17 ilm 1995
    }
1996
 
1997
    // * commit
1998
 
1999
    /**
2000
     * S'assure que ces valeurs arrivent dans la base. Si la ligne possède un ID équivaut à update()
2001
     * sinon insert().
2002
     *
2003
     * @return the affected row.
2004
     * @throws SQLException
2005
     */
2006
    public SQLRow commit() throws SQLException {
142 ilm 2007
        return this.store(SQLRowValuesCluster.StoreMode.COMMIT);
17 ilm 2008
    }
2009
 
142 ilm 2010
    SQLTableEvent commitJustThis(boolean fetchStoredRow) throws SQLException {
17 ilm 2011
        if (!hasID()) {
142 ilm 2012
            return this.insertJustThis(fetchStoredRow, Collections.<SQLField> emptySet());
17 ilm 2013
        } else
142 ilm 2014
            return this.updateJustThis(fetchStoredRow, this.getID());
17 ilm 2015
    }
2016
 
2017
    /**
2018
     * Returns a string representation of this (excluding any foreign or referent rows).
2019
     *
2020
     * @return a compact representation of this.
2021
     * @see #printGraph()
2022
     */
2023
    @Override
2024
    public String toString() {
151 ilm 2025
        return mapToString();
2026
    }
2027
 
2028
    @Override
2029
    public String mapToString() {
17 ilm 2030
        String result = this.getClass().getSimpleName() + " on " + this.getTable() + " : {";
2031
        result += CollectionUtils.join(this.values.entrySet(), ", ", new ITransformer<Entry<String, ?>, String>() {
2032
            public String transformChecked(final Entry<String, ?> e) {
2033
                final String className = e.getValue() == null ? "" : "(" + e.getValue().getClass() + ")";
2034
                final String value;
2035
                // avoid infinite loop (and overly verbose string)
2036
                if (e.getValue() instanceof SQLRowValues) {
2037
                    final SQLRowValues foreignVals = (SQLRowValues) e.getValue();
2038
                    if (foreignVals == SQLRowValues.this) {
2039
                        value = "this";
2040
                    } else if (foreignVals.hasID()) {
2041
                        value = foreignVals.getIDNumber().toString();
2042
                    } else {
2043
                        // so that if the same vals is referenced multiple times, we can see it
2044
                        value = "@" + System.identityHashCode(foreignVals);
2045
                    }
2046
                } else
2047
                    value = String.valueOf(e.getValue());
2048
                return e.getKey() + "=" + value + className;
2049
            }
2050
        });
2051
        result += "}";
2052
        return result;
2053
    }
2054
 
2055
    /**
2056
     * Return a graphical representation (akin to the result of a query) of the tree rooted at
2057
     * <code>this</code>.
2058
     *
2059
     * @return a string representing the rows pointing to this.
2060
     * @see SQLRowValuesCluster#printTree(SQLRowValues, int)
2061
     */
2062
    public final String printTree() {
2063
        return this.getGraph().printTree(this, 16);
2064
    }
2065
 
2066
    /**
2067
     * Return the list of all nodes and their links.
2068
     *
2069
     * @return a string representing the graph of this.
2070
     */
2071
    public final String printGraph() {
2072
        return this.getGraph().printNodes();
2073
    }
2074
 
2075
    @Override
2076
    public boolean equals(Object obj) {
93 ilm 2077
        if (obj == this)
2078
            return true;
17 ilm 2079
        if (obj instanceof SQLRowValues) {
93 ilm 2080
            return this.equalsGraph((SQLRowValues) obj);
17 ilm 2081
        } else
2082
            return false;
2083
    }
2084
 
2085
    @Override
2086
    public int hashCode() {
93 ilm 2087
        final int prime = 31;
2088
        int result = 1;
2089
        result = prime * result + this.getTable().hashCode();
2090
        // don't use SQLRowValues to avoid infinite loop
2091
        result = prime * result + this.getFields().hashCode();
2092
        result = prime * result + this.getGraphSize();
2093
        result = prime * result + this.foreigns.keySet().hashCode();
2094
        result = prime * result + this.referents.keySet().hashCode();
2095
        return result;
17 ilm 2096
    }
2097
 
2098
    /**
2099
     * Indicates whether some other graph is "equal to" this one.
2100
     *
2101
     * @param other another rowValues.
2102
     * @return <code>true</code> if both graph are equals.
80 ilm 2103
     * @see #getGraphFirstDifference(SQLRowValues)
17 ilm 2104
     */
2105
    public final boolean equalsGraph(final SQLRowValues other) {
80 ilm 2106
        return this.getGraphFirstDifference(other) == null;
17 ilm 2107
    }
2108
 
80 ilm 2109
    /**
93 ilm 2110
     * Return the first difference between this graph and another, ignoring order of fields.
80 ilm 2111
     *
2112
     * @param other another instance.
2113
     * @return the first difference, <code>null</code> if equals.
2114
     */
2115
    public final String getGraphFirstDifference(final SQLRowValues other) {
83 ilm 2116
        return this.getGraphFirstDifference(other, false);
80 ilm 2117
    }
2118
 
83 ilm 2119
    /**
93 ilm 2120
     * Return the first difference between this graph and another. Most of the time fields orders
83 ilm 2121
     * need not to be used, since when inserting they don't matter (which isn't true of the
2122
     * referents). But they can matter if e.g. this is used to construct a query.
2123
     *
2124
     * @param other another instance.
93 ilm 2125
     * @param useOrder <code>true</code> to also compare the order of fields.
83 ilm 2126
     * @return the first difference, <code>null</code> if equals.
2127
     */
93 ilm 2128
    public final String getGraphFirstDifference(final SQLRowValues other, final boolean useOrder) {
2129
        if (this == other)
2130
            return null;
132 ilm 2131
        return this.getGraph().getFirstDifference(this, other, useOrder, useOrder, true).getFirstDifference();
83 ilm 2132
    }
2133
 
93 ilm 2134
    public final boolean equalsJustThis(final SQLRowValues o) {
2135
        // don't compare the order of fields, since inserting doesn't change with it
2136
        return this.equalsJustThis(o, false);
17 ilm 2137
    }
2138
 
93 ilm 2139
    /**
2140
     * Whether this equals the passed instance without following linked rows. This method use
2141
     * {@link ForeignCopyMode#COPY_ID_OR_RM}, so that a row having a foreign ID and a row having a
2142
     * foreign row with the same ID are equal.
2143
     *
2144
     * @param o another instance.
2145
     * @param useFieldsOrder <code>true</code> if the order of {@link #getFields()} is to be
2146
     *        checked.
2147
     * @return <code>true</code> if both rows have the same {@link #getFields() fields} defined, and
2148
     *         {@link #getAllValues(ForeignCopyMode) all values} of this are equal to all values of
2149
     *         <code>o</code>.
2150
     * @see #equalsGraph(SQLRowValues)
2151
     */
2152
    public final boolean equalsJustThis(final SQLRowValues o, final boolean useFieldsOrder) {
2153
        return this.equalsJustThis(o, useFieldsOrder, true);
2154
    }
2155
 
2156
    final boolean equalsJustThis(final SQLRowValues o, final boolean useFieldsOrder, final boolean useForeignID) {
132 ilm 2157
        return this.equalsJustThis(o, useFieldsOrder, useForeignID, true);
2158
    }
2159
 
2160
    final boolean equalsJustThis(final SQLRowValues o, final boolean useFieldsOrder, final boolean useForeignID, final boolean usePK) {
93 ilm 2161
        if (this == o)
2162
            return true;
2163
        if (!this.getTable().equals(o.getTable()))
2164
            return false;
2165
        // first compare keySet as ForeignCopyMode can remove entries
2166
        if (useFieldsOrder) {
2167
            if (!CompareUtils.equalsUsingIterator(this.values.keySet(), o.values.keySet()))
2168
                return false;
2169
        } else {
2170
            if (!this.values.keySet().equals(o.values.keySet()))
2171
                return false;
2172
        }
2173
        // fields are already checked so if IDs are not wanted, just omit foreign rows
2174
        final ForeignCopyMode copyMode = useForeignID ? ForeignCopyMode.COPY_ID_OR_RM : ForeignCopyMode.NO_COPY;
132 ilm 2175
        final Map<String, Object> thisVals = this.getAllValues(copyMode, !usePK);
2176
        final Map<String, Object> oVals = o.getAllValues(copyMode, !usePK);
2177
        if (!usePK) {
2178
            final List<String> pk = this.getTable().getPKsNames();
2179
            thisVals.keySet().removeAll(pk);
2180
            oVals.keySet().removeAll(pk);
2181
        }
93 ilm 2182
        // LinkedHashMap.equals() does not compare the order of entries
2183
        return thisVals.equals(oVals);
2184
    }
2185
 
17 ilm 2186
    // *** static
2187
 
2188
    static private Tuple2<PreparedStatement, List<String>> createInsertStatement(final SQLTable table, Map<String, Object> values) throws SQLException {
2189
        final Tuple2<List<String>, List<Object>> l = CollectionUtils.mapToLists(values);
2190
        final List<String> fieldsNames = l.get0();
2191
        final List<Object> vals = l.get1();
2192
 
2193
        addMetadata(fieldsNames, vals, table.getCreationUserField(), getUser());
2194
        addMetadata(fieldsNames, vals, table.getCreationDateField(), new Timestamp(System.currentTimeMillis()));
2195
 
2196
        return createStatement(table, fieldsNames, vals, true);
2197
    }
2198
 
2199
    static private Tuple2<PreparedStatement, List<String>> createUpdateStatement(SQLTable table, Map<String, Object> values, int id) throws SQLException {
2200
        final Tuple2<List<String>, List<Object>> l = CollectionUtils.mapToLists(values);
2201
        final List<String> fieldsNames = l.get0();
2202
        final List<Object> vals = l.get1();
2203
 
2204
        vals.add(new Integer(id));
2205
        return createStatement(table, fieldsNames, vals, false);
2206
    }
2207
 
2208
    static private void addMetadata(List<String> fieldsNames, List<Object> values, SQLField field, Object fieldValue) throws SQLException {
2209
        if (field != null) {
2210
            // TODO updateVerbatim to force a value
2211
            final int index = fieldsNames.indexOf(field.getName());
2212
            if (index < 0) {
2213
                // ajout au dbt car le where du UPDATE a besoin de l'ID en dernier
2214
                fieldsNames.add(0, field.getName());
2215
                values.add(0, fieldValue);
2216
            } else {
2217
                values.set(index, fieldValue);
2218
            }
2219
        }
2220
    }
2221
 
2222
    static private Object getUser() {
2223
        final int userID = UserManager.getUserID();
2224
        return userID < SQLRow.MIN_VALID_ID ? SQL_DEFAULT : userID;
2225
    }
2226
 
2227
    /**
2228
     * Create a prepared statement.
2229
     *
2230
     * @param table the table to change.
2231
     * @param fieldsNames the columns names of <code>table</code>.
2232
     * @param values their values.
2233
     * @param insert whether to insert or update.
2234
     * @return the new statement and its columns.
2235
     * @throws SQLException if an error occurs.
2236
     */
2237
    static private Tuple2<PreparedStatement, List<String>> createStatement(SQLTable table, List<String> fieldsNames, List<Object> values, boolean insert) throws SQLException {
2238
        addMetadata(fieldsNames, values, table.getModifUserField(), getUser());
2239
        addMetadata(fieldsNames, values, table.getModifDateField(), new Timestamp(System.currentTimeMillis()));
2240
 
2241
        final PreparedStatement pStmt;
2242
        final String tableQuoted = table.getSQLName().quote();
2243
        String req = (insert ? "INSERT INTO " : "UPDATE ") + tableQuoted + " ";
2244
        if (insert) {
2245
            assert fieldsNames.size() == values.size();
2246
            // remove DEFAULT since they are useless and prevent us from using
2247
            // INSERT INTO "TABLEAU_ELECTRIQUE" ("ID_OBSERVATION", ...) select DEFAULT, ?,
2248
            // MAX("ORDRE") + 1 FROM "TABLEAU_ELECTRIQUE"
2249
            for (int i = values.size() - 1; i >= 0; i--) {
2250
                if (values.get(i) == SQL_DEFAULT) {
2251
                    fieldsNames.remove(i);
2252
                    values.remove(i);
2253
                }
2254
            }
2255
            assert fieldsNames.size() == values.size();
2256
 
2257
            // ajout de l'ordre
2258
            final SQLField order = table.getOrderField();
2259
            final boolean selectOrder;
2260
            if (order != null && !fieldsNames.contains(order.getName())) {
2261
                // si l'ordre n'est pas spécifié, ajout à la fin
2262
                fieldsNames.add(order.getName());
2263
                selectOrder = true;
2264
            } else {
2265
                selectOrder = false;
2266
            }
2267
 
2268
            if (fieldsNames.size() == 0 && table.getServer().getSQLSystem() != SQLSystem.MYSQL) {
2269
                // "LOCAL" () VALUES () is a syntax error on PG
2270
                req += "DEFAULT VALUES";
2271
            } else {
2272
                req += "(" + CollectionUtils.join(fieldsNames, ", ", new ITransformer<String, String>() {
2273
                    public String transformChecked(String input) {
2274
                        return SQLBase.quoteIdentifier(input);
2275
                    }
2276
                }) + ")";
2277
                // no DEFAULT thus only ?
2278
                final String questionMarks = CollectionUtils.join(Collections.nCopies(values.size(), "?"), ", ");
2279
                if (selectOrder) {
2280
                    // needed since VALUES ( (select MAX("ORDRE") from "LOCAL") ) on MySQL yield
2281
                    // "You can't specify target table 'LOCAL' for update in FROM clause"
2282
                    req += " select ";
2283
                    req += questionMarks;
2284
                    if (values.size() > 0)
2285
                        req += ", ";
19 ilm 2286
                    // COALESCE for empty tables, MIN_ORDER + 1 since MIN_ORDER cannot be moved
2287
                    req += "COALESCE(MAX(" + SQLBase.quoteIdentifier(order.getName()) + "), " + ReOrder.MIN_ORDER + ") + 1 FROM " + tableQuoted;
17 ilm 2288
                } else {
2289
                    req += " VALUES (";
2290
                    req += questionMarks;
2291
                    req += ")";
2292
                }
2293
            }
2294
            pStmt = createInsertStatement(req, table);
2295
        } else {
2296
            // ID at the end
2297
            assert fieldsNames.size() == values.size() - 1;
2298
            final List<String> fieldAndValues = new ArrayList<String>(fieldsNames.size());
2299
            final ListIterator<String> iter = fieldsNames.listIterator();
2300
            while (iter.hasNext()) {
2301
                final String fieldName = iter.next();
2302
                final SQLField field = table.getField(fieldName);
2303
                final Object value = values.get(iter.previousIndex());
2304
                // postgresql doesn't support prefixing fields with their tables in an update
2305
                fieldAndValues.add(SQLBase.quoteIdentifier(field.getName()) + "= " + getFieldValue(value));
2306
            }
2307
 
2308
            req += "SET " + CollectionUtils.join(fieldAndValues, ", ");
2309
            req += " WHERE " + table.getKey().getFieldRef() + "= ?";
2310
            final Connection c = table.getBase().getDataSource().getConnection();
2311
            pStmt = c.prepareStatement(req);
2312
        }
2313
        // set fields values
2314
        int i = 0;
2315
        for (final Object value : values) {
2316
            // nothing to set if there's no corresponding '?'
2317
            if (value != SQL_DEFAULT) {
2318
                final Object toIns;
2319
                if (value instanceof SQLRowValues) {
2320
                    // TODO if we already point to some row, archive it
25 ilm 2321
                    toIns = ((SQLRowValues) value).insert().getIDNumber();
17 ilm 2322
                } else
2323
                    toIns = value;
2324
                // sql index start at 1
142 ilm 2325
                pStmt.setObject(i + 1, toIns);
17 ilm 2326
                i++;
2327
            }
2328
        }
2329
        return Tuple2.create(pStmt, fieldsNames);
2330
    }
2331
 
2332
    private static String getFieldValue(final Object value) {
2333
        return value == SQL_DEFAULT ? "DEFAULT" : "?";
2334
    }
2335
 
25 ilm 2336
    @Override
2337
    public SQLTableModifiedListener createTableListener(SQLDataListener l) {
17 ilm 2338
        return new SQLTableListenerData<SQLRowValues>(this, l);
2339
    }
2340
 
2341
    // *** static
2342
 
2343
    /**
2344
     * Create an insert statement which can provide the inserted ID.
2345
     *
2346
     * @param req the INSERT sql.
2347
     * @param table the table where the row will be inserted.
2348
     * @return a new <code>PreparedStatement</code> object, containing the pre-compiled SQL
2349
     *         statement, that will have the capability of returning the primary key.
2350
     * @throws SQLException if a database access error occurs.
2351
     * @see #insert(PreparedStatement, SQLTable)
2352
     */
2353
    static public final PreparedStatement createInsertStatement(String req, final SQLTable table) throws SQLException {
2354
        final boolean rowable = table.isRowable();
2355
        final boolean isPG = table.getServer().getSQLSystem() == SQLSystem.POSTGRESQL;
2356
        if (rowable && isPG)
2357
            req += " RETURNING " + SQLBase.quoteIdentifier(table.getKey().getName());
2358
        final Connection c = table.getDBSystemRoot().getDataSource().getConnection();
2359
        final int returnGenK = rowable && !isPG && c.getMetaData().supportsGetGeneratedKeys() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS;
2360
        return c.prepareStatement(req, returnGenK);
2361
    }
2362
 
2363
    /**
2364
     * Execute the passed INSERT statement and return the ID of the new row.
2365
     *
2366
     * @param pStmt an INSERT statement (should have been obtained using
2367
     *        {@link #createInsertStatement(String, SQLTable)}).
2368
     * @param table the table where the row will be inserted.
2369
     * @return the new ID.
2370
     * @throws SQLException if the insertion fails.
2371
     */
2372
    static public final Number insert(final PreparedStatement pStmt, final SQLTable table) throws SQLException {
73 ilm 2373
        final long timeMs = System.currentTimeMillis();
2374
 
2375
        final long time = System.nanoTime();
17 ilm 2376
        pStmt.execute();
73 ilm 2377
        final long afterExecute = System.nanoTime();
2378
 
17 ilm 2379
        final Number newID;
2380
        if (table.isRowable()) {
2381
            final ResultSet rs;
2382
            if (table.getServer().getSQLSystem() == SQLSystem.POSTGRESQL) {
2383
                // uses RETURNING
2384
                rs = pStmt.getResultSet();
2385
            } else {
2386
                rs = pStmt.getGeneratedKeys();
2387
            }
2388
            try {
2389
                if (rs.next()) {
2390
                    newID = (Number) rs.getObject(1);
2391
                } else
2392
                    throw new IllegalStateException("no keys have been autogenerated for the successfully executed statement :" + pStmt);
2393
            } catch (SQLException exn) {
2394
                throw new IllegalStateException("can't get autogenerated keys for the successfully executed statement :" + pStmt);
2395
            }
2396
        } else {
2397
            newID = null;
2398
        }
73 ilm 2399
        final long afterHandle = System.nanoTime();
2400
        SQLRequestLog.log(pStmt, "rowValues.insert()", timeMs, time, afterExecute, afterExecute, afterExecute, afterHandle, System.nanoTime());
2401
 
17 ilm 2402
        return newID;
2403
    }
2404
 
2405
    /**
2406
     * Insert rows in the passed table.
2407
     *
2408
     * @param t a table, eg /LOCAL/.
2409
     * @param sql the sql specifying the data to be inserted, eg ("DESIGNATION") VALUES('A'), ('B').
2410
     * @return the inserted IDs, or <code>null</code> if <code>t</code> is not
2411
     *         {@link SQLTable#isRowable() rowable}.
2412
     * @throws SQLException if an error occurs while inserting.
2413
     */
2414
    @SuppressWarnings("unchecked")
2415
    public static final List<Number> insertIDs(final SQLTable t, final String sql) throws SQLException {
80 ilm 2416
        final boolean rowable = t.isRowable();
2417
        final Insertion<?> res = insert(t, sql, rowable ? ReturnMode.FIRST_FIELD : ReturnMode.NO_FIELDS);
2418
        if (rowable)
17 ilm 2419
            return ((Insertion<Number>) res).getRows();
2420
        else
2421
            return null;
2422
    }
2423
 
2424
    /**
2425
     * Insert rows in the passed table.
2426
     *
2427
     * @param t a table, eg /LOCAL/.
2428
     * @param sql the sql specifying the data to be inserted, eg ("DESIGNATION") VALUES('A'), ('B').
2429
     * @return an object to always know the insertion count and possibly the inserted primary keys.
2430
     * @throws SQLException if an error occurs while inserting.
2431
     */
2432
    @SuppressWarnings("unchecked")
142 ilm 2433
    public static final Insertion<List<Object>> insert(final SQLTable t, final String sql) throws SQLException {
2434
        return (Insertion<List<Object>>) insert(t, sql, ReturnMode.ALL_FIELDS);
17 ilm 2435
    }
2436
 
2437
    /**
2438
     * Insert rows in the passed table. Should be faster than other insert methods since it doesn't
2439
     * fetch primary keys.
2440
     *
2441
     * @param t a table, eg /LOCAL/.
2442
     * @param sql the sql specifying the data to be inserted, eg ("DESIGNATION") VALUES('A'), ('B').
2443
     * @return the insertion count.
2444
     * @throws SQLException if an error occurs while inserting.
2445
     */
2446
    public static final int insertCount(final SQLTable t, final String sql) throws SQLException {
80 ilm 2447
        return insert(t, sql, ReturnMode.NO_FIELDS).getCount();
17 ilm 2448
    }
2449
 
2450
    // if scalar is null primary keys aren't fetched
80 ilm 2451
    private static final Insertion<?> insert(final SQLTable t, final String sql, final ReturnMode mode) throws SQLException {
2452
        return new Inserter(t).insert(sql, mode, true);
17 ilm 2453
    }
2454
 
2455
    /**
2456
     * Insert rows in the passed table.
2457
     *
2458
     * @param t a table, eg /LOCAL/.
2459
     * @param sql the sql specifying the data to be inserted, eg ("DESIGNATION") VALUES('A'), ('B').
2460
     * @return the inserted rows (with no values, ie a call to a getter will trigger a db access),
2461
     *         or <code>null</code> if <code>t</code> is not {@link SQLTable#isRowable() rowable}.
2462
     * @throws SQLException if an error occurs while inserting.
2463
     */
2464
    public static final List<SQLRow> insertRows(final SQLTable t, final String sql) throws SQLException {
2465
        final List<Number> ids = insertIDs(t, sql);
2466
        if (ids == null)
2467
            return null;
2468
        final List<SQLRow> res = new ArrayList<SQLRow>(ids.size());
2469
        for (final Number id : ids)
2470
            res.add(new SQLRow(t, id.intValue()));
2471
        return res;
2472
    }
2473
 
19 ilm 2474
    // MAYBE add insertFromSelect(SQLTable, SQLSelect) if aliases are kept in SQLSelect (so that we
2475
    // can map arbitray expressions to fields in the destination table)
2476
    public static final int insertFromTable(final SQLTable dest, final SQLTable src) throws SQLException {
2477
        return insertFromTable(dest, src, src.getChildrenNames());
2478
    }
2479
 
17 ilm 2480
    /**
19 ilm 2481
     * Copy all rows from <code>src</code> to <code>dest</code>.
2482
     *
2483
     * @param dest the table where rows will be inserted.
2484
     * @param src the table where rows will be selected.
2485
     * @param fieldsNames the fields to use.
2486
     * @return the insertion count.
2487
     * @throws SQLException if an error occurs while inserting.
2488
     */
2489
    public static final int insertFromTable(final SQLTable dest, final SQLTable src, final Set<String> fieldsNames) throws SQLException {
2490
        if (dest.getDBSystemRoot() != src.getDBSystemRoot())
2491
            throw new IllegalArgumentException("Tables are not on the same system root : " + dest.getSQLName() + " / " + src.getSQLName());
2492
        if (!dest.getChildrenNames().containsAll(fieldsNames))
2493
            throw new IllegalArgumentException("Destination table " + dest.getSQLName() + " doesn't contain all fields of the source " + src + " : " + fieldsNames);
2494
 
2495
        final List<SQLField> fields = new ArrayList<SQLField>(fieldsNames.size());
2496
        for (final String fName : fieldsNames)
2497
            fields.add(src.getField(fName));
67 ilm 2498
        final SQLSelect sel = new SQLSelect(true);
19 ilm 2499
        sel.addAllSelect(fields);
2500
        final String colNames = "(" + CollectionUtils.join(fields, ",", new ITransformer<SQLField, String>() {
2501
            @Override
2502
            public String transformChecked(SQLField input) {
2503
                return SQLBase.quoteIdentifier(input.getName());
2504
            }
2505
        }) + ") ";
2506
        return insertCount(dest, colNames + sel.asString());
2507
    }
2508
 
2509
    /**
17 ilm 2510
     * Trim a collection of SQLRowValues.
2511
     *
2512
     * @param graphs the rowValues to trim.
2513
     * @return a copy of <code>graphs</code> without any linked SQLRowValues.
2514
     */
2515
    public static final List<SQLRowValues> trim(final Collection<SQLRowValues> graphs) {
2516
        final List<SQLRowValues> res = new ArrayList<SQLRowValues>(graphs.size());
2517
        for (final SQLRowValues r : graphs)
2518
            res.add(trim(r));
2519
        return res;
2520
    }
2521
 
2522
    public static final SQLRowValues trim(final SQLRowValues r) {
2523
        return new SQLRowValues(r, ForeignCopyMode.COPY_ID_OR_RM);
2524
    }
2525
}