OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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