OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 151 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 151 Rev 182
1
/*
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 * 
3
 * 
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
5
 * 
5
 * 
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
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
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
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.
9
 * language governing permissions and limitations under the License.
10
 * 
10
 * 
11
 * When distributing the software, include this License Header Notice in each file.
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
12
 */
13
 
13
 
14
 package org.openconcerto.sql.model;
14
 package org.openconcerto.sql.model;
15
 
15
 
16
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
16
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
17
import org.openconcerto.sql.model.SQLRowValues.FillMode;
17
import org.openconcerto.sql.model.SQLRowValues.FillMode;
18
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
18
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
19
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeEvent;
19
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeEvent;
20
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeListener;
20
import org.openconcerto.sql.model.SQLRowValues.ReferentChangeListener;
21
import org.openconcerto.sql.model.graph.Link.Direction;
21
import org.openconcerto.sql.model.graph.Link.Direction;
22
import org.openconcerto.sql.model.graph.Path;
22
import org.openconcerto.sql.model.graph.Path;
23
import org.openconcerto.sql.model.graph.Step;
23
import org.openconcerto.sql.model.graph.Step;
24
import org.openconcerto.sql.utils.SQLUtils;
24
import org.openconcerto.sql.utils.SQLUtils;
25
import org.openconcerto.utils.CollectionMap2.Mode;
25
import org.openconcerto.utils.CollectionMap2.Mode;
26
import org.openconcerto.utils.CollectionUtils;
26
import org.openconcerto.utils.CollectionUtils;
27
import org.openconcerto.utils.CompareUtils;
27
import org.openconcerto.utils.CompareUtils;
28
import org.openconcerto.utils.Matrix;
28
import org.openconcerto.utils.Matrix;
29
import org.openconcerto.utils.RecursionType;
29
import org.openconcerto.utils.RecursionType;
30
import org.openconcerto.utils.SetMap;
30
import org.openconcerto.utils.SetMap;
31
import org.openconcerto.utils.StringUtils;
31
import org.openconcerto.utils.StringUtils;
32
import org.openconcerto.utils.StringUtils.Side;
32
import org.openconcerto.utils.StringUtils.Side;
33
import org.openconcerto.utils.cc.Closure;
33
import org.openconcerto.utils.cc.Closure;
34
import org.openconcerto.utils.cc.IClosure;
34
import org.openconcerto.utils.cc.IClosure;
35
import org.openconcerto.utils.cc.ITransformer;
35
import org.openconcerto.utils.cc.ITransformer;
36
import org.openconcerto.utils.cc.IdentityHashSet;
36
import org.openconcerto.utils.cc.IdentityHashSet;
37
import org.openconcerto.utils.cc.IdentitySet;
37
import org.openconcerto.utils.cc.IdentitySet;
38
 
38
 
39
import java.sql.SQLException;
39
import java.sql.SQLException;
40
import java.util.ArrayList;
40
import java.util.ArrayList;
41
import java.util.Collection;
41
import java.util.Collection;
42
import java.util.Collections;
42
import java.util.Collections;
43
import java.util.Comparator;
43
import java.util.Comparator;
44
import java.util.EventObject;
44
import java.util.EventObject;
45
import java.util.HashSet;
45
import java.util.HashSet;
46
import java.util.IdentityHashMap;
46
import java.util.IdentityHashMap;
47
import java.util.Iterator;
47
import java.util.Iterator;
48
import java.util.LinkedHashMap;
48
import java.util.LinkedHashMap;
49
import java.util.List;
49
import java.util.List;
50
import java.util.Map;
50
import java.util.Map;
51
import java.util.Map.Entry;
51
import java.util.Map.Entry;
52
import java.util.Set;
52
import java.util.Set;
53
import java.util.concurrent.atomic.AtomicInteger;
53
import java.util.concurrent.atomic.AtomicInteger;
54
 
54
 
55
import net.jcip.annotations.Immutable;
55
import net.jcip.annotations.Immutable;
56
 
56
 
57
/**
57
/**
58
 * A set of linked SQLRowValues.
58
 * A set of linked SQLRowValues.
59
 * 
59
 * 
60
 * @author Sylvain
60
 * @author Sylvain
61
 */
61
 */
62
public class SQLRowValuesCluster {
62
public class SQLRowValuesCluster {
63
    private static final Comparator<SQLField> FIELD_COMPARATOR = new Comparator<SQLField>() {
63
    private static final Comparator<SQLField> FIELD_COMPARATOR = new Comparator<SQLField>() {
64
        @Override
64
        @Override
65
        public int compare(SQLField o1, SQLField o2) {
65
        public int compare(SQLField o1, SQLField o2) {
66
            return o1.getSQLName().quote().compareTo(o2.getSQLName().quote());
66
            return o1.getSQLName().quote().compareTo(o2.getSQLName().quote());
67
        }
67
        }
68
    };
68
    };
69
 
69
 
70
    /**
70
    /**
71
     * The list of links in the order they've been set. This allows deterministic and predictable
71
     * The list of links in the order they've been set. This allows deterministic and predictable
72
     * insertion order. E.g. for
72
     * insertion order. E.g. for
73
     * 
73
     * 
74
     * <pre>
74
     * <pre>
75
     * r2.put(f, r1);
75
     * r2.put(f, r1);
76
     * r3.put(f2, r2);
76
     * r3.put(f2, r2);
77
     * r4.put(f2, r2);
77
     * r4.put(f2, r2);
78
     * </pre>
78
     * </pre>
79
     * 
79
     * 
80
     * the links will be :
80
     * the links will be :
81
     * 
81
     * 
82
     * <pre>
82
     * <pre>
83
     * r1
83
     * r1
84
     * r2 f r1
84
     * r2 f r1
85
     * r3 f2 r2
85
     * r3 f2 r2
86
     * r4 f2 r2
86
     * r4 f2 r2
87
     * </pre>
87
     * </pre>
88
     */
88
     */
89
    private final List<Link> links;
89
    private final List<Link> links;
90
    private final IdentitySet<SQLRowValues> items;
90
    private final IdentitySet<SQLRowValues> items;
91
    // { vals -> listener on vals' graph }
91
    // { vals -> listener on vals' graph }
92
    private Map<SQLRowValues, List<ValueChangeListener>> listeners;
92
    private Map<SQLRowValues, List<ValueChangeListener>> listeners;
93
 
93
 
94
    private boolean frozen = false;
94
    private boolean frozen = false;
95
 
95
 
96
    private SQLRowValuesCluster() {
96
    private SQLRowValuesCluster() {
97
        this.links = new ArrayList<Link>();
97
        this.links = new ArrayList<Link>();
98
        // SQLRowValues equals() depends on their values, but we must tell apart each reference
98
        // SQLRowValues equals() depends on their values, but we must tell apart each reference
99
        this.items = new IdentityHashSet<SQLRowValues>();
99
        this.items = new IdentityHashSet<SQLRowValues>();
100
        this.listeners = null;
100
        this.listeners = null;
101
    }
101
    }
102
 
102
 
103
    SQLRowValuesCluster(SQLRowValues vals) {
103
    SQLRowValuesCluster(SQLRowValues vals) {
104
        this();
104
        this();
105
        addVals(-1, vals);
105
        addVals(-1, vals);
106
    }
106
    }
107
 
107
 
108
    // add a lonely node to this
108
    // add a lonely node to this
109
    private final void addVals(final int index, final SQLRowValues vals) {
109
    private final void addVals(final int index, final SQLRowValues vals) {
110
        assert vals.getGraph(false) == null;
110
        assert vals.getGraph(false) == null;
111
        if (index < 0)
111
        if (index < 0)
112
            this.links.add(new Link(vals));
112
            this.links.add(new Link(vals));
113
        else
113
        else
114
            this.links.add(index, new Link(vals));
114
            this.links.add(index, new Link(vals));
115
        this.items.add(vals);
115
        this.items.add(vals);
116
    }
116
    }
117
 
117
 
118
    private final SQLRowValues getHead() {
118
    private final SQLRowValues getHead() {
119
        return this.links.get(0).getSrc();
119
        return this.links.get(0).getSrc();
120
    }
120
    }
121
 
121
 
122
    private final DBSystemRoot getSystemRoot() {
122
    private final DBSystemRoot getSystemRoot() {
123
        return this.getHead().getTable().getDBSystemRoot();
123
        return this.getHead().getTable().getDBSystemRoot();
124
    }
124
    }
125
 
125
 
126
    public final int getLinksCount() {
126
    public final int getLinksCount() {
127
        return this.links.size();
127
        return this.links.size();
128
    }
128
    }
129
 
129
 
130
    /**
130
    /**
131
     * All the rowValues in this cluster.
131
     * All the rowValues in this cluster.
132
     * 
132
     * 
133
     * @return the set of SQLRowValues.
133
     * @return the set of SQLRowValues.
134
     */
134
     */
135
    public final Set<SQLRowValues> getItems() {
135
    public final Set<SQLRowValues> getItems() {
136
        return Collections.unmodifiableSet(this.items);
136
        return Collections.unmodifiableSet(this.items);
137
    }
137
    }
138
 
138
 
139
    /**
139
    /**
140
     * The number of items in this instance. NOTE: calling {@link SQLRowValues#getGraphSize()} is
140
     * The number of items in this instance. NOTE: calling {@link SQLRowValues#getGraphSize()} is
141
     * preferable since it might avoid an allocation.
141
     * preferable since it might avoid an allocation.
142
     * 
142
     * 
143
     * @return the number of items.
143
     * @return the number of items.
144
     */
144
     */
145
    public final int size() {
145
    public final int size() {
146
        return this.items.size();
146
        return this.items.size();
147
    }
147
    }
148
 
148
 
149
    public final boolean contains(SQLRowValues start) {
149
    public final boolean contains(SQLRowValues start) {
150
        return this.items.contains(start);
150
        return this.items.contains(start);
151
    }
151
    }
152
 
152
 
153
    private final void containsCheck(SQLRowValues vals) {
153
    private final void containsCheck(SQLRowValues vals) {
154
        if (!this.contains(vals))
154
        if (!this.contains(vals))
155
            throw new IllegalArgumentException(vals + " not in " + this);
155
            throw new IllegalArgumentException(vals + " not in " + this);
156
    }
156
    }
157
 
157
 
158
    // if true a single link path passed to followPath() will never yield more than one row
158
    // if true a single link path passed to followPath() will never yield more than one row
159
    final boolean hasOneRowPerPath() {
159
    final boolean hasOneRowPerPath() {
160
        for (final SQLRowValues item : this.getItems()) {
160
        for (final SQLRowValues item : this.getItems()) {
161
            for (final Entry<SQLField, Set<SQLRowValues>> e : item.getReferents().entrySet()) {
161
            for (final Entry<SQLField, Set<SQLRowValues>> e : item.getReferents().entrySet()) {
162
                if (e.getValue().size() > 1)
162
                if (e.getValue().size() > 1)
163
                    return false;
163
                    return false;
164
            }
164
            }
165
        }
165
        }
166
        return true;
166
        return true;
167
    }
167
    }
168
 
168
 
169
    public final Set<SQLTable> getTables() {
169
    public final Set<SQLTable> getTables() {
170
        final Set<SQLTable> res = new HashSet<SQLTable>();
170
        final Set<SQLTable> res = new HashSet<SQLTable>();
171
        for (final SQLRowValues v : this.items)
171
        for (final SQLRowValues v : this.items)
172
            res.add(v.getTable());
172
            res.add(v.getTable());
173
        return res;
173
        return res;
174
    }
174
    }
175
 
175
 
176
    public final boolean isFrozen() {
176
    public final boolean isFrozen() {
177
        return this.frozen;
177
        return this.frozen;
178
    }
178
    }
179
 
179
 
180
    /**
180
    /**
181
     * Freeze this graph so that no modification can be made. Once this method returns, this
181
     * Freeze this graph so that no modification can be made. Once this method returns, this
182
     * instance can be safely published (e.g. stored into a field that is properly guarded by a
182
     * instance can be safely published (e.g. stored into a field that is properly guarded by a
183
     * lock) to other threads without further synchronizations.
183
     * lock) to other threads without further synchronizations.
184
     * 
184
     * 
185
     * @return <code>true</code> if this call changed the frozen status.
185
     * @return <code>true</code> if this call changed the frozen status.
186
     */
186
     */
187
    public final boolean freeze() {
187
    public final boolean freeze() {
188
        if (this.frozen)
188
        if (this.frozen)
189
            return false;
189
            return false;
190
        this.frozen = true;
190
        this.frozen = true;
191
        return true;
191
        return true;
192
    }
192
    }
193
 
193
 
194
    void remove(SQLRowValues src, SQLField f, SQLRowValues dest) {
194
    void remove(SQLRowValues src, SQLField f, SQLRowValues dest) {
195
        assert dest != null;
195
        assert dest != null;
196
        assert src.getGraph() == this;
196
        assert src.getGraph() == this;
197
        assert src.getTable() == f.getTable();
197
        assert src.getTable() == f.getTable();
198
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
198
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
199
 
199
 
200
        final Link toRm = new Link(src, f, dest);
200
        final Link toRm = new Link(src, f, dest);
201
        this.links.remove(toRm);
201
        this.links.remove(toRm);
202
 
202
 
203
        // test if the removal of the link split the graph
203
        // test if the removal of the link split the graph
204
        final IdentitySet<SQLRowValues> reachable = this.getReachable(src);
204
        final IdentitySet<SQLRowValues> reachable = this.getReachable(src);
205
        if (reachable.size() < this.size()) {
205
        if (reachable.size() < this.size()) {
206
            final SQLRowValuesCluster newCluster = new SQLRowValuesCluster();
206
            final SQLRowValuesCluster newCluster = new SQLRowValuesCluster();
207
 
207
 
208
            // moves the links no longer in us into a new cluster
208
            // moves the links no longer in us into a new cluster
209
            final Iterator<Link> iter = this.links.iterator();
209
            final Iterator<Link> iter = this.links.iterator();
210
            while (iter.hasNext()) {
210
            while (iter.hasNext()) {
211
                final Link l = iter.next();
211
                final Link l = iter.next();
212
                // the graph is split in two, so every link is in one part
212
                // the graph is split in two, so every link is in one part
213
                assert l.getDest() == null || reachable.contains(l.getSrc()) == reachable.contains(l.getDest());
213
                assert l.getDest() == null || reachable.contains(l.getSrc()) == reachable.contains(l.getDest());
214
                // hence only test the source
214
                // hence only test the source
215
                if (!reachable.contains(l.getSrc())) {
215
                if (!reachable.contains(l.getSrc())) {
216
                    iter.remove();
216
                    iter.remove();
217
                    newCluster.links.add(l);
217
                    newCluster.links.add(l);
218
                }
218
                }
219
            }
219
            }
220
            // move unreachable items
220
            // move unreachable items
221
            assert newCluster.items.isEmpty();
221
            assert newCluster.items.isEmpty();
222
            final Iterator<SQLRowValues> itemIter = this.items.iterator();
222
            final Iterator<SQLRowValues> itemIter = this.items.iterator();
223
            while (itemIter.hasNext()) {
223
            while (itemIter.hasNext()) {
224
                final SQLRowValues key = itemIter.next();
224
                final SQLRowValues key = itemIter.next();
225
                if (!reachable.contains(key)) {
225
                if (!reachable.contains(key)) {
226
                    itemIter.remove();
226
                    itemIter.remove();
227
                    newCluster.items.add(key);
227
                    newCluster.items.add(key);
228
                    if (this.listeners != null && this.listeners.containsKey(key))
228
                    if (this.listeners != null && this.listeners.containsKey(key))
229
                        newCluster.getListeners().put(key, this.listeners.remove(key));
229
                        newCluster.getListeners().put(key, this.listeners.remove(key));
230
                }
230
                }
231
            }
231
            }
232
            // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6612102
232
            // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6612102
233
            // resolved in 7b25 : iterator.remove() might decrement the size twice : e.g. size is 0
233
            // resolved in 7b25 : iterator.remove() might decrement the size twice : e.g. size is 0
234
            // and thus isEmpty() is true, while there's still elements in this.items
234
            // and thus isEmpty() is true, while there's still elements in this.items
235
            assert !this.items.isEmpty() : "Empty items while removing " + f + " -> " + dest + " from " + src;
235
            assert !this.items.isEmpty() : "Empty items while removing " + f + " -> " + dest + " from " + src;
236
            assert !newCluster.items.isEmpty() : "New graph is empty while removing " + f + " -> " + dest + " from " + src;
236
            assert !newCluster.items.isEmpty() : "New graph is empty while removing " + f + " -> " + dest + " from " + src;
237
            assert !CollectionUtils.containsAny(this.items, newCluster.items) : "Shared items while removing " + f + " -> " + dest + " from " + src;
237
            assert !CollectionUtils.containsAny(this.items, newCluster.items) : "Shared items while removing " + f + " -> " + dest + " from " + src;
238
 
238
 
239
            for (final SQLRowValues vals : newCluster.getItems())
239
            for (final SQLRowValues vals : newCluster.getItems())
240
                vals.setGraph(newCluster);
240
                vals.setGraph(newCluster);
241
        }
241
        }
242
    }
242
    }
243
 
243
 
244
    void add(SQLRowValues src, SQLField f, SQLRowValues dest) {
244
    void add(SQLRowValues src, SQLField f, SQLRowValues dest) {
245
        assert dest != null;
245
        assert dest != null;
246
        assert src.getTable() == f.getTable();
246
        assert src.getTable() == f.getTable();
247
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
247
        assert !this.isFrozen() : "Should already be checked by SQLRowValues";
248
 
248
 
249
        final boolean containsSrc = this.contains(src);
249
        final boolean containsSrc = this.contains(src);
250
        final boolean containsDest = this.contains(dest);
250
        final boolean containsDest = this.contains(dest);
251
        if (!containsSrc && !containsDest)
251
        if (!containsSrc && !containsDest)
252
            throw new IllegalArgumentException("Neither source nor destination are contained in this :\n" + src + "\n" + dest);
252
            throw new IllegalArgumentException("Neither source nor destination are contained in this :\n" + src + "\n" + dest);
253
 
253
 
254
        final Link toAdd = new Link(src, f, dest);
254
        final Link toAdd = new Link(src, f, dest);
255
        if (containsSrc && containsDest) {
255
        if (containsSrc && containsDest) {
256
            // both source and dest are in us
256
            // both source and dest are in us
257
            this.links.add(toAdd);
257
            this.links.add(toAdd);
258
        } else {
258
        } else {
259
            assert src.getGraph(false) != dest.getGraph(false);
259
            assert src.getGraph(false) != dest.getGraph(false);
260
            final SQLRowValues rowToAdd;
260
            final SQLRowValues rowToAdd;
261
            final int index;
261
            final int index;
262
            if (containsSrc) {
262
            if (containsSrc) {
263
                rowToAdd = dest;
263
                rowToAdd = dest;
264
                // merge the two graphs
264
                // merge the two graphs
265
                // add dest before src since it will be needed to store us (adding at the end would
265
                // add dest before src since it will be needed to store us (adding at the end would
266
                // work, but src would need to be inserted, then updated)
266
                // work, but src would need to be inserted, then updated)
267
                // add dest just before src, to keep the order of foreigns (needed for deepCopy()
267
                // add dest just before src, to keep the order of foreigns (needed for deepCopy()
268
                // and insertion order) (adding at the beginning would reverse the order of foreign
268
                // and insertion order) (adding at the beginning would reverse the order of foreign
269
                // rows to insert)
269
                // rows to insert)
270
                final int srcIndex = this.links.indexOf(new Link(src));
270
                final int srcIndex = this.links.indexOf(new Link(src));
271
                if (srcIndex < 0)
271
                if (srcIndex < 0)
272
                    throw new IllegalStateException("Source link not found for " + src);
272
                    throw new IllegalStateException("Source link not found for " + src);
273
                index = srcIndex;
273
                index = srcIndex;
274
            } else {
274
            } else {
275
                assert containsDest;
275
                assert containsDest;
276
                rowToAdd = src;
276
                rowToAdd = src;
277
                index = -1;
277
                index = -1;
278
            }
278
            }
279
            final SQLRowValuesCluster graphToAdd = rowToAdd.getGraph(false);
279
            final SQLRowValuesCluster graphToAdd = rowToAdd.getGraph(false);
280
 
280
 
281
            // to preserve memory a single node has no graph unless required
281
            // to preserve memory a single node has no graph unless required
282
            // this way rowToAdd never had to create a Cluster, it will use us.
282
            // this way rowToAdd never had to create a Cluster, it will use us.
283
            if (graphToAdd == null) {
283
            if (graphToAdd == null) {
284
                this.addVals(index, rowToAdd);
284
                this.addVals(index, rowToAdd);
285
                rowToAdd.setGraph(this);
285
                rowToAdd.setGraph(this);
286
            } else {
286
            } else {
287
                if (index < 0)
287
                if (index < 0)
288
                    this.links.addAll(graphToAdd.links);
288
                    this.links.addAll(graphToAdd.links);
289
                else
289
                else
290
                    this.links.addAll(index, graphToAdd.links);
290
                    this.links.addAll(index, graphToAdd.links);
291
                graphToAdd.links.clear();
291
                graphToAdd.links.clear();
292
                this.items.addAll(graphToAdd.items);
292
                this.items.addAll(graphToAdd.items);
293
                for (final SQLRowValues newlyAdded : graphToAdd.items) {
293
                for (final SQLRowValues newlyAdded : graphToAdd.items) {
294
                    newlyAdded.setGraph(this);
294
                    newlyAdded.setGraph(this);
295
                }
295
                }
296
                graphToAdd.items.clear();
296
                graphToAdd.items.clear();
297
                if (graphToAdd.listeners != null) {
297
                if (graphToAdd.listeners != null) {
298
                    this.getListeners().putAll(graphToAdd.listeners);
298
                    this.getListeners().putAll(graphToAdd.listeners);
299
                    graphToAdd.listeners = null;
299
                    graphToAdd.listeners = null;
300
                }
300
                }
301
            }
301
            }
302
            // To keep foreign links order, new links should be added after existing ones from src
302
            // To keep foreign links order, new links should be added after existing ones from src
303
            // (which are after srcIndex). Since they're merged in store(), just add at the end.
303
            // (which are after srcIndex). Since they're merged in store(), just add at the end.
304
            this.links.add(toAdd);
304
            this.links.add(toAdd);
305
        }
305
        }
306
        assert src.getGraph() == dest.getGraph();
306
        assert src.getGraph() == dest.getGraph();
307
    }
307
    }
308
 
308
 
309
    private IdentitySet<SQLRowValues> getReachable(final SQLRowValues from) {
309
    private IdentitySet<SQLRowValues> getReachable(final SQLRowValues from) {
310
        final IdentitySet<SQLRowValues> res = new IdentityHashSet<SQLRowValues>();
310
        final IdentitySet<SQLRowValues> res = new IdentityHashSet<SQLRowValues>();
311
        getReachableRec(from, res);
311
        getReachableRec(from, res);
312
        return res;
312
        return res;
313
    }
313
    }
314
 
314
 
315
    private void getReachableRec(final SQLRowValues from, final IdentitySet<SQLRowValues> acc) {
315
    private void getReachableRec(final SQLRowValues from, final IdentitySet<SQLRowValues> acc) {
316
        if (!acc.add(from))
316
        if (!acc.add(from))
317
            return;
317
            return;
318
 
318
 
319
        for (final SQLRowValues fVals : from.getForeigns().values()) {
319
        for (final SQLRowValues fVals : from.getForeigns().values()) {
320
            this.getReachableRec(fVals, acc);
320
            this.getReachableRec(fVals, acc);
321
        }
321
        }
322
        for (final SQLRowValues fVals : from.getReferentRows()) {
322
        for (final SQLRowValues fVals : from.getReferentRows()) {
323
            this.getReachableRec(fVals, acc);
323
            this.getReachableRec(fVals, acc);
324
        }
324
        }
325
    }
325
    }
326
 
326
 
327
    final SQLRowValues deepCopy(SQLRowValues v, final boolean freeze) {
327
    final SQLRowValues deepCopy(SQLRowValues v, final boolean freeze) {
328
        return deepCopy(freeze).get(v);
328
        return deepCopy(freeze).get(v);
329
    }
329
    }
330
 
330
 
331
    public final Map<SQLRowValues, SQLRowValues> deepCopy(final boolean freeze) {
331
    public final Map<SQLRowValues, SQLRowValues> deepCopy(final boolean freeze) {
332
        return this.copy(null, false, freeze);
332
        return this.copy(null, false, freeze);
333
    }
333
    }
334
 
334
 
335
    /**
335
    /**
336
     * Copy a subset of this graph. For each link to copy, if the destination was copied then the
336
     * Copy a subset of this graph. For each link to copy, if the destination was copied then the
337
     * new row will point to it, otherwise the new row will point to the original row. For example,
337
     * new row will point to it, otherwise the new row will point to the original row. For example,
338
     * if
338
     * if
339
     * 
339
     * 
340
     * <pre>
340
     * <pre>
341
     * start is CONTAINER <-- *ITEM [F1, F2]* --> PRIVATE
341
     * start is CONTAINER <-- *ITEM [F1, F2]* --> PRIVATE
342
     * and graph is ITEM [F2, ID_PRIVATE]
342
     * and graph is ITEM [F2, ID_PRIVATE]
343
     * then after this method :
343
     * then after this method :
344
     *  CONTAINER <-- ITEM [F1, F2] --> PRIVATE
344
     *  CONTAINER <-- ITEM [F1, F2] --> PRIVATE
345
     *            \-- ITEM [F2]     --> PRIVATE
345
     *            \-- ITEM [F2]     --> PRIVATE
346
     * </pre>
346
     * </pre>
347
     * 
347
     * 
348
     * @param start where to start copying.
348
     * @param start where to start copying.
349
     * @param graph which rows and which fields to copy, not <code>null</code>.
349
     * @param graph which rows and which fields to copy, not <code>null</code>.
350
     * @return the new rows, indexed by the original rows in this instance.
350
     * @return the new rows, indexed by the original rows in this instance.
351
     */
351
     */
352
    public final Map<SQLRowValues, SQLRowValues> copy(final SQLRowValues start, final SQLRowValues graph) {
352
    public final Map<SQLRowValues, SQLRowValues> copy(final SQLRowValues start, final SQLRowValues graph) {
353
        return this.copy(computeToRetain(start, graph, true), true, false);
353
        return this.copy(computeToRetain(start, graph, true), true, false);
354
    }
354
    }
355
 
355
 
356
    /**
356
    /**
357
     * Copy rows of this graph.
357
     * Copy rows of this graph.
358
     * 
358
     * 
359
     * @param subset which rows and which fields to copy, <code>null</code> to copy all rows.
359
     * @param subset which rows and which fields to copy, <code>null</code> to copy all rows.
360
     * @param allowSameGraph used when copied rows point to rows which weren't copied,
360
     * @param allowSameGraph used when copied rows point to rows which weren't copied,
361
     *        <code>true</code> to point to the original rows (and thus linking the new rows into
361
     *        <code>true</code> to point to the original rows (and thus linking the new rows into
362
     *        the existing graph), <code>false</code> to flatten the link (like
362
     *        the existing graph), <code>false</code> to flatten the link (like
363
     *        {@link ForeignCopyMode#COPY_ID_OR_RM}).
363
     *        {@link ForeignCopyMode#COPY_ID_OR_RM}).
364
     * @param freeze <code>true</code> if the copied rows should be frozen.
364
     * @param freeze <code>true</code> if the copied rows should be frozen.
365
     * @return the new rows, indexed by the original rows in this instance.
365
     * @return the new rows, indexed by the original rows in this instance.
366
     */
366
     */
367
    private final Map<SQLRowValues, SQLRowValues> copy(final SetMap<SQLRowValues, String> subset, final boolean allowSameGraph, final boolean freeze) {
367
    private final Map<SQLRowValues, SQLRowValues> copy(final SetMap<SQLRowValues, String> subset, final boolean allowSameGraph, final boolean freeze) {
368
        assert !allowSameGraph || !freeze;
368
        assert !allowSameGraph || !freeze;
369
        // copy all rowValues of this graph
369
        // copy all rowValues of this graph
370
        final Map<SQLRowValues, SQLRowValues> noLinkCopy = new IdentityHashMap<SQLRowValues, SQLRowValues>();
370
        final Map<SQLRowValues, SQLRowValues> noLinkCopy = new IdentityHashMap<SQLRowValues, SQLRowValues>();
371
        // don't copy foreigns, but we want to preserve the order of all fields. This works because
371
        // don't copy foreigns, but we want to preserve the order of all fields. This works because
372
        // the second put() with the actual foreign row doesn't change the order.
372
        // the second put() with the actual foreign row doesn't change the order.
373
        final ForeignCopyMode copyMode = ForeignCopyMode.COPY_NULL;
373
        final ForeignCopyMode copyMode = ForeignCopyMode.COPY_NULL;
374
        for (final SQLRowValues n : this.getItems()) {
374
        for (final SQLRowValues n : this.getItems()) {
375
            final SQLRowValues copy;
375
            final SQLRowValues copy;
376
            if (subset == null || subset.containsKey(n)) {
376
            if (subset == null || subset.containsKey(n)) {
377
                // might as well use the minimum memory if the values won't change
377
                // might as well use the minimum memory if the values won't change
378
                if (freeze) {
378
                if (freeze) {
379
                    copy = new SQLRowValues(n.getTable(), n.size(), n.getForeignsSize(), n.getReferents().size());
379
                    copy = new SQLRowValues(n.getTable(), n.size(), n.getForeignsSize(), n.getReferents().size());
380
                    copy.setAll(n.getAllValues(copyMode));
380
                    copy.setAll(n.getAllValues(copyMode));
381
                } else {
381
                } else {
382
                    copy = new SQLRowValues(n, copyMode);
382
                    copy = new SQLRowValues(n, copyMode);
383
                    if (subset != null) {
383
                    if (subset != null) {
384
                        copy.retainAll(subset.get(n));
384
                        copy.retainAll(subset.get(n));
385
                    }
385
                    }
386
                }
386
                }
387
                noLinkCopy.put(n, copy);
387
                noLinkCopy.put(n, copy);
388
            }
388
            }
389
        }
389
        }
390
 
390
 
391
        // and link them together in order
391
        // and link them together in order
392
        final List<Link> iterableLinks = subset == null ? this.links : new ArrayList<Link>(this.links);
392
        final List<Link> iterableLinks = subset == null ? this.links : new ArrayList<Link>(this.links);
393
        for (final Link l : iterableLinks) {
393
        for (final Link l : iterableLinks) {
394
            if (l.getField() != null) {
394
            if (l.getField() != null) {
395
                final SQLRowValues sourceCopy = noLinkCopy.get(l.getSrc());
395
                final SQLRowValues sourceCopy = noLinkCopy.get(l.getSrc());
396
                if (subset == null || (sourceCopy != null && sourceCopy.contains(l.getField().getName()))) {
396
                if (subset == null || (sourceCopy != null && sourceCopy.contains(l.getField().getName()))) {
397
                    assert l.getDest() != null;
397
                    assert l.getDest() != null;
398
                    final SQLRowValues destCopy = noLinkCopy.get(l.getDest());
398
                    final SQLRowValues destCopy = noLinkCopy.get(l.getDest());
399
                    final Object dest;
399
                    final Object dest;
400
                    if (destCopy != null)
400
                    if (destCopy != null)
401
                        dest = destCopy;
401
                        dest = destCopy;
402
                    else if (allowSameGraph)
402
                    else if (allowSameGraph)
403
                        dest = l.getDest();
403
                        dest = l.getDest();
404
                    else if (l.getDest().hasID())
404
                    else if (l.getDest().hasID())
405
                        dest = l.getDest().getIDNumber();
405
                        dest = l.getDest().getIDNumber();
406
                    else
406
                    else
407
                        dest = null;
407
                        dest = null;
408
                    if (dest != null) {
408
                    if (dest != null) {
409
                        sourceCopy.put(l.getField().getName(), dest);
409
                        sourceCopy.put(l.getField().getName(), dest);
410
                    } else {
410
                    } else {
411
                        // ForeignCopyMode.COPY_ID_OR_RM like pruneWithoutCopy() (avoids
411
                        // ForeignCopyMode.COPY_ID_OR_RM like pruneWithoutCopy() (avoids
412
                        // leaving nulls)
412
                        // leaving nulls)
413
                        sourceCopy.remove(l.getField().getName());
413
                        sourceCopy.remove(l.getField().getName());
414
                    }
414
                    }
415
                }
415
                }
416
            } else {
416
            } else {
417
                assert subset != null || noLinkCopy.containsKey(l.getSrc());
417
                assert subset != null || noLinkCopy.containsKey(l.getSrc());
418
            }
418
            }
419
        }
419
        }
420
 
420
 
421
        final SQLRowValues res = noLinkCopy.values().iterator().next();
421
        final SQLRowValues res = noLinkCopy.values().iterator().next();
422
        // only force graph creation if needed
422
        // only force graph creation if needed
423
        if (freeze)
423
        if (freeze)
424
            res.getGraph().freeze();
424
            res.getGraph().freeze();
425
        assert res.isFrozen() == freeze;
425
        assert res.isFrozen() == freeze;
426
        assert allowSameGraph || res.getGraph() != this;
426
        assert allowSameGraph || res.getGraph() != this;
427
 
427
 
428
        return noLinkCopy;
428
        return noLinkCopy;
429
    }
429
    }
430
 
430
 
431
    public final StoreResult insert() throws SQLException {
431
    public final StoreResult insert() throws SQLException {
432
        return this.store(StoreMode.INSERT);
432
        return this.store(StoreMode.INSERT);
433
    }
433
    }
434
 
434
 
435
    public final StoreResult store(final StoreMode mode) throws SQLException {
435
    public final StoreResult store(final StoreMode mode) throws SQLException {
436
        return this.store(mode, null);
436
        return this.store(mode, null);
437
    }
437
    }
438
 
438
 
439
    // checkValidity false useful when we want to avoid loading the graph
439
    // checkValidity false useful when we want to avoid loading the graph
440
    public final StoreResult store(final StoreMode mode, final Boolean checkValidity) throws SQLException {
440
    public final StoreResult store(final StoreMode mode, final Boolean checkValidity) throws SQLException {
441
        return this.store(mode, null, null, checkValidity, true);
441
        return this.store(mode, null, null, checkValidity, true);
442
    }
442
    }
443
 
443
 
444
    /**
444
    /**
445
     * Store this graph into the DB.
445
     * Store this graph into the DB.
446
     * 
446
     * 
447
     * @param mode how to store.
447
     * @param mode how to store.
448
     * @param start when storing a subset, the start of <code>pruneGraph</code> in this, can be
448
     * @param start when storing a subset, the start of <code>pruneGraph</code> in this, can be
449
     *        <code>null</code>.
449
     *        <code>null</code>.
450
     * @param pruneGraph the maximum graph to store, can be <code>null</code>.
450
     * @param pruneGraph the maximum graph to store, can be <code>null</code>.
451
     * @param checkValidity whether to {@link SQLRowValues#getInvalid() checking validity} of rows,
451
     * @param checkValidity whether to {@link SQLRowValues#getInvalid() checking validity} of rows,
452
     *        see {@link SQLRowValues#setValidityChecked(SQLRowValues.ValidityCheck)}.
452
     *        see {@link SQLRowValues#setValidityChecked(SQLRowValues.ValidityCheck)}.
453
     * @param fireEvent <code>false</code> if stored rows shouldn't be fetched and
453
     * @param fireEvent <code>false</code> if stored rows shouldn't be fetched and
454
     *        {@link SQLTableEvent} should not be fired.
454
     *        {@link SQLTableEvent} should not be fired.
455
     * @return the store result.
455
     * @return the store result.
456
     * @throws SQLException if an exception occurs.
456
     * @throws SQLException if an exception occurs.
457
     * @see {@link #prune(SQLRowValues, SQLRowValues)}
457
     * @see {@link #prune(SQLRowValues, SQLRowValues)}
458
     */
458
     */
459
    public final StoreResult store(final StoreMode mode, final SQLRowValues start, final SQLRowValues pruneGraph, final Boolean checkValidity, final boolean fireEvent) throws SQLException {
459
    public final StoreResult store(final StoreMode mode, final SQLRowValues start, final SQLRowValues pruneGraph, final Boolean checkValidity, final boolean fireEvent) throws SQLException {
460
        final Map<SQLRowValues, SQLRowValues> prune2orig;
460
        final Map<SQLRowValues, SQLRowValues> prune2orig;
461
        final SQLRowValuesCluster toStore;
461
        final SQLRowValuesCluster toStore;
462
        final boolean prune = pruneGraph != null;
462
        final boolean prune = pruneGraph != null;
463
        if (!prune) {
463
        if (!prune) {
464
            toStore = this;
464
            toStore = this;
465
            prune2orig = null;
465
            prune2orig = null;
466
        } else {
466
        } else {
467
            final Map<SQLRowValues, SQLRowValues> orig2prune = this.pruneMap(start, pruneGraph, true);
467
            final Map<SQLRowValues, SQLRowValues> orig2prune = this.pruneMap(start, pruneGraph, true);
468
            toStore = orig2prune.get(start).getGraph();
468
            toStore = orig2prune.get(start).getGraph();
469
            prune2orig = CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), orig2prune);
469
            prune2orig = CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), orig2prune);
470
        }
470
        }
471
        final Map<SQLRowValues, Node> nodes = new IdentityHashMap<SQLRowValues, Node>(toStore.size());
471
        final Map<SQLRowValues, Node> nodes = new IdentityHashMap<SQLRowValues, Node>(toStore.size());
472
        final Map<SQLRowValues, Node> res = prune ? new IdentityHashMap<SQLRowValues, Node>(toStore.size()) : nodes;
472
        final Map<SQLRowValues, Node> res = prune ? new IdentityHashMap<SQLRowValues, Node>(toStore.size()) : nodes;
473
        for (final SQLRowValues vals : toStore.getItems()) {
473
        for (final SQLRowValues vals : toStore.getItems()) {
474
            nodes.put(vals, new Node(vals));
474
            nodes.put(vals, new Node(vals));
475
            if (prune) {
475
            if (prune) {
476
                final SQLRowValues src = prune2orig.get(vals);
476
                final SQLRowValues src = prune2orig.get(vals);
477
                assert this.contains(src);
477
                assert this.contains(src);
478
                res.put(src, nodes.get(vals));
478
                res.put(src, nodes.get(vals));
479
            }
479
            }
480
        }
480
        }
481
        // check validity first, avoid beginning a transaction for nothing
481
        // check validity first, avoid beginning a transaction for nothing
482
        // do it after reset otherwise check previous values
482
        // do it after reset otherwise check previous values
483
        if (SQLRowValues.isValidityChecked(checkValidity))
483
        if (SQLRowValues.isValidityChecked(checkValidity))
484
            for (final Node n : nodes.values()) {
484
            for (final Node n : nodes.values()) {
485
                n.noLink.checkValidity();
485
                n.noLink.checkValidity();
486
            }
486
            }
487
        // this will hold the links and their ID as they are known
487
        // this will hold the links and their ID as they are known
488
        /**
488
        /**
489
         * A cycle example :
489
         * A cycle example :
490
         * 
490
         * 
491
         * <pre>
491
         * <pre>
492
         * s null
492
         * s null
493
         * c ID_SITE s
493
         * c ID_SITE s
494
         * c null
494
         * c null
495
         * s ID_CONTACT_RAPPORT c
495
         * s ID_CONTACT_RAPPORT c
496
         * s ID_CONTACT_UTILE c
496
         * s ID_CONTACT_UTILE c
497
         * </pre>
497
         * </pre>
498
         * 
498
         * 
499
         * First s will be inserted :
499
         * First s will be inserted :
500
         * 
500
         * 
501
         * <pre>
501
         * <pre>
502
         * c ID_SITE sID
502
         * c ID_SITE sID
503
         * c null
503
         * c null
504
         * s ID_CONTACT_RAPPORT c
504
         * s ID_CONTACT_RAPPORT c
505
         * s ID_CONTACT_UTILE c
505
         * s ID_CONTACT_UTILE c
506
         * </pre>
506
         * </pre>
507
         * 
507
         * 
508
         * Then c :
508
         * Then c :
509
         * 
509
         * 
510
         * <pre>
510
         * <pre>
511
         * s ID_CONTACT_RAPPORT cID
511
         * s ID_CONTACT_RAPPORT cID
512
         * s ID_CONTACT_UTILE cID
512
         * s ID_CONTACT_UTILE cID
513
         * </pre>
513
         * </pre>
514
         * 
514
         * 
515
         * And finally, s will be updated.
515
         * And finally, s will be updated.
516
         */
516
         */
517
        final List<StoringLink> storingLinks = new ArrayList<StoringLink>(toStore.links.size());
517
        final List<StoringLink> storingLinks = new ArrayList<StoringLink>(toStore.links.size());
518
        for (final Link l : toStore.links)
518
        for (final Link l : toStore.links)
519
            storingLinks.add(new StoringLink(l));
519
            storingLinks.add(new StoringLink(l));
520
 
520
 
521
        // store the whole graph atomically
521
        // store the whole graph atomically
522
        final List<SQLTableEvent> events = SQLUtils.executeAtomic(getSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<List<SQLTableEvent>, SQLException>() {
522
        final List<SQLTableEvent> events = SQLUtils.executeAtomic(getSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<List<SQLTableEvent>, SQLException>() {
523
            @Override
523
            @Override
524
            public List<SQLTableEvent> handle(SQLDataSource ds) throws SQLException {
524
            public List<SQLTableEvent> handle(SQLDataSource ds) throws SQLException {
525
                final List<SQLTableEvent> res = new ArrayList<SQLTableEvent>();
525
                final List<SQLTableEvent> res = new ArrayList<SQLTableEvent>();
526
                while (storingLinks.size() > 0) {
526
                while (storingLinks.size() > 0) {
527
                    final StoringLink toStore = storingLinks.remove(0);
527
                    final StoringLink toStore = storingLinks.remove(0);
528
                    if (!toStore.canStore())
528
                    if (!toStore.canStore())
529
                        throw new IllegalStateException();
529
                        throw new IllegalStateException();
530
                    final Node n = nodes.get(toStore.getSrc());
530
                    final Node n = nodes.get(toStore.getSrc());
531
 
531
 
532
                    // merge the maximum of links starting from the row to be stored
532
                    // merge the maximum of links starting from the row to be stored
533
                    boolean lastDBAccess = true;
533
                    boolean lastDBAccess = true;
534
                    final Iterator<StoringLink> iter = storingLinks.iterator();
534
                    final Iterator<StoringLink> iter = storingLinks.iterator();
535
                    while (iter.hasNext()) {
535
                    while (iter.hasNext()) {
536
                        final StoringLink sl = iter.next();
536
                        final StoringLink sl = iter.next();
537
                        if (sl.getSrc() == toStore.getSrc()) {
537
                        if (sl.getSrc() == toStore.getSrc()) {
538
                            if (sl.canStore()) {
538
                            if (sl.canStore()) {
539
                                iter.remove();
539
                                iter.remove();
540
                                // sl can either be the main row or one the link from the row
540
                                // sl can either be the main row or one the link from the row
541
                                // (bear in mind that toStore can be not the main row if the link
541
                                // (bear in mind that toStore can be not the main row if the link
542
                                // destination has already been inserted)
542
                                // destination has already been inserted)
543
                                if (sl.destID != null)
543
                                if (sl.destID != null)
544
                                    n.noLink.put(sl.getField().getName(), sl.destID);
544
                                    n.noLink.put(sl.getField().getName(), sl.destID);
545
                            } else {
545
                            } else {
546
                                lastDBAccess = false;
546
                                lastDBAccess = false;
547
                            }
547
                            }
548
                        }
548
                        }
549
                    }
549
                    }
550
 
550
 
551
                    if (n.isStored()) {
551
                    if (n.isStored()) {
552
                        // if there's a cycle, we have to update an already inserted row
552
                        // if there's a cycle, we have to update an already inserted row
553
                        res.add(n.update(fireEvent));
553
                        res.add(n.update(fireEvent));
554
                    } else {
554
                    } else {
555
                        res.add(n.store(fireEvent, mode));
555
                        res.add(n.store(fireEvent, mode));
556
                        final SQLRow r = n.getStoredRow();
556
                        final SQLRow r = n.getStoredRow();
557
 
557
 
558
                        // fill the noLink of referent nodes with the new ID
558
                        // fill the noLink of referent nodes with the new ID
559
                        for (final StoringLink sl : storingLinks) {
559
                        for (final StoringLink sl : storingLinks) {
560
                            if (sl.getDest() == toStore.getSrc()) {
560
                            if (sl.getDest() == toStore.getSrc()) {
561
                                sl.destID = r.getIDNumber();
561
                                sl.destID = r.getIDNumber();
562
                                nodes.get(sl.getSrc()).noLink.put(sl.getField().getName(), r.getIDNumber());
562
                                nodes.get(sl.getSrc()).noLink.put(sl.getField().getName(), r.getIDNumber());
563
                            }
563
                            }
564
                        }
564
                        }
565
                    }
565
                    }
566
 
566
 
567
                    // link together the new values
567
                    // link together the new values
568
                    // if there is a cycle not all foreign keys can be stored at the same time, so
568
                    // if there is a cycle not all foreign keys can be stored at the same time, so
569
                    // wait for the last DB access
569
                    // wait for the last DB access
570
                    if (lastDBAccess) {
570
                    if (lastDBAccess) {
571
                        for (final Map.Entry<String, SQLRowValues> e : toStore.getSrc().getForeigns().entrySet()) {
571
                        for (final Map.Entry<String, SQLRowValues> e : toStore.getSrc().getForeigns().entrySet()) {
572
                            final SQLRowValues foreign = nodes.get(e.getValue()).getStoredValues();
572
                            final SQLRowValues foreign = nodes.get(e.getValue()).getStoredValues();
573
                            assert foreign != null : "since this the last db access for this row, all foreigns should have been inserted";
573
                            assert foreign != null : "since this the last db access for this row, all foreigns should have been inserted";
574
                            // check coherence
574
                            // check coherence
575
                            if (n.getStoredValues().getLong(e.getKey()) != foreign.getIDNumber().longValue())
575
                            if (n.getStoredValues().getLong(e.getKey()) != foreign.getIDNumber().longValue())
576
                                throw new IllegalStateException("stored " + n.getStoredValues().getObject(e.getKey()) + " but foreign is " + SQLRowValues.trim(foreign));
576
                                throw new IllegalStateException("stored " + n.getStoredValues().getObject(e.getKey()) + " but foreign is " + SQLRowValues.trim(foreign));
577
                            n.getStoredValues().put(e.getKey(), foreign);
577
                            n.getStoredValues().put(e.getKey(), foreign);
578
                        }
578
                        }
579
                    }
579
                    }
580
                }
580
                }
581
                // all nodes share the same graph, so pick any and freeze the graph
581
                // all nodes share the same graph, so pick any and freeze the graph
582
                // null if !fireEvent or if non-rowable table
582
                // null if !fireEvent or if non-rowable table
583
                final SQLRowValues graphFetched = nodes.values().iterator().next().getStoredValues();
583
                final SQLRowValues graphFetched = nodes.values().iterator().next().getStoredValues();
584
                if (graphFetched != null)
584
                if (graphFetched != null)
585
                    graphFetched.getGraph().freeze();
585
                    graphFetched.getGraph().freeze();
586
                return res;
586
                return res;
587
            }
587
            }
588
        });
588
        });
589
        // fire events
589
        // fire events
590
        if (fireEvent) {
590
        if (fireEvent) {
591
            for (final SQLTableEvent n : events) {
591
            for (final SQLTableEvent n : events) {
592
                // MAYBE put a Map<SQLRowValues, SQLTableEvent> to know how our fellow values have
592
                // MAYBE put a Map<SQLRowValues, SQLTableEvent> to know how our fellow values have
593
                // been affected
593
                // been affected
594
                n.getTable().fire(n);
594
                n.getTable().fire(n);
595
            }
595
            }
596
        }
596
        }
597
 
597
 
598
        return new StoreResult(res);
598
        return new StoreResult(res);
599
    }
599
    }
600
 
600
 
601
    static public final class WalkOptions {
601
    static public final class WalkOptions {
602
        private final Direction direction;
602
        private final Direction direction;
603
        private RecursionType recType;
603
        private RecursionType recType;
604
        private boolean allowCycle;
604
        private boolean allowCycle;
605
        private boolean includeStart;
605
        private boolean includeStart;
606
        private boolean ignoreForeignsOrder;
606
        private boolean ignoreForeignsOrder;
607
 
607
 
608
        public WalkOptions(final Direction dir) {
608
        public WalkOptions(final Direction dir) {
609
            if (dir == null)
609
            if (dir == null)
610
                throw new NullPointerException("No direction");
610
                throw new NullPointerException("No direction");
611
            this.direction = dir;
611
            this.direction = dir;
612
            this.recType = RecursionType.BREADTH_FIRST;
612
            this.recType = RecursionType.BREADTH_FIRST;
613
            this.allowCycle = false;
613
            this.allowCycle = false;
614
            this.includeStart = true;
614
            this.includeStart = true;
615
            this.ignoreForeignsOrder = true;
615
            this.ignoreForeignsOrder = true;
616
        }
616
        }
617
 
617
 
618
        public Direction getDirection() {
618
        public Direction getDirection() {
619
            return this.direction;
619
            return this.direction;
620
        }
620
        }
621
 
621
 
622
        public RecursionType getRecursionType() {
622
        public RecursionType getRecursionType() {
623
            return this.recType;
623
            return this.recType;
624
        }
624
        }
625
 
625
 
626
        public WalkOptions setRecursionType(RecursionType recType) {
626
        public WalkOptions setRecursionType(RecursionType recType) {
627
            if (recType == null)
627
            if (recType == null)
628
                throw new NullPointerException("No type");
628
                throw new NullPointerException("No type");
629
            this.recType = recType;
629
            this.recType = recType;
630
            return this;
630
            return this;
631
        }
631
        }
632
 
632
 
633
        public boolean isCycleAllowed() {
633
        public boolean isCycleAllowed() {
634
            return this.allowCycle;
634
            return this.allowCycle;
635
        }
635
        }
636
 
636
 
637
        /**
637
        /**
638
         * Set whether cycles (encountering the same row twice) are allowed. If <code>false</code>
638
         * Set whether cycles (encountering the same row twice) are allowed. If <code>false</code>
639
         * is passed, then steps that go back to already visited rows will never be crossed :
639
         * is passed, then steps that go back to already visited rows will never be crossed :
640
         * 
640
         * 
641
         * <pre>
641
         * <pre>
642
         *       /- ID_CONTACT_RAPPORT \
642
         *       /- ID_CONTACT_RAPPORT \
643
         *  SITE -- ID_CONTACT_CHEF ---> CONTACT 
643
         *  SITE -- ID_CONTACT_CHEF ---> CONTACT 
644
         *       \-     ID_SITE        /
644
         *       \-     ID_SITE        /
645
         * </pre>
645
         * </pre>
646
         * 
646
         * 
647
         * The SITE will only be passed once and ID_SITE would never be crossed from CONTACT. To go
647
         * The SITE will only be passed once and ID_SITE would never be crossed from CONTACT. To go
648
         * through all paths, pass <code>true</code> and SITE will be visited 3 times if
648
         * through all paths, pass <code>true</code> and SITE will be visited 3 times if
649
         * {@link Direction#FOREIGN} and 7 times if {@link Direction#ANY}. Note that in both cases,
649
         * {@link Direction#FOREIGN} and 7 times if {@link Direction#ANY}. Note that in both cases,
650
         * CONTACT will be visited 3 times if {@link Direction#ANY} and twice if
650
         * CONTACT will be visited 3 times if {@link Direction#ANY} and twice if
651
         * {@link Direction#FOREIGN}.
651
         * {@link Direction#FOREIGN}.
652
         * 
652
         * 
653
         * @param allowCycle <code>true</code> if {@link State#getValsPath()} can contains the same
653
         * @param allowCycle <code>true</code> if {@link State#getValsPath()} can contains the same
654
         *        row twice, <code>false</code> to stop before that happens.
654
         *        row twice, <code>false</code> to stop before that happens.
655
         * @return this.
655
         * @return this.
656
         */
656
         */
657
        public WalkOptions setCycleAllowed(boolean allowCycle) {
657
        public WalkOptions setCycleAllowed(boolean allowCycle) {
658
            this.allowCycle = allowCycle;
658
            this.allowCycle = allowCycle;
659
            return this;
659
            return this;
660
        }
660
        }
661
 
661
 
662
        public boolean isStartIncluded() {
662
        public boolean isStartIncluded() {
663
            return this.includeStart;
663
            return this.includeStart;
664
        }
664
        }
665
 
665
 
666
        public WalkOptions setStartIncluded(boolean includeStart) {
666
        public WalkOptions setStartIncluded(boolean includeStart) {
667
            this.includeStart = includeStart;
667
            this.includeStart = includeStart;
668
            return this;
668
            return this;
669
        }
669
        }
670
 
670
 
671
        public boolean isForeignsOrderIgnored() {
671
        public boolean isForeignsOrderIgnored() {
672
            return this.ignoreForeignsOrder;
672
            return this.ignoreForeignsOrder;
673
        }
673
        }
674
 
674
 
675
        public WalkOptions setForeignsOrderIgnored(boolean ignoreForeignsOrder) {
675
        public WalkOptions setForeignsOrderIgnored(boolean ignoreForeignsOrder) {
676
            this.ignoreForeignsOrder = ignoreForeignsOrder;
676
            this.ignoreForeignsOrder = ignoreForeignsOrder;
677
            return this;
677
            return this;
678
        }
678
        }
679
    }
679
    }
680
 
680
 
681
    /**
681
    /**
682
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
682
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
683
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
683
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
684
     * <code>start</code> is not the root, some nodes will not be traversed.
684
     * <code>start</code> is not the root, some nodes will not be traversed.
685
     * 
685
     * 
686
     * @param <T> type of acc
686
     * @param <T> type of acc
687
     * @param start where to start the walk.
687
     * @param start where to start the walk.
688
     * @param acc the initial value.
688
     * @param acc the initial value.
689
     * @param closure what to do on each node.
689
     * @param closure what to do on each node.
690
     */
690
     */
691
    public final <T> void walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure) {
691
    public final <T> void walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure) {
692
        this.walk(start, acc, closure, RecursionType.BREADTH_FIRST);
692
        this.walk(start, acc, closure, RecursionType.BREADTH_FIRST);
693
    }
693
    }
694
 
694
 
695
    /**
695
    /**
696
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
696
     * Walk the graph from the passed node, executing the closure for each node on the path. NOTE
697
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
697
     * that this method only goes one way through foreign keys, ie if this cluster is a tree and
698
     * <code>start</code> is not the root, some nodes will not be traversed. Also you can stop the
698
     * <code>start</code> is not the root, some nodes will not be traversed. Also you can stop the
699
     * recursion by throwing {@link StopRecurseException} in <code>closure</code>.
699
     * recursion by throwing {@link StopRecurseException} in <code>closure</code>.
700
     * 
700
     * 
701
     * @param <T> type of acc
701
     * @param <T> type of acc
702
     * @param start where to start the walk.
702
     * @param start where to start the walk.
703
     * @param acc the initial value.
703
     * @param acc the initial value.
704
     * @param closure what to do on each node.
704
     * @param closure what to do on each node.
705
     * @param recType how to recurse.
705
     * @param recType how to recurse.
706
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
706
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
707
     */
707
     */
708
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType) {
708
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType) {
709
        return this.walk(start, acc, closure, recType, Direction.FOREIGN);
709
        return this.walk(start, acc, closure, recType, Direction.FOREIGN);
710
    }
710
    }
711
 
711
 
712
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType, final Direction foreign) {
712
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, RecursionType recType, final Direction foreign) {
713
        return this.walk(start, acc, closure, new WalkOptions(foreign).setRecursionType(recType));
713
        return this.walk(start, acc, closure, new WalkOptions(foreign).setRecursionType(recType));
714
    }
714
    }
715
 
715
 
716
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, final WalkOptions options) {
716
    public final <T> StopRecurseException walk(final SQLRowValues start, T acc, ITransformer<State<T>, T> closure, final WalkOptions options) {
717
        this.containsCheck(start);
717
        this.containsCheck(start);
718
        return this.walk(new State<T>(Collections.singletonList(start), Path.get(start.getTable()), acc, closure), options, options.isStartIncluded());
718
        return this.walk(new State<T>(Collections.singletonList(start), Path.get(start.getTable()), acc, closure), options, options.isStartIncluded());
719
    }
719
    }
720
 
720
 
721
    /**
721
    /**
722
     * Walk through the graph from the passed state. NOTE: this method will call the {@link State}
722
     * Walk through the graph from the passed state. NOTE: this method will call the {@link State}
723
     * with each path a row can be reached (except if this would cause a cycle, see
723
     * with each path a row can be reached (except if this would cause a cycle, see
724
     * {@link WalkOptions#setCycleAllowed(boolean)}). E.g. for :
724
     * {@link WalkOptions#setCycleAllowed(boolean)}). E.g. for :
725
     * 
725
     * 
726
     * <pre>
726
     * <pre>
727
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT -- ID_TITLE ---> TITLE 
727
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT -- ID_TITLE ---> TITLE 
728
     *       \- ID_CONTACT_RAPPORT /
728
     *       \- ID_CONTACT_RAPPORT /
729
     * </pre>
729
     * </pre>
730
     * 
730
     * 
731
     * The CONTACT and TITLE will be passed twice, once for each link.
731
     * The CONTACT and TITLE will be passed twice, once for each link.
732
     * 
732
     * 
733
     * @param <T> type of acc.
733
     * @param <T> type of acc.
734
     * @param state the current position in the graph.
734
     * @param state the current position in the graph.
735
     * @param options how to walk the graph.
735
     * @param options how to walk the graph.
736
     * @param computeThisState <code>false</code> if the <code>state</code> should not be
736
     * @param computeThisState <code>false</code> if the <code>state</code> should not be
737
     *        {@link State#compute() computed}.
737
     *        {@link State#compute() computed}.
738
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
738
     * @return the exception that stopped the recursion, <code>null</code> if none was thrown.
739
     */
739
     */
740
    private final <T> StopRecurseException walk(final State<T> state, final WalkOptions options, final boolean computeThisState) {
740
    private final <T> StopRecurseException walk(final State<T> state, final WalkOptions options, final boolean computeThisState) {
741
        if (computeThisState && options.getRecursionType() == RecursionType.BREADTH_FIRST) {
741
        if (computeThisState && options.getRecursionType() == RecursionType.BREADTH_FIRST) {
742
            final StopRecurseException e = state.compute();
742
            final StopRecurseException e = state.compute();
743
            if (e != null)
743
            if (e != null)
744
                return e;
744
                return e;
745
        }
745
        }
746
 
746
 
747
        if (!options.isCycleAllowed() || !state.hasCycle()) {
747
        if (!options.isCycleAllowed() || !state.hasCycle()) {
748
            // get the foreign or referents rowValues
748
            // get the foreign or referents rowValues
749
            StopRecurseException res = null;
749
            StopRecurseException res = null;
750
            if (options.getDirection() != Direction.REFERENT) {
750
            if (options.getDirection() != Direction.REFERENT) {
751
                res = rec(state, options, Direction.FOREIGN);
751
                res = rec(state, options, Direction.FOREIGN);
752
            }
752
            }
753
            if (res != null)
753
            if (res != null)
754
                return res;
754
                return res;
755
            if (options.getDirection() != Direction.FOREIGN) {
755
            if (options.getDirection() != Direction.FOREIGN) {
756
                res = rec(state, options, Direction.REFERENT);
756
                res = rec(state, options, Direction.REFERENT);
757
            }
757
            }
758
            if (res != null)
758
            if (res != null)
759
                return res;
759
                return res;
760
        }
760
        }
761
 
761
 
762
        if (computeThisState && options.getRecursionType() == RecursionType.DEPTH_FIRST) {
762
        if (computeThisState && options.getRecursionType() == RecursionType.DEPTH_FIRST) {
763
            final StopRecurseException e = state.compute();
763
            final StopRecurseException e = state.compute();
764
            if (e != null)
764
            if (e != null)
765
                return e;
765
                return e;
766
        }
766
        }
767
        return null;
767
        return null;
768
    }
768
    }
769
 
769
 
770
    private <T> StopRecurseException rec(final State<T> state, final WalkOptions options, final Direction actualDirection) {
770
    private <T> StopRecurseException rec(final State<T> state, final WalkOptions options, final Direction actualDirection) {
771
        final SQLRowValues current = state.getCurrent();
771
        final SQLRowValues current = state.getCurrent();
772
        final List<SQLRowValues> currentValsPath = state.getValsPath();
772
        final List<SQLRowValues> currentValsPath = state.getValsPath();
773
        final SetMap<SQLField, SQLRowValues> nextVals;
773
        final SetMap<SQLField, SQLRowValues> nextVals;
774
        if (actualDirection == Direction.FOREIGN) {
774
        if (actualDirection == Direction.FOREIGN) {
775
            final Map<SQLField, SQLRowValues> foreigns = current.getForeignsBySQLField();
775
            final Map<SQLField, SQLRowValues> foreigns = current.getForeignsBySQLField();
776
            nextVals = new SetMap<SQLField, SQLRowValues>(new LinkedHashMap<SQLField, Set<SQLRowValues>>(foreigns.size()), Mode.NULL_FORBIDDEN);
776
            nextVals = new SetMap<SQLField, SQLRowValues>(new LinkedHashMap<SQLField, Set<SQLRowValues>>(foreigns.size()), Mode.NULL_FORBIDDEN);
777
            nextVals.mergeScalarMap(foreigns);
777
            nextVals.mergeScalarMap(foreigns);
778
        } else {
778
        } else {
779
            assert actualDirection == Direction.REFERENT;
779
            assert actualDirection == Direction.REFERENT;
780
            nextVals = current.getReferents();
780
            nextVals = current.getReferents();
781
        }
781
        }
782
        // predictable and repeatable order (SQLRowValues.referents has no order, but .foreigns has)
782
        // predictable and repeatable order (SQLRowValues.referents has no order, but .foreigns has)
783
        final List<SQLField> keys = new ArrayList<SQLField>(nextVals.keySet());
783
        final List<SQLField> keys = new ArrayList<SQLField>(nextVals.keySet());
784
        if (actualDirection == Direction.REFERENT || options.isForeignsOrderIgnored())
784
        if (actualDirection == Direction.REFERENT || options.isForeignsOrderIgnored())
785
            Collections.sort(keys, FIELD_COMPARATOR);
785
            Collections.sort(keys, FIELD_COMPARATOR);
786
        for (final SQLField f : keys) {
786
        for (final SQLField f : keys) {
787
            final Step step = Step.create(f, actualDirection);
787
            final Step step = Step.create(f, actualDirection);
788
            // we must never go back from where we came
788
            // we must never go back from where we came
789
            final boolean backtrack = state.getPath().length() > 0 && state.getPath().getStep(-1).equals(step.reverse());
789
            final boolean backtrack = state.getPath().length() > 0 && state.getPath().getStep(-1).equals(step.reverse());
790
            for (final SQLRowValues v : nextVals.getNonNull(f)) {
790
            for (final SQLRowValues v : nextVals.getNonNull(f)) {
791
                // backtrack is not enough, there can be other referents than previous
791
                // backtrack is not enough, there can be other referents than previous
792
                // avoid infinite loop (don't use equals so that we can go over several equals rows)
792
                // avoid infinite loop (don't use equals so that we can go over several equals rows)
793
                if (!(backtrack && v == state.getPrevious()) && (options.isCycleAllowed() || !state.identityContains(v))) {
793
                if (!(backtrack && v == state.getPrevious()) && (options.isCycleAllowed() || !state.identityContains(v))) {
794
                    final Path path = state.getPath().add(step);
794
                    final Path path = state.getPath().add(step);
795
                    final List<SQLRowValues> valsPath = new ArrayList<SQLRowValues>(currentValsPath);
795
                    final List<SQLRowValues> valsPath = new ArrayList<SQLRowValues>(currentValsPath);
796
                    valsPath.add(v);
796
                    valsPath.add(v);
797
                    final StopRecurseException e = this.walk(new State<T>(Collections.unmodifiableList(valsPath), path, state.getAcc(), state.closure), options, true);
797
                    final StopRecurseException e = this.walk(new State<T>(Collections.unmodifiableList(valsPath), path, state.getAcc(), state.closure), options, true);
798
                    if (e != null && e.isCompletely())
798
                    if (e != null && e.isCompletely())
799
                        return e;
799
                        return e;
800
                }
800
                }
801
            }
801
            }
802
        }
802
        }
803
        return null;
803
        return null;
804
    }
804
    }
805
 
805
 
806
    @Immutable
806
    @Immutable
807
    static public final class IndexedRows {
807
    static public final class IndexedRows {
808
        private final List<State<?>> flatList;
808
        private final List<State<?>> flatList;
809
        private final Map<SQLRowValues, Integer> indexes;
809
        private final Map<SQLRowValues, Integer> indexes;
810
 
810
 
811
        // optimization
811
        // optimization
812
        private IndexedRows(final SQLRowValues sole) {
812
        private IndexedRows(final SQLRowValues sole) {
813
            this(Collections.<State<?>> singletonList(new State<Object>(Collections.singletonList(sole), Path.get(sole.getTable()), null, null)), Collections.singletonMap(sole, 0));
813
            this(Collections.<State<?>> singletonList(new State<Object>(Collections.singletonList(sole), Path.get(sole.getTable()), null, null)), Collections.singletonMap(sole, 0));
814
            if (sole.getGraphSize() != 1)
814
            if (sole.getGraphSize() != 1)
815
                throw new IllegalArgumentException("Row is not alone : " + sole.printGraph());
815
                throw new IllegalArgumentException("Row is not alone : " + sole.printGraph());
816
        }
816
        }
817
 
817
 
818
        // private to make sure the arguments are immutable
818
        // private to make sure the arguments are immutable
819
        private IndexedRows(List<State<?>> flatList, Map<SQLRowValues, Integer> indexes) {
819
        private IndexedRows(List<State<?>> flatList, Map<SQLRowValues, Integer> indexes) {
820
            super();
820
            super();
821
            this.flatList = flatList;
821
            this.flatList = flatList;
822
            this.indexes = indexes;
822
            this.indexes = indexes;
823
            assert flatList.size() == indexes.size();
823
            assert flatList.size() == indexes.size();
824
        }
824
        }
825
 
825
 
826
        List<State<?>> getStates() {
826
        List<State<?>> getStates() {
827
            return this.flatList;
827
            return this.flatList;
828
        }
828
        }
829
 
829
 
830
        public int getSize() {
830
        public int getSize() {
831
            return this.flatList.size();
831
            return this.flatList.size();
832
        }
832
        }
833
 
833
 
834
        State<?> getFirstState(int i) {
834
        State<?> getFirstState(int i) {
835
            return this.flatList.get(i);
835
            return this.flatList.get(i);
836
        }
836
        }
837
 
837
 
838
        public SQLRowValues getRow(int i) {
838
        public SQLRowValues getRow(int i) {
839
            return this.getFirstState(i).getCurrent();
839
            return this.getFirstState(i).getCurrent();
840
        }
840
        }
841
 
841
 
842
        public Path getFirstPath(int i) {
842
        public Path getFirstPath(int i) {
843
            return this.getFirstState(i).getPath();
843
            return this.getFirstState(i).getPath();
844
        }
844
        }
845
 
845
 
846
        public int getIndex(final SQLRowValues v) {
846
        public int getIndex(final SQLRowValues v) {
847
            return this.indexes.get(v);
847
            return this.indexes.get(v);
848
        }
848
        }
849
    }
849
    }
850
 
850
 
851
    /**
851
    /**
852
     * Return all rows of this graph. This method will
852
     * Return all rows of this graph. This method will
853
     * {@link #walk(SQLRowValues, Object, ITransformer, WalkOptions) walk} the graph with the passed
853
     * {@link #walk(SQLRowValues, Object, ITransformer, WalkOptions) walk} the graph with the passed
854
     * parameters and collect the {@link State state} when it first reaches each row.
854
     * parameters and collect the {@link State state} when it first reaches each row.
855
     * 
855
     * 
856
     * @param vals where to start.
856
     * @param vals where to start.
857
     * @param recType how to recurse.
857
     * @param recType how to recurse.
858
     * @param useForeignsOrder <code>true</code> to use the order of foreign keys.
858
     * @param useForeignsOrder <code>true</code> to use the order of foreign keys.
859
     * @return all rows.
859
     * @return all rows.
860
     */
860
     */
861
    public final IndexedRows getIndexedRows(final SQLRowValues vals, final RecursionType recType, final boolean useForeignsOrder) {
861
    public final IndexedRows getIndexedRows(final SQLRowValues vals, final RecursionType recType, final boolean useForeignsOrder) {
862
        final int size = this.size();
862
        final int size = this.size();
863
        final List<State<?>> flatList = new ArrayList<State<?>>(size);
863
        final List<State<?>> flatList = new ArrayList<State<?>>(size);
864
        final IdentityHashMap<SQLRowValues, Integer> thisIndexes = new IdentityHashMap<SQLRowValues, Integer>(size);
864
        final IdentityHashMap<SQLRowValues, Integer> thisIndexes = new IdentityHashMap<SQLRowValues, Integer>(size);
865
 
865
 
866
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(recType).setForeignsOrderIgnored(!useForeignsOrder).setStartIncluded(true);
866
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(recType).setForeignsOrderIgnored(!useForeignsOrder).setStartIncluded(true);
867
        this.walk(vals, null, new ITransformer<State<Object>, Object>() {
867
        this.walk(vals, null, new ITransformer<State<Object>, Object>() {
868
            @Override
868
            @Override
869
            public Object transformChecked(State<Object> input) {
869
            public Object transformChecked(State<Object> input) {
870
                final SQLRowValues r = input.getCurrent();
870
                final SQLRowValues r = input.getCurrent();
871
                if (thisIndexes.containsKey(r)) {
871
                if (thisIndexes.containsKey(r)) {
872
                    // happens when there's multiple paths between 2 rows
872
                    // happens when there's multiple paths between 2 rows
873
                    throw new StopRecurseException("already added").setCompletely(false);
873
                    throw new StopRecurseException("already added").setCompletely(false);
874
                } else {
874
                } else {
875
                    thisIndexes.put(r, flatList.size());
875
                    thisIndexes.put(r, flatList.size());
876
                    flatList.add(input);
876
                    flatList.add(input);
877
                }
877
                }
878
                return null;
878
                return null;
879
            }
879
            }
880
        }, walkOptions);
880
        }, walkOptions);
881
 
881
 
882
        assert flatList.size() == size : "missing rows, should have been " + size + " but was " + flatList.size() + " : " + flatList;
882
        assert flatList.size() == size : "missing rows, should have been " + size + " but was " + flatList.size() + " : " + flatList;
883
        return new IndexedRows(Collections.unmodifiableList(flatList), Collections.unmodifiableMap(thisIndexes));
883
        return new IndexedRows(Collections.unmodifiableList(flatList), Collections.unmodifiableMap(thisIndexes));
884
    }
884
    }
885
 
885
 
886
    final void walkFields(final SQLRowValues start, IClosure<FieldPath> closure, final boolean includeFK) {
886
    final void walkFields(final SQLRowValues start, IClosure<FieldPath> closure, final boolean includeFK) {
887
        walkFields(start, Path.get(start.getTable()), Collections.singletonList(start), closure, includeFK);
887
        walkFields(start, Path.get(start.getTable()), Collections.singletonList(start), closure, includeFK);
888
    }
888
    }
889
 
889
 
890
    private void walkFields(final SQLRowValues current, final Path p, final List<SQLRowValues> currentValsPath, IClosure<FieldPath> closure, final boolean includeFK) {
890
    private void walkFields(final SQLRowValues current, final Path p, final List<SQLRowValues> currentValsPath, IClosure<FieldPath> closure, final boolean includeFK) {
891
        final Map<String, SQLRowValues> foreigns = current.getForeigns();
891
        final Map<String, SQLRowValues> foreigns = current.getForeigns();
892
        for (final String field : current.getFields()) {
892
        for (final String field : current.getFields()) {
893
            final boolean isFK = foreigns.containsKey(field);
893
            final boolean isFK = foreigns.containsKey(field);
894
            if (!isFK || includeFK)
894
            if (!isFK || includeFK)
895
                closure.executeChecked(new FieldPath(p, field));
895
                closure.executeChecked(new FieldPath(p, field));
896
            if (isFK) {
896
            if (isFK) {
897
                final SQLRowValues newVals = foreigns.get(field);
897
                final SQLRowValues newVals = foreigns.get(field);
898
                // avoid infinite loop
898
                // avoid infinite loop
899
                if (!currentValsPath.contains(newVals)) {
899
                if (!currentValsPath.contains(newVals)) {
900
                    final Path newP = p.add(current.getTable().getField(field), Direction.FOREIGN);
900
                    final Path newP = p.add(current.getTable().getField(field), Direction.FOREIGN);
901
                    final List<SQLRowValues> newValsPath = new ArrayList<SQLRowValues>(currentValsPath);
901
                    final List<SQLRowValues> newValsPath = new ArrayList<SQLRowValues>(currentValsPath);
902
                    newValsPath.add(newVals);
902
                    newValsPath.add(newVals);
903
                    this.walkFields(newVals, newP, newValsPath, closure, includeFK);
903
                    this.walkFields(newVals, newP, newValsPath, closure, includeFK);
904
                }
904
                }
905
            }
905
            }
906
        }
906
        }
907
    }
907
    }
908
 
908
 
909
    /**
909
    /**
910
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
910
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
911
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
911
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
912
     * contain all the paths.
912
     * contain all the paths.
913
     * 
913
     * 
914
     * @param start where to start pruning, eg RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
914
     * @param start where to start pruning, eg RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
915
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
915
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
916
     * @param graph the maximum structure wanted, eg RECEPTEUR {DESIGNATION=null}.
916
     * @param graph the maximum structure wanted, eg RECEPTEUR {DESIGNATION=null}.
917
     * @return a copy of this no larger than <code>graph</code>, eg RECEPTEUR {DESIGNATION="rec"}.
917
     * @return a copy of this no larger than <code>graph</code>, eg RECEPTEUR {DESIGNATION="rec"}.
918
     */
918
     */
919
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph) {
919
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph) {
920
        return this.prune(start, graph, true);
920
        return this.prune(start, graph, true);
921
    }
921
    }
922
 
922
 
923
    /**
923
    /**
924
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
924
     * Copy this leaving only the fields specified by <code>graph</code>. NOTE: the result is
925
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
925
     * guaranteed not to be larger than <code>graph</code>, but it might be smaller if this didn't
926
     * contain all the paths. NOTE : only fields are considered, i.e. referents are not used. E.g.
926
     * contain all the paths. NOTE : only fields are considered, i.e. referents are not used. E.g.
927
     * with the rows below, no links would be removed, even though one LOCAL in the graph has no
927
     * with the rows below, no links would be removed, even though one LOCAL in the graph has no
928
     * CPI.
928
     * CPI.
929
     * 
929
     * 
930
     * <pre>
930
     * <pre>
931
     * Graph :
931
     * Graph :
932
     *        LOCAL LOCAL
932
     *        LOCAL LOCAL
933
     *      /        |
933
     *      /        |
934
     *   SOURCE --> CPI
934
     *   SOURCE --> CPI
935
     * 
935
     * 
936
     * This : 
936
     * This : 
937
     *        LOCAL
937
     *        LOCAL
938
     *      /       \
938
     *      /       \
939
     *   SOURCE --> CPI
939
     *   SOURCE --> CPI
940
     * </pre>
940
     * </pre>
941
     * 
941
     * 
942
     * 
942
     * 
943
     * @param start where to start pruning, e.g. RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
943
     * @param start where to start pruning, e.g. RECEPTEUR {DESIGNATION="rec", CONSTAT="ok",
944
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
944
     *        ID_LOCAL=LOCAL{DESIGNATION="local"}}.
945
     * @param graph the maximum structure wanted, e.g. RECEPTEUR {DESIGNATION=null}.
945
     * @param graph the maximum structure wanted, e.g. RECEPTEUR {DESIGNATION=null}.
946
     * @param keepUnionOfFields only relevant when the same row from this can be reached by more
946
     * @param keepUnionOfFields only relevant when the same row from this can be reached by more
947
     *        than one path in <code>graph</code>.
947
     *        than one path in <code>graph</code>.
948
     * 
948
     * 
949
     *        <pre>
949
     *        <pre>
950
     * Graph :
950
     * Graph :
951
     *       /- ID_CONTACT_RAPPORT --> CONTACT1
951
     *       /- ID_CONTACT_RAPPORT --> CONTACT1
952
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT2 
952
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT2 
953
     * 
953
     * 
954
     * This :
954
     * This :
955
     *       /- ID_CONTACT_RAPPORT \
955
     *       /- ID_CONTACT_RAPPORT \
956
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT
956
     *  SITE -- ID_CONTACT_CHEF ---> CONTACT
957
     *        </pre>
957
     *        </pre>
958
     * 
958
     * 
959
     *        If <code>true</code> keep the union of all fields, if <code>false</code> the
959
     *        If <code>true</code> keep the union of all fields, if <code>false</code> the
960
     *        intersection.
960
     *        intersection.
961
     * @return a copy of this no larger than <code>graph</code>, e.g. RECEPTEUR {DESIGNATION="rec"}.
961
     * @return a copy of this no larger than <code>graph</code>, e.g. RECEPTEUR {DESIGNATION="rec"}.
962
     */
962
     */
963
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
963
    public final SQLRowValues prune(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
964
        return pruneMap(start, graph, keepUnionOfFields).get(start);
964
        return pruneMap(start, graph, keepUnionOfFields).get(start);
965
    }
965
    }
966
 
966
 
967
    public final Map<SQLRowValues, SQLRowValues> pruneMap(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
967
    public final Map<SQLRowValues, SQLRowValues> pruneMap(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
968
        this.containsCheck(start);
968
        this.containsCheck(start);
969
        return this.copy(computeToRetain(start, graph, keepUnionOfFields), false, false);
969
        return this.copy(computeToRetain(start, graph, keepUnionOfFields), false, false);
970
    }
970
    }
971
 
971
 
972
    static SetMap<SQLRowValues, String> computeToRetain(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
972
    static SetMap<SQLRowValues, String> computeToRetain(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
973
        if (!start.getTable().equals(graph.getTable()))
973
        if (!start.getTable().equals(graph.getTable()))
974
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
974
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
975
        // there's no way to tell apart 2 referents
975
        // there's no way to tell apart 2 referents
976
        if (!graph.getGraph().hasOneRowPerPath())
976
        if (!graph.getGraph().hasOneRowPerPath())
977
            throw new IllegalArgumentException("More than one row for " + graph.printGraph());
977
            throw new IllegalArgumentException("More than one row for " + graph.printGraph());
978
 
978
 
979
        final SetMap<SQLRowValues, String> toRetain = new SetMap<SQLRowValues, String>(new IdentityHashMap<SQLRowValues, Set<String>>(), Mode.NULL_FORBIDDEN);
979
        final SetMap<SQLRowValues, String> toRetain = new SetMap<SQLRowValues, String>(new IdentityHashMap<SQLRowValues, Set<String>>(), Mode.NULL_FORBIDDEN);
980
        // BREADTH_FIRST to stop as soon as this no longer have rows in the graph
980
        // BREADTH_FIRST to stop as soon as this no longer have rows in the graph
981
        // CycleAllowed since we need to go through every path (e.g. what is a cycle in graph might
981
        // CycleAllowed since we need to go through every path (e.g. what is a cycle in graph might
982
        // not be in this :
982
        // not be in this :
983
        // SITE1 -> CONTACT1 -> SITE1 for graph and
983
        // SITE1 -> CONTACT1 -> SITE1 for graph and
984
        // SITE1 -> CONTACT1 -> SITE2 for this)
984
        // SITE1 -> CONTACT1 -> SITE2 for this)
985
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setCycleAllowed(true);
985
        final WalkOptions walkOptions = new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setCycleAllowed(true);
986
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
986
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
987
            @Override
987
            @Override
988
            public Object transformChecked(State<Object> input) {
988
            public Object transformChecked(State<Object> input) {
989
                final SQLRowValues r = input.getCurrent();
989
                final SQLRowValues r = input.getCurrent();
990
                // since we allowed cycles in graph, allow it here
990
                // since we allowed cycles in graph, allow it here
991
                final Collection<SQLRowValues> rows = start.followPath(input.getPath(), CreateMode.CREATE_NONE, false, true);
991
                final Collection<SQLRowValues> rows = start.followPath(input.getPath(), CreateMode.CREATE_NONE, false, true);
992
                // since we're using BREADTH_FIRST, the next path will be longer so no need to
992
                // since we're using BREADTH_FIRST, the next path will be longer so no need to
993
                // continue if there's already no row
993
                // continue if there's already no row
994
                if (rows.isEmpty())
994
                if (rows.isEmpty())
995
                    throw new StopRecurseException().setCompletely(false);
995
                    throw new StopRecurseException().setCompletely(false);
996
                for (final SQLRowValues row : rows) {
996
                for (final SQLRowValues row : rows) {
997
                    if (keepUnionOfFields || !toRetain.containsKey(row))
997
                    if (keepUnionOfFields || !toRetain.containsKey(row))
998
                        toRetain.addAll(row, r.getFields());
998
                        toRetain.addAll(row, r.getFields());
999
                    else
999
                    else
1000
                        toRetain.getCollection(row).retainAll(r.getFields());
1000
                        toRetain.getCollection(row).retainAll(r.getFields());
1001
                }
1001
                }
1002
                return null;
1002
                return null;
1003
            }
1003
            }
1004
        }, walkOptions);
1004
        }, walkOptions);
1005
        return toRetain;
1005
        return toRetain;
1006
    }
1006
    }
1007
 
1007
 
1008
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph) {
1008
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph) {
1009
        return this.pruneWithoutCopy(start, graph, true);
1009
        return this.pruneWithoutCopy(start, graph, true);
1010
    }
1010
    }
1011
 
1011
 
1012
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
1012
    public final SQLRowValues pruneWithoutCopy(final SQLRowValues start, final SQLRowValues graph, final boolean keepUnionOfFields) {
1013
        this.containsCheck(start);
1013
        this.containsCheck(start);
1014
        final SetMap<SQLRowValues, String> toRetain = computeToRetain(start, graph, keepUnionOfFields);
1014
        final SetMap<SQLRowValues, String> toRetain = computeToRetain(start, graph, keepUnionOfFields);
1015
        assert toRetain.containsKey(start);
1015
        assert toRetain.containsKey(start);
1016
 
1016
 
1017
        // remove extra fields and flatten if necessary
1017
        // remove extra fields and flatten if necessary
1018
        for (final Entry<SQLRowValues, Set<String>> e : toRetain.entrySet()) {
1018
        for (final Entry<SQLRowValues, Set<String>> e : toRetain.entrySet()) {
1019
            final SQLRowValues r = e.getKey();
1019
            final SQLRowValues r = e.getKey();
1020
            r.retainAll(e.getValue());
1020
            r.retainAll(e.getValue());
1021
            final Set<String> toFlatten = new HashSet<String>();
1021
            final Set<String> toFlatten = new HashSet<String>();
1022
            for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1022
            for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1023
                final SQLRowValues foreign = e2.getValue();
1023
                final SQLRowValues foreign = e2.getValue();
1024
                if (!toRetain.containsKey(foreign)) {
1024
                if (!toRetain.containsKey(foreign)) {
1025
                    toFlatten.add(e2.getKey());
1025
                    toFlatten.add(e2.getKey());
1026
                }
1026
                }
1027
            }
1027
            }
1028
            for (final String fieldToFlatten : toFlatten) {
1028
            for (final String fieldToFlatten : toFlatten) {
1029
                r.flatten(fieldToFlatten, ForeignCopyMode.COPY_ID_OR_RM);
1029
                r.flatten(fieldToFlatten, ForeignCopyMode.COPY_ID_OR_RM);
1030
            }
1030
            }
1031
        }
1031
        }
1032
        // now, remove referents that aren't in the graph
1032
        // now, remove referents that aren't in the graph
1033
        for (final SQLRowValues r : new ArrayList<SQLRowValues>(start.getGraph().getItems())) {
1033
        for (final SQLRowValues r : new ArrayList<SQLRowValues>(start.getGraph().getItems())) {
1034
            if (!toRetain.containsKey(r)) {
1034
            if (!toRetain.containsKey(r)) {
1035
                // only remove links at the border and even then, only remove links to the result :
1035
                // only remove links at the border and even then, only remove links to the result :
1036
                // avoid creating a myriad of graphs
1036
                // avoid creating a myriad of graphs
1037
                final Set<String> toFlatten = new HashSet<String>();
1037
                final Set<String> toFlatten = new HashSet<String>();
1038
                for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1038
                for (final Entry<String, SQLRowValues> e2 : r.getForeigns().entrySet()) {
1039
                    final SQLRowValues foreign = e2.getValue();
1039
                    final SQLRowValues foreign = e2.getValue();
1040
                    if (toRetain.containsKey(foreign)) {
1040
                    if (toRetain.containsKey(foreign)) {
1041
                        toFlatten.add(e2.getKey());
1041
                        toFlatten.add(e2.getKey());
1042
                    }
1042
                    }
1043
                }
1043
                }
1044
                r.removeAll(toFlatten);
1044
                r.removeAll(toFlatten);
1045
            }
1045
            }
1046
        }
1046
        }
1047
        assert start.getGraph().getItems().equals(toRetain.keySet());
1047
        assert start.getGraph().getItems().equals(toRetain.keySet());
1048
        // NOTE this instance no longer the graph of start if referents were removed
1048
        // NOTE this instance no longer the graph of start if referents were removed
1049
        return start;
1049
        return start;
1050
    }
1050
    }
1051
 
1051
 
1052
    // TODO handle referents (and decide how to handle multiple paths to the same node)
1052
    // TODO handle referents (and decide how to handle multiple paths to the same node)
1053
    final void grow(final SQLRowValues start, final SQLRowValues toGrow, final boolean checkFields, final boolean growUndefined) {
1053
    final void grow(final SQLRowValues start, final SQLRowValues toGrow, final boolean checkFields, final boolean growUndefined) {
1054
        this.containsCheck(start);
1054
        this.containsCheck(start);
1055
        if (!start.getTable().equals(toGrow.getTable()))
1055
        if (!start.getTable().equals(toGrow.getTable()))
1056
            throw new IllegalArgumentException(start + " is not from the same table as " + toGrow);
1056
            throw new IllegalArgumentException(start + " is not from the same table as " + toGrow);
1057
        this.walk(start, toGrow, new ITransformer<State<SQLRowValues>, SQLRowValues>() {
1057
        this.walk(start, toGrow, new ITransformer<State<SQLRowValues>, SQLRowValues>() {
1058
            @Override
1058
            @Override
1059
            public SQLRowValues transformChecked(State<SQLRowValues> input) {
1059
            public SQLRowValues transformChecked(State<SQLRowValues> input) {
1060
                final SQLRowValues existing = toGrow.followPath(input.getPath());
1060
                final SQLRowValues existing = toGrow.followPath(input.getPath());
1061
                // don't add undefined row if there's none or if don't want
1061
                // don't add undefined row if there's none or if don't want
1062
                if (existing == null && input.getAcc().isForeignEmpty(input.getFrom().getName()) && (input.getPath().getLast().getUndefinedIDNumber() == null || !growUndefined))
1062
                if (existing == null && input.getAcc().isForeignEmpty(input.getFrom().getName()) && (input.getPath().getLast().getUndefinedIDNumber() == null || !growUndefined))
1063
                    throw new StopRecurseException().setCompletely(false);
1063
                    throw new StopRecurseException().setCompletely(false);
1064
                if (existing == null || (checkFields && !existing.getFields().containsAll(input.getCurrent().getFields()))) {
1064
                if (existing == null || (checkFields && !existing.getFields().containsAll(input.getCurrent().getFields()))) {
1065
                    final SQLRowValues leaf = toGrow.assurePath(input.getPath());
1065
                    final SQLRowValues leaf = toGrow.assurePath(input.getPath());
1066
                    if (leaf.hasID()) {
1066
                    if (leaf.hasID()) {
1067
                        final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(input.getCurrent());
1067
                        final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(input.getCurrent());
1068
                        // don't exclude undef otherwise cannot grow eg
1068
                        // don't exclude undef otherwise cannot grow eg
1069
                        // LOCAL.ID_FAMILLE_2 = 1
1069
                        // LOCAL.ID_FAMILLE_2 = 1
1070
                        if (growUndefined) {
1070
                        if (growUndefined) {
1071
                            fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
1071
                            fetcher.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
1072
                                @Override
1072
                                @Override
1073
                                public SQLSelect transformChecked(SQLSelect input) {
1073
                                public SQLSelect transformChecked(SQLSelect input) {
1074
                                    input.setExcludeUndefined(false);
1074
                                    input.setExcludeUndefined(false);
1075
                                    return input;
1075
                                    return input;
1076
                                }
1076
                                }
1077
                            });
1077
                            });
1078
                        }
1078
                        }
1079
                        final SQLRowValues fetched = fetcher.fetchOne(leaf.getIDNumber());
1079
                        final SQLRowValues fetched = fetcher.fetchOne(leaf.getIDNumber());
1080
                        if (fetched == null)
1080
                        if (fetched == null)
1081
                            throw new IllegalArgumentException("no row for " + fetcher);
1081
                            throw new IllegalArgumentException("no row for " + fetcher);
1082
                        leaf.load(fetched, null);
1082
                        leaf.load(fetched, null);
1083
                        // we already loaded all further rows
1083
                        // we already loaded all further rows
1084
                        throw new StopRecurseException().setCompletely(false);
1084
                        throw new StopRecurseException().setCompletely(false);
1085
                    } else {
1085
                    } else {
1086
                        throw new IllegalArgumentException("cannot expand, missing ID in " + leaf + " at " + input.getPath());
1086
                        throw new IllegalArgumentException("cannot expand, missing ID in " + leaf + " at " + input.getPath());
1087
                    }
1087
                    }
1088
                } else {
1088
                } else {
1089
                    return existing;
1089
                    return existing;
1090
                }
1090
                }
1091
            }
1091
            }
1092
        }, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
1092
        }, RecursionType.BREADTH_FIRST, Direction.FOREIGN);
1093
    }
1093
    }
1094
 
1094
 
1095
    public final String contains(final SQLRowValues start, SQLRowValues graph) {
1095
    public final String contains(final SQLRowValues start, SQLRowValues graph) {
1096
        return this.contains(start, graph, true);
1096
        return this.contains(start, graph, true);
1097
    }
1097
    }
1098
 
1098
 
1099
    /**
1099
    /**
1100
     * Whether the tree begining at <code>start</code> contains the tree begining at
1100
     * Whether the tree begining at <code>start</code> contains the tree begining at
1101
     * <code>graph</code>. Ie each path of <code>graph</code> must exist from <code>start</code>.
1101
     * <code>graph</code>. Ie each path of <code>graph</code> must exist from <code>start</code>.
1102
     * 
1102
     * 
1103
     * @param start a SQLRowValues of this.
1103
     * @param start a SQLRowValues of this.
1104
     * @param graph another SQLRowValues.
1104
     * @param graph another SQLRowValues.
1105
     * @param checkFields <code>true</code> to check that each rowValues of this containsAll the
1105
     * @param checkFields <code>true</code> to check that each rowValues of this containsAll the
1106
     *        fields of the other.
1106
     *        fields of the other.
1107
     * @return a String explaining the first problem, <code>null</code> if <code>graph</code> is
1107
     * @return a String explaining the first problem, <code>null</code> if <code>graph</code> is
1108
     *         contained in this.
1108
     *         contained in this.
1109
     */
1109
     */
1110
    public final String contains(final SQLRowValues start, SQLRowValues graph, final boolean checkFields) {
1110
    public final String contains(final SQLRowValues start, SQLRowValues graph, final boolean checkFields) {
1111
        this.containsCheck(start);
1111
        this.containsCheck(start);
1112
        if (!start.getTable().equals(graph.getTable()))
1112
        if (!start.getTable().equals(graph.getTable()))
1113
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1113
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1114
        final StopRecurseException res = graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1114
        final StopRecurseException res = graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1115
            @Override
1115
            @Override
1116
            public Object transformChecked(State<Object> input) {
1116
            public Object transformChecked(State<Object> input) {
1117
                final SQLRowValues v = start.followPath(input.getPath());
1117
                final SQLRowValues v = start.followPath(input.getPath());
1118
                if (v == null)
1118
                if (v == null)
1119
                    throw new StopRecurseException("no " + input.getPath() + " in " + start);
1119
                    throw new StopRecurseException("no " + input.getPath() + " in " + start);
1120
                if (checkFields && !v.getFields().containsAll(input.getCurrent().getFields()))
1120
                if (checkFields && !v.getFields().containsAll(input.getCurrent().getFields()))
1121
                    throw new StopRecurseException("at " + input.getPath() + " " + v.getFields() + " does not contain " + input.getCurrent().getFields());
1121
                    throw new StopRecurseException("at " + input.getPath() + " " + v.getFields() + " does not contain " + input.getCurrent().getFields());
1122
 
1122
 
1123
                return null;
1123
                return null;
1124
            }
1124
            }
1125
        }, RecursionType.BREADTH_FIRST);
1125
        }, RecursionType.BREADTH_FIRST);
1126
        return res == null ? null : res.getMessage();
1126
        return res == null ? null : res.getMessage();
1127
    }
1127
    }
1128
 
1128
 
1129
    /**
1129
    /**
1130
     * Merge one graph (including its fields) into another.
1130
     * Merge one graph (including its fields) into another.
1131
     * 
1131
     * 
1132
     * Merge
1132
     * Merge
1133
     * 
1133
     * 
1134
     * <pre>
1134
     * <pre>
1135
     *  LOCAL(DESIGNATION, ORDRE) <-- CPI(ORDRE)
1135
     *  LOCAL(DESIGNATION, ORDRE) <-- CPI(ORDRE)
1136
     *                             \--- SRC --/
1136
     *                             \--- SRC --/
1137
     * </pre>
1137
     * </pre>
1138
     * 
1138
     * 
1139
     * Into
1139
     * Into
1140
     * 
1140
     * 
1141
     * <pre>
1141
     * <pre>
1142
     *  LOCAL(DESIGNATION, ARCHIVE) <-- CPI(ARCHIVE)
1142
     *  LOCAL(DESIGNATION, ARCHIVE) <-- CPI(ARCHIVE)
1143
     * </pre>
1143
     * </pre>
1144
     * 
1144
     * 
1145
     * Result in this :
1145
     * Result in this :
1146
     * 
1146
     * 
1147
     * <pre>
1147
     * <pre>
1148
     *  LOCAL(DESIGNATION, ORDRE, ARCHIVE) <-- CPI(ORDRE, ARCHIVE)
1148
     *  LOCAL(DESIGNATION, ORDRE, ARCHIVE) <-- CPI(ORDRE, ARCHIVE)
1149
     *                                      \--- SRC --/
1149
     *                                      \--- SRC --/
1150
     * </pre>
1150
     * </pre>
1151
     * 
1151
     * 
1152
     * @param start the graph that will be modified.
1152
     * @param start the graph that will be modified.
1153
     * @param graph the graph that will be merged (not modified).
1153
     * @param graph the graph that will be merged (not modified).
1154
     */
1154
     */
1155
    public final void merge(final SQLRowValues start, final SQLRowValues graph) {
1155
    public final void merge(final SQLRowValues start, final SQLRowValues graph) {
1156
        this.containsCheck(start);
1156
        this.containsCheck(start);
1157
        if (start == graph)
1157
        if (start == graph)
1158
            return;
1158
            return;
1159
        if (!start.getTable().equals(graph.getTable()))
1159
        if (!start.getTable().equals(graph.getTable()))
1160
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1160
            throw new IllegalArgumentException(start + " is not from the same table as " + graph);
1161
        final Map<SQLRowValues, SQLRowValues> from = new IdentityHashMap<>();
1161
        final Map<SQLRowValues, SQLRowValues> from = new IdentityHashMap<>();
1162
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1162
        graph.getGraph().walk(graph, null, new ITransformer<State<Object>, Object>() {
1163
            @Override
1163
            @Override
1164
            public Object transformChecked(State<Object> input) {
1164
            public Object transformChecked(State<Object> input) {
1165
                final SQLRowValues previousReceiver = from.get(input.getPrevious());
1165
                final SQLRowValues previousReceiver = from.get(input.getPrevious());
1166
                final SQLRowValues alreadyMergedReceiver = from.get(input.getCurrent());
1166
                final SQLRowValues alreadyMergedReceiver = from.get(input.getCurrent());
1167
                if (alreadyMergedReceiver != null) {
1167
                if (alreadyMergedReceiver != null) {
1168
                    // fields already merged just add a link
1168
                    // fields already merged just add a link
1169
                    previousReceiver.put(input.getPath().getStep(-1), alreadyMergedReceiver);
1169
                    previousReceiver.put(input.getPath().getStep(-1), alreadyMergedReceiver);
1170
                } else {
1170
                } else {
1171
                    final SQLRowValues currentReceiver;
1171
                    final SQLRowValues currentReceiver;
1172
                    if (input.getPath().length() == 0) {
1172
                    if (input.getPath().length() == 0) {
1173
                        assert input.getCurrent() == graph;
1173
                        assert input.getCurrent() == graph;
1174
                        assert previousReceiver == null;
1174
                        assert previousReceiver == null;
1175
                        currentReceiver = start;
1175
                        currentReceiver = start;
1176
                    } else {
1176
                    } else {
1177
                        currentReceiver = previousReceiver.followPath(input.getPath().subPath(-1));
1177
                        currentReceiver = previousReceiver.followPath(input.getPath().subPath(-1));
1178
                    }
1178
                    }
1179
                    final SQLRowValues actualReceiver;
1179
                    final SQLRowValues actualReceiver;
1180
                    if (currentReceiver != null) {
1180
                    if (currentReceiver != null) {
1181
                        actualReceiver = currentReceiver;
1181
                        actualReceiver = currentReceiver;
1182
                        // merge fields
1182
                        // merge fields
1183
                        actualReceiver.putAll(input.getCurrent().getAllValues(ForeignCopyMode.COPY_NULL), null, FillMode.DONT_OVERWRITE);
1183
                        actualReceiver.putAll(input.getCurrent().getAllValues(ForeignCopyMode.COPY_NULL), null, FillMode.DONT_OVERWRITE);
1184
                    } else {
1184
                    } else {
1185
                        // node didn't exist in the receiving graph, copy the single node with all
1185
                        // node didn't exist in the receiving graph, copy the single node with all
1186
                        // its fields
1186
                        // its fields
1187
                        actualReceiver = new SQLRowValues(input.getCurrent(), ForeignCopyMode.COPY_NULL);
1187
                        actualReceiver = new SQLRowValues(input.getCurrent(), ForeignCopyMode.COPY_NULL);
1188
                        previousReceiver.put(input.getPath().getStep(-1), actualReceiver);
1188
                        previousReceiver.put(input.getPath().getStep(-1), actualReceiver);
1189
                    }
1189
                    }
1190
                    assert actualReceiver != null;
1190
                    assert actualReceiver != null;
1191
                    from.put(input.getCurrent(), actualReceiver);
1191
                    from.put(input.getCurrent(), actualReceiver);
1192
                }
1192
                }
1193
                return null;
1193
                return null;
1194
            }
1194
            }
1195
            // cycle allowed to go through all links
1195
            // cycle allowed to go through all links
1196
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setForeignsOrderIgnored(false).setCycleAllowed(true));
1196
        }, new WalkOptions(Direction.ANY).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(true).setForeignsOrderIgnored(false).setCycleAllowed(true));
1197
    }
1197
    }
1198
 
1198
 
1199
    /**
1199
    /**
1200
     * Return a graphical representation of the tree rooted at <code>root</code>. The returned
1200
     * Return a graphical representation of the tree rooted at <code>root</code>. The returned
1201
     * string is akin to the result of a query :
1201
     * string is akin to the result of a query :
1202
     * 
1202
     * 
1203
     * <pre>
1203
     * <pre>
1204
     * BATIMENT[2]      LOCAL[5]         CPI_BT[5]        
1204
     * BATIMENT[2]      LOCAL[5]         CPI_BT[5]        
1205
     *                  LOCAL[3]         
1205
     *                  LOCAL[3]         
1206
     *                  LOCAL[2]         CPI_BT[3]        
1206
     *                  LOCAL[2]         CPI_BT[3]        
1207
     *                                   CPI_BT[2]
1207
     *                                   CPI_BT[2]
1208
     * </pre>
1208
     * </pre>
1209
     * 
1209
     * 
1210
     * In the above example, the BATIMENT has 3 LOCAL. LOCAL[3] is empty and LOCAL[2] has 2 CPI.
1210
     * In the above example, the BATIMENT has 3 LOCAL. LOCAL[3] is empty and LOCAL[2] has 2 CPI.
1211
     * 
1211
     * 
1212
     * @param root the root of the tree to print, eg BATIMENT[2].
1212
     * @param root the root of the tree to print, eg BATIMENT[2].
1213
     * @param cellLength the length of each cell.
1213
     * @param cellLength the length of each cell.
1214
     * @return a string representing the tree.
1214
     * @return a string representing the tree.
1215
     */
1215
     */
1216
    public final String printTree(final SQLRowValues root, int cellLength) {
1216
    public final String printTree(final SQLRowValues root, int cellLength) {
1217
        this.containsCheck(root);
1217
        this.containsCheck(root);
1218
        final Map<SQLRowValues, Integer> ys = new IdentityHashMap<SQLRowValues, Integer>();
1218
        final Map<SQLRowValues, Integer> ys = new IdentityHashMap<SQLRowValues, Integer>();
1219
        final AtomicInteger currentY = new AtomicInteger(0);
1219
        final AtomicInteger currentY = new AtomicInteger(0);
1220
        final Matrix<SQLRowValues> matrix = new Matrix<SQLRowValues>();
1220
        final Matrix<SQLRowValues> matrix = new Matrix<SQLRowValues>();
1221
        this.walk(root, null, new Closure<State<Object>>() {
1221
        this.walk(root, null, new Closure<State<Object>>() {
1222
            @Override
1222
            @Override
1223
            public void executeChecked(State<Object> input) {
1223
            public void executeChecked(State<Object> input) {
1224
                // x is easy : it's the length of the path
1224
                // x is easy : it's the length of the path
1225
                // y : for each rowValues we go up the tree and set the y (if not already set)
1225
                // y : for each rowValues we go up the tree and set the y (if not already set)
1226
                // then y is either this value or the current line.
1226
                // then y is either this value or the current line.
1227
                final SQLRowValues r = input.getCurrent();
1227
                final SQLRowValues r = input.getCurrent();
1228
                final int y;
1228
                final int y;
1229
                if (ys.containsKey(r))
1229
                if (ys.containsKey(r))
1230
                    y = ys.get(r);
1230
                    y = ys.get(r);
1231
                else
1231
                else
1232
                    y = currentY.getAndIncrement();
1232
                    y = currentY.getAndIncrement();
1233
                matrix.put(input.getPath().length(), y, input.getCurrent());
1233
                matrix.put(input.getPath().length(), y, input.getCurrent());
1234
 
1234
 
1235
                final SQLRowValues ancestor = input.getPrevious();
1235
                final SQLRowValues ancestor = input.getPrevious();
1236
                if (ancestor != null) {
1236
                if (ancestor != null) {
1237
                    ancestor.walkGraph(null, new Closure<State<Object>>() {
1237
                    ancestor.walkGraph(null, new Closure<State<Object>>() {
1238
                        @Override
1238
                        @Override
1239
                        public void executeChecked(State<Object> input) {
1239
                        public void executeChecked(State<Object> input) {
1240
                            final SQLRowValues ancestorRow = input.getCurrent();
1240
                            final SQLRowValues ancestorRow = input.getCurrent();
1241
                            if (!ys.containsKey(ancestorRow))
1241
                            if (!ys.containsKey(ancestorRow))
1242
                                ys.put(ancestorRow, y);
1242
                                ys.put(ancestorRow, y);
1243
                            else
1243
                            else
1244
                                throw new StopRecurseException();
1244
                                throw new StopRecurseException();
1245
                        }
1245
                        }
1246
                    });
1246
                    });
1247
                }
1247
                }
1248
            }
1248
            }
1249
        }, RecursionType.DEPTH_FIRST, Direction.REFERENT);
1249
        }, RecursionType.DEPTH_FIRST, Direction.REFERENT);
1250
 
1250
 
1251
        return matrix.print(cellLength, new ITransformer<SQLRowValues, String>() {
1251
        return matrix.print(cellLength, new ITransformer<SQLRowValues, String>() {
1252
            @Override
1252
            @Override
1253
            public String transformChecked(SQLRowValues input) {
1253
            public String transformChecked(SQLRowValues input) {
1254
                if (input == null)
1254
                if (input == null)
1255
                    return "";
1255
                    return "";
1256
                else if (input.hasID())
1256
                else if (input.hasID())
1257
                    // avoid requests
1257
                    // avoid requests
1258
                    return input.asRow().simpleToString();
1258
                    return input.asRow().simpleToString();
1259
                else
1259
                else
1260
                    return input.getTable().toString();
1260
                    return input.getTable().toString();
1261
            }
1261
            }
1262
        });
1262
        });
1263
    }
1263
    }
1264
 
1264
 
1265
    public final String printNodes() {
1265
    public final String printNodes() {
1266
        final StringBuilder sb = new StringBuilder(this.getClass().getSimpleName() + " of " + size() + " nodes:\n");
1266
        final StringBuilder sb = new StringBuilder(this.getClass().getSimpleName() + " of " + size() + " nodes:\n");
1267
        for (final SQLRowValues n : getItems()) {
1267
        for (final SQLRowValues n : getItems()) {
1268
            StringUtils.appendFixedWidthString(sb, String.valueOf(System.identityHashCode(n)), 12, Side.LEFT, ' ', true);
1268
            StringUtils.appendFixedWidthString(sb, String.valueOf(System.identityHashCode(n)), 12, Side.LEFT, ' ', true);
1269
            sb.append(' ');
1269
            sb.append(' ');
1270
            sb.append(n.getTable());
1270
            sb.append(n.getTable());
1271
            sb.append('\t');
1271
            sb.append('\t');
1272
            for (final Map.Entry<String, SQLRowValues> e : n.getForeigns().entrySet()) {
1272
            for (final Map.Entry<String, SQLRowValues> e : n.getForeigns().entrySet()) {
1273
                sb.append(e.getKey());
1273
                sb.append(e.getKey());
1274
                sb.append(" -> ");
1274
                sb.append(" -> ");
1275
                sb.append(System.identityHashCode(e.getValue()));
1275
                sb.append(System.identityHashCode(e.getValue()));
1276
                sb.append(" ; ");
1276
                sb.append(" ; ");
1277
            }
1277
            }
1278
            sb.append(new SQLRowValues(n, ForeignCopyMode.NO_COPY));
1278
            sb.append(new SQLRowValues(n, ForeignCopyMode.NO_COPY));
1279
            sb.append("\n");
1279
            sb.append("\n");
1280
        }
1280
        }
1281
        return sb.toString();
1281
        return sb.toString();
1282
    }
1282
    }
1283
 
1283
 
1284
    @Immutable
1284
    @Immutable
1285
    public static final class DiffResult {
1285
    public static final class DiffResult {
1286
 
1286
 
1287
        private final String firstDiff;
1287
        private final String firstDiff;
1288
        private final SQLRowValues vals, otherVals;
1288
        private final SQLRowValues vals, otherVals;
1289
        // null if difference is trivial
1289
        // null if difference is trivial
1290
        private final IndexedRows thisRows, otherRows;
1290
        private final IndexedRows thisRows, otherRows;
1291
 
1291
 
1292
        private DiffResult(final String firstDiff, final SQLRowValues vals, final SQLRowValues otherVals) {
1292
        private DiffResult(final String firstDiff, final SQLRowValues vals, final SQLRowValues otherVals) {
1293
            this.firstDiff = firstDiff;
1293
            this.firstDiff = firstDiff;
1294
            this.thisRows = null;
1294
            this.thisRows = null;
1295
            this.otherRows = null;
1295
            this.otherRows = null;
1296
            this.vals = vals;
1296
            this.vals = vals;
1297
            this.otherVals = otherVals;
1297
            this.otherVals = otherVals;
1298
        }
1298
        }
1299
 
1299
 
1300
        private DiffResult(final String firstDiff, final IndexedRows thisRows, final IndexedRows otherRows) {
1300
        private DiffResult(final String firstDiff, final IndexedRows thisRows, final IndexedRows otherRows) {
1301
            super();
1301
            super();
1302
            this.firstDiff = firstDiff;
1302
            this.firstDiff = firstDiff;
1303
            this.thisRows = thisRows;
1303
            this.thisRows = thisRows;
1304
            this.otherRows = otherRows;
1304
            this.otherRows = otherRows;
1305
            assert !this.isEqual() || this.getRows1().getSize() == this.getRows2().getSize();
1305
            assert !this.isEqual() || this.getRows1().getSize() == this.getRows2().getSize();
1306
            this.vals = thisRows.getRow(0);
1306
            this.vals = thisRows.getRow(0);
1307
            this.otherVals = otherRows.getRow(0);
1307
            this.otherVals = otherRows.getRow(0);
1308
        }
1308
        }
1309
 
1309
 
1310
        public String getFirstDifference() {
1310
        public String getFirstDifference() {
1311
            return this.firstDiff;
1311
            return this.firstDiff;
1312
        }
1312
        }
1313
 
1313
 
1314
        public final boolean isEqual() {
1314
        public final boolean isEqual() {
1315
            return this.getFirstDifference() == null;
1315
            return this.getFirstDifference() == null;
1316
        }
1316
        }
1317
 
1317
 
1318
        public SQLRowValues getRow1() {
1318
        public SQLRowValues getRow1() {
1319
            return this.vals;
1319
            return this.vals;
1320
        }
1320
        }
1321
 
1321
 
1322
        public IndexedRows getRows1() {
1322
        public IndexedRows getRows1() {
1323
            return this.thisRows;
1323
            return this.thisRows;
1324
        }
1324
        }
1325
 
1325
 
1326
        public SQLRowValues getRow2() {
1326
        public SQLRowValues getRow2() {
1327
            return this.otherVals;
1327
            return this.otherVals;
1328
        }
1328
        }
1329
 
1329
 
1330
        public IndexedRows getRows2() {
1330
        public IndexedRows getRows2() {
1331
            return this.otherRows;
1331
            return this.otherRows;
1332
        }
1332
        }
1333
 
1333
 
1334
        public final void fillRowMap(final Map<SQLRow, SQLRow> m, final boolean fromFirst) {
1334
        public final void fillRowMap(final Map<SQLRow, SQLRow> m, final boolean fromFirst) {
1335
            if (!this.isEqual())
1335
            if (!this.isEqual())
1336
                throw new IllegalStateException("Rows are not equal : " + this.getFirstDifference());
1336
                throw new IllegalStateException("Rows are not equal : " + this.getFirstDifference());
1337
            final int stop = this.getRows1().getSize();
1337
            final int stop = this.getRows1().getSize();
1338
            final IndexedRows i1, i2;
1338
            final IndexedRows i1, i2;
1339
            if (fromFirst) {
1339
            if (fromFirst) {
1340
                i1 = this.getRows1();
1340
                i1 = this.getRows1();
1341
                i2 = this.getRows2();
1341
                i2 = this.getRows2();
1342
            } else {
1342
            } else {
1343
                i1 = this.getRows2();
1343
                i1 = this.getRows2();
1344
                i2 = this.getRows1();
1344
                i2 = this.getRows1();
1345
            }
1345
            }
1346
            for (int i = 0; i < stop; i++) {
1346
            for (int i = 0; i < stop; i++) {
1347
                final SQLRow key = i1.getRow(i).asRow();
1347
                final SQLRow key = i1.getRow(i).asRow();
1348
                final SQLRow value = i2.getRow(i).asRow();
1348
                final SQLRow value = i2.getRow(i).asRow();
1349
                final SQLRow prev = m.put(key, value);
1349
                final SQLRow prev = m.put(key, value);
1350
                if (prev != null)
1350
                if (prev != null)
1351
                    throw new IllegalStateException(key + " already encountered in this : " + this.getRow1());
1351
                    throw new IllegalStateException(key + " already encountered in this : " + this.getRow1());
1352
            }
1352
            }
1353
        }
1353
        }
1354
    }
1354
    }
1355
 
1355
 
1356
    private static final class DiffResultBuilder {
1356
    private static final class DiffResultBuilder {
1357
        private final SQLRowValues vals, other;
1357
        private final SQLRowValues vals, other;
1358
        private IndexedRows valsRows, otherRows;
1358
        private IndexedRows valsRows, otherRows;
1359
 
1359
 
1360
        private DiffResultBuilder(SQLRowValues vals, SQLRowValues other) {
1360
        private DiffResultBuilder(SQLRowValues vals, SQLRowValues other) {
1361
            super();
1361
            super();
1362
            this.vals = vals;
1362
            this.vals = vals;
1363
            this.other = other;
1363
            this.other = other;
1364
        }
1364
        }
1365
 
1365
 
1366
        private void setRows(IndexedRows thisRows, IndexedRows otherRows) {
1366
        private void setRows(IndexedRows thisRows, IndexedRows otherRows) {
1367
            assert thisRows.getRow(0) == this.vals;
1367
            assert thisRows.getRow(0) == this.vals;
1368
            assert otherRows.getRow(0) == this.other;
1368
            assert otherRows.getRow(0) == this.other;
1369
            this.valsRows = thisRows;
1369
            this.valsRows = thisRows;
1370
            this.otherRows = otherRows;
1370
            this.otherRows = otherRows;
1371
        }
1371
        }
1372
 
1372
 
1373
        private DiffResult build(final String firstDiff) {
1373
        private DiffResult build(final String firstDiff) {
1374
            if (this.valsRows != null && this.otherRows != null) {
1374
            if (this.valsRows != null && this.otherRows != null) {
1375
                return new DiffResult(firstDiff, this.valsRows, this.otherRows);
1375
                return new DiffResult(firstDiff, this.valsRows, this.otherRows);
1376
            } else if (firstDiff == null) {
1376
            } else if (firstDiff == null) {
1377
                // if rows are equal but there's no IndexedRows, then the graphs have only one row
1377
                // if rows are equal but there's no IndexedRows, then the graphs have only one row
1378
                return new DiffResult(firstDiff, new IndexedRows(this.vals), new IndexedRows(this.other));
1378
                return new DiffResult(firstDiff, new IndexedRows(this.vals), new IndexedRows(this.other));
1379
            } else {
1379
            } else {
1380
                return new DiffResult(firstDiff, this.vals, this.other);
1380
                return new DiffResult(firstDiff, this.vals, this.other);
1381
            }
1381
            }
1382
        }
1382
        }
1383
    }
1383
    }
1384
 
1384
 
1385
    public final DiffResult getFirstDifference(final SQLRowValues vals, final SQLRowValues other, final boolean useForeignsOrder, final boolean useFieldsOrder, final boolean usePK) {
1385
    public final DiffResult getFirstDifference(final SQLRowValues vals, final SQLRowValues other, final boolean useForeignsOrder, final boolean useFieldsOrder, final boolean usePK) {
1386
        this.containsCheck(vals);
1386
        this.containsCheck(vals);
1387
        final DiffResultBuilder b = new DiffResultBuilder(vals, other);
1387
        final DiffResultBuilder b = new DiffResultBuilder(vals, other);
1388
        if (vals == other)
1388
        if (vals == other)
1389
            return b.build(null);
1389
            return b.build(null);
1390
        final int size = this.size();
1390
        final int size = this.size();
1391
        // don't call walk() if we can avoid as it is quite costly
1391
        // don't call walk() if we can avoid as it is quite costly
1392
        if (size != other.getGraph().size())
1392
        if (size != other.getGraph().size())
1393
            return b.build("different size : " + size + " != " + other.getGraph().size());
1393
            return b.build("different size : " + size + " != " + other.getGraph().size());
1394
        else if (!vals.equalsJustThis(other, useFieldsOrder, false, usePK))
1394
        else if (!vals.equalsJustThis(other, useFieldsOrder, false, usePK))
1395
            return b.build("unequal :\n" + vals + " !=\n" + other);
1395
            return b.build("unequal :\n" + vals + " !=\n" + other);
1396
        if (size == 1)
1396
        if (size == 1)
1397
            return b.build(null);
1397
            return b.build(null);
1398
 
1398
 
1399
        // BREADTH_FIRST no need to go deep if the first values are not equals
1399
        // BREADTH_FIRST no need to go deep if the first values are not equals
1400
        final IndexedRows thisRows = this.getIndexedRows(vals, RecursionType.BREADTH_FIRST, useForeignsOrder);
1400
        final IndexedRows thisRows = this.getIndexedRows(vals, RecursionType.BREADTH_FIRST, useForeignsOrder);
1401
        final IndexedRows otherRows = other.getGraph().getIndexedRows(other, RecursionType.BREADTH_FIRST, useForeignsOrder);
1401
        final IndexedRows otherRows = other.getGraph().getIndexedRows(other, RecursionType.BREADTH_FIRST, useForeignsOrder);
1402
        b.setRows(thisRows, otherRows);
1402
        b.setRows(thisRows, otherRows);
1403
 
1403
 
1404
        // now check that each row is equal
1404
        // now check that each row is equal
1405
        // (this works because walk() always goes with the same order, see #FIELD_COMPARATOR and
1405
        // (this works because walk() always goes with the same order, see #FIELD_COMPARATOR and
1406
        // WalkOptions.setForeignsOrderIgnored())
1406
        // WalkOptions.setForeignsOrderIgnored())
1407
        for (int i = 0; i < size; i++) {
1407
        for (int i = 0; i < size; i++) {
1408
            final SQLRowValues thisVals = thisRows.getRow(i);
1408
            final SQLRowValues thisVals = thisRows.getRow(i);
1409
            final SQLRowValues oVals = otherRows.getRow(i);
1409
            final SQLRowValues oVals = otherRows.getRow(i);
1410
            final Path thisPath = thisRows.getFirstPath(i);
1410
            final Path thisPath = thisRows.getFirstPath(i);
1411
            final Path oPath = otherRows.getFirstPath(i);
1411
            final Path oPath = otherRows.getFirstPath(i);
1412
 
1412
 
1413
            if (!thisPath.equals(oPath))
1413
            if (!thisPath.equals(oPath))
1414
                return b.build("unequal graph at index " + i + " " + thisPath + " != " + oPath);
1414
                return b.build("unequal graph at index " + i + " " + thisPath + " != " + oPath);
1415
 
1415
 
1416
            // no need to check again
1416
            // no need to check again
1417
            assert i != 0 || (thisVals == vals && oVals == other);
1417
            assert i != 0 || (thisVals == vals && oVals == other);
1418
            // don't use foreign ID, foreign rows are checked below
1418
            // don't use foreign ID, foreign rows are checked below
1419
            if (i != 0 && !thisVals.equalsJustThis(oVals, useFieldsOrder, false, usePK)) {
1419
            if (i != 0 && !thisVals.equalsJustThis(oVals, useFieldsOrder, false, usePK)) {
1420
                return b.build("unequal local values at " + thisPath + " :\n" + thisVals + " !=\n" + oVals);
1420
                return b.build("unequal local values at " + thisPath + " :\n" + thisVals + " !=\n" + oVals);
1421
            }
1421
            }
1422
            final Map<String, SQLRowValues> thisForeigns = thisVals.getForeigns();
1422
            final Map<String, SQLRowValues> thisForeigns = thisVals.getForeigns();
1423
            final Map<String, SQLRowValues> otherForeigns = oVals.getForeigns();
1423
            final Map<String, SQLRowValues> otherForeigns = oVals.getForeigns();
1424
            if (!thisForeigns.keySet().equals(otherForeigns.keySet()))
1424
            if (!thisForeigns.keySet().equals(otherForeigns.keySet()))
1425
                return b.build("unequal foreigns at " + thisPath + " :\n" + thisForeigns.keySet() + " !=\n" + otherForeigns.keySet());
1425
                return b.build("unequal foreigns at " + thisPath + " :\n" + thisForeigns.keySet() + " !=\n" + otherForeigns.keySet());
1426
 
1426
 
1427
            for (final Entry<String, SQLRowValues> e : thisForeigns.entrySet()) {
1427
            for (final Entry<String, SQLRowValues> e : thisForeigns.entrySet()) {
1428
                final String ff = e.getKey();
1428
                final String ff = e.getKey();
1429
                final SQLRowValues thisForeign = e.getValue();
1429
                final SQLRowValues thisForeign = e.getValue();
1430
                final SQLRowValues otherForeign = otherForeigns.get(ff);
1430
                final SQLRowValues otherForeign = otherForeigns.get(ff);
1431
                if (thisRows.getIndex(thisForeign) != otherRows.getIndex(otherForeign))
1431
                if (thisRows.getIndex(thisForeign) != otherRows.getIndex(otherForeign))
1432
                    return b.build("unequal foreign " + ff + " at " + thisPath + " for " + thisVals + " and " + oVals);
1432
                    return b.build("unequal foreign " + ff + " at " + thisPath + " for " + thisVals + " and " + oVals);
1433
                // since they point to the same index, the equality will be checked by the time this
1433
                // since they point to the same index, the equality will be checked by the time this
1434
                // loop ends
1434
                // loop ends
1435
            }
1435
            }
1436
        }
1436
        }
1437
 
1437
 
1438
        return b.build(null);
1438
        return b.build(null);
1439
    }
1439
    }
1440
 
1440
 
1441
    static public final class StopRecurseException extends RuntimeException {
1441
    static public final class StopRecurseException extends RuntimeException {
1442
 
1442
 
1443
        private boolean completely = true;
1443
        private boolean completely = true;
1444
 
1444
 
1445
        public StopRecurseException() {
1445
        public StopRecurseException() {
1446
            super();
1446
            super();
1447
        }
1447
        }
1448
 
1448
 
1449
        public StopRecurseException(String message, Throwable cause) {
1449
        public StopRecurseException(String message, Throwable cause) {
1450
            super(message, cause);
1450
            super(message, cause);
1451
        }
1451
        }
1452
 
1452
 
1453
        public StopRecurseException(String message) {
1453
        public StopRecurseException(String message) {
1454
            super(message);
1454
            super(message);
1455
        }
1455
        }
1456
 
1456
 
1457
        public StopRecurseException(Throwable cause) {
1457
        public StopRecurseException(Throwable cause) {
1458
            super(cause);
1458
            super(cause);
1459
        }
1459
        }
1460
 
1460
 
1461
        public final StopRecurseException setCompletely(boolean completely) {
1461
        public final StopRecurseException setCompletely(boolean completely) {
1462
            this.completely = completely;
1462
            this.completely = completely;
1463
            return this;
1463
            return this;
1464
        }
1464
        }
1465
 
1465
 
1466
        public final boolean isCompletely() {
1466
        public final boolean isCompletely() {
1467
            return this.completely;
1467
            return this.completely;
1468
        }
1468
        }
1469
    }
1469
    }
1470
 
1470
 
1471
    static public final class State<T> {
1471
    static public final class State<T> {
1472
        private final List<SQLRowValues> valsPath;
1472
        private final List<SQLRowValues> valsPath;
1473
        private final Path path;
1473
        private final Path path;
1474
        private T acc;
1474
        private T acc;
1475
        private final ITransformer<State<T>, T> closure;
1475
        private final ITransformer<State<T>, T> closure;
1476
 
1476
 
1477
        State(List<SQLRowValues> valsPath, Path path, T acc, ITransformer<State<T>, T> closure) {
1477
        State(List<SQLRowValues> valsPath, Path path, T acc, ITransformer<State<T>, T> closure) {
1478
            super();
1478
            super();
1479
            this.valsPath = valsPath;
1479
            this.valsPath = valsPath;
1480
            this.path = path;
1480
            this.path = path;
1481
            this.acc = acc;
1481
            this.acc = acc;
1482
            this.closure = closure;
1482
            this.closure = closure;
1483
        }
1483
        }
1484
 
1484
 
1485
        public SQLField getFrom() {
1485
        public SQLField getFrom() {
1486
            return this.path.length() == 0 ? null : this.path.getSingleField(this.path.length() - 1);
1486
            return this.path.length() == 0 ? null : this.path.getSingleField(this.path.length() - 1);
1487
        }
1487
        }
1488
 
1488
 
1489
        /**
1489
        /**
1490
         * Whether the last step of the path was taken backwards through a foreign field. Eg
1490
         * Whether the last step of the path was taken backwards through a foreign field. Eg
1491
         * <code>true</code> if the path is BATIMENT,LOCAL.ID_BATIMENT, and <code>false</code> if
1491
         * <code>true</code> if the path is BATIMENT,LOCAL.ID_BATIMENT, and <code>false</code> if
1492
         * LOCAL,LOCAL.ID_BATIMENT.
1492
         * LOCAL,LOCAL.ID_BATIMENT.
1493
         * 
1493
         * 
1494
         * @return <code>true</code> if the last step was backwards.
1494
         * @return <code>true</code> if the last step was backwards.
1495
         */
1495
         */
1496
        public final boolean isBackwards() {
1496
        public final boolean isBackwards() {
1497
            if (this.path.length() == 0)
1497
            if (this.path.length() == 0)
1498
                throw new IllegalStateException("empty path");
1498
                throw new IllegalStateException("empty path");
1499
            return this.path.isBackwards(this.path.length() - 1);
1499
            return this.path.isBackwards(this.path.length() - 1);
1500
        }
1500
        }
1501
 
1501
 
1502
        /**
1502
        /**
1503
         * Compute the new acc.
1503
         * Compute the new acc.
1504
         * 
1504
         * 
1505
         * @return whether this recursion should continue.
1505
         * @return whether this recursion should continue.
1506
         */
1506
         */
1507
        StopRecurseException compute() {
1507
        StopRecurseException compute() {
1508
            try {
1508
            try {
1509
                this.acc = this.closure.transformChecked(this);
1509
                this.acc = this.closure.transformChecked(this);
1510
                return null;
1510
                return null;
1511
            } catch (StopRecurseException e) {
1511
            } catch (StopRecurseException e) {
1512
                return e;
1512
                return e;
1513
            }
1513
            }
1514
        }
1514
        }
1515
 
1515
 
1516
        @Override
1516
        @Override
1517
        public String toString() {
1517
        public String toString() {
1518
            return this.getClass().getSimpleName() + " path: " + this.path + " current node: " + this.getCurrent() + " current acc: " + this.getAcc();
1518
            return this.getClass().getSimpleName() + " path: " + this.path + " current node: " + this.getCurrent() + " current acc: " + this.getAcc();
1519
        }
1519
        }
1520
 
1520
 
1521
        public final SQLRowValues getCurrent() {
1521
        public final SQLRowValues getCurrent() {
1522
            return CollectionUtils.getLast(this.valsPath);
1522
            return CollectionUtils.getLast(this.valsPath);
1523
        }
1523
        }
1524
 
1524
 
1525
        public final SQLRowValues getPrevious() {
1525
        public final SQLRowValues getPrevious() {
1526
            return CollectionUtils.getNoExn(this.valsPath, this.valsPath.size() - 2);
1526
            return CollectionUtils.getNoExn(this.valsPath, this.valsPath.size() - 2);
1527
        }
1527
        }
1528
 
1528
 
1529
        public final List<SQLRowValues> getValsPath() {
1529
        public final List<SQLRowValues> getValsPath() {
1530
            return this.valsPath;
1530
            return this.valsPath;
1531
        }
1531
        }
1532
 
1532
 
1533
        final boolean identityContains(final SQLRowValues vals) {
1533
        final boolean identityContains(final SQLRowValues vals) {
1534
            return CollectionUtils.identityContains(this.valsPath, vals);
1534
            return CollectionUtils.identityContains(this.valsPath, vals);
1535
        }
1535
        }
1536
 
1536
 
1537
        boolean hasCycle() {
1537
        boolean hasCycle() {
1538
            final int size = this.valsPath.size();
1538
            final int size = this.valsPath.size();
1539
            if (size < 2)
1539
            if (size < 2)
1540
                return false;
1540
                return false;
1541
            return CollectionUtils.identityContains(this.valsPath.subList(0, size - 1), this.valsPath.get(size - 1));
1541
            return CollectionUtils.identityContains(this.valsPath.subList(0, size - 1), this.valsPath.get(size - 1));
1542
        }
1542
        }
1543
 
1543
 
1544
        public Path getPath() {
1544
        public Path getPath() {
1545
            return this.path;
1545
            return this.path;
1546
        }
1546
        }
1547
 
1547
 
1548
        public T getAcc() {
1548
        public T getAcc() {
1549
            return this.acc;
1549
            return this.acc;
1550
        }
1550
        }
1551
    }
1551
    }
1552
 
1552
 
1553
    @Override
1553
    @Override
1554
    public String toString() {
1554
    public String toString() {
1555
        return this.getClass().getSimpleName() + " " + this.links;
1555
        return this.getClass().getSimpleName() + " " + this.links;
1556
    }
1556
    }
1557
 
1557
 
1558
    private static class Link {
1558
    private static class Link {
1559
        private final SQLRowValues src;
1559
        private final SQLRowValues src;
1560
        private final SQLField f;
1560
        private final SQLField f;
1561
        private final SQLRowValues dest;
1561
        private final SQLRowValues dest;
1562
 
1562
 
1563
        public Link(final SQLRowValues src) {
1563
        public Link(final SQLRowValues src) {
1564
            this(src, null, null);
1564
            this(src, null, null);
1565
        }
1565
        }
1566
 
1566
 
1567
        public Link(final SQLRowValues src, final SQLField f, final SQLRowValues dest) {
1567
        public Link(final SQLRowValues src, final SQLField f, final SQLRowValues dest) {
1568
            if (src == null)
1568
            if (src == null)
1569
                throw new NullPointerException("src is null");
1569
                throw new NullPointerException("src is null");
1570
            assert (f == null && dest == null) || (dest != null && f.getTable() == src.getTable());
1570
            assert (f == null && dest == null) || (dest != null && f.getTable() == src.getTable());
1571
            this.src = src;
1571
            this.src = src;
1572
            this.f = f;
1572
            this.f = f;
1573
            this.dest = dest;
1573
            this.dest = dest;
1574
        }
1574
        }
1575
 
1575
 
1576
        public final SQLRowValues getSrc() {
1576
        public final SQLRowValues getSrc() {
1577
            return this.src;
1577
            return this.src;
1578
        }
1578
        }
1579
 
1579
 
1580
        public final SQLRowValues getDest() {
1580
        public final SQLRowValues getDest() {
1581
            return this.dest;
1581
            return this.dest;
1582
        }
1582
        }
1583
 
1583
 
1584
        public final SQLField getField() {
1584
        public final SQLField getField() {
1585
            return this.f;
1585
            return this.f;
1586
        }
1586
        }
1587
 
1587
 
1588
        @Override
1588
        @Override
1589
        public int hashCode() {
1589
        public int hashCode() {
1590
            final int prime = 31;
1590
            final int prime = 31;
1591
            int result = 1;
1591
            int result = 1;
1592
            result = prime * result + System.identityHashCode(this.src);
1592
            result = prime * result + System.identityHashCode(this.src);
1593
            result = prime * result + System.identityHashCode(this.dest);
1593
            result = prime * result + System.identityHashCode(this.dest);
1594
            result = prime * result + ((this.f == null) ? 0 : this.f.hashCode());
1594
            result = prime * result + ((this.f == null) ? 0 : this.f.hashCode());
1595
            return result;
1595
            return result;
1596
        }
1596
        }
1597
 
1597
 
1598
        @Override
1598
        @Override
1599
        public boolean equals(Object obj) {
1599
        public boolean equals(Object obj) {
1600
            if (this == obj)
1600
            if (this == obj)
1601
                return true;
1601
                return true;
1602
            if (obj == null)
1602
            if (obj == null)
1603
                return false;
1603
                return false;
1604
            if (getClass() != obj.getClass())
1604
            if (getClass() != obj.getClass())
1605
                return false;
1605
                return false;
1606
 
1606
 
1607
            final Link other = (Link) obj;
1607
            final Link other = (Link) obj;
1608
            return this.src == other.src && this.dest == other.dest && CompareUtils.equals(this.f, other.f);
1608
            return this.src == other.src && this.dest == other.dest && CompareUtils.equals(this.f, other.f);
1609
        }
1609
        }
1610
 
1610
 
1611
        @Override
1611
        @Override
1612
        public String toString() {
1612
        public String toString() {
1613
            return this.getClass().getSimpleName() + " " + System.identityHashCode(this.src) + (this.f == null ? "" : " " + this.f.getName() + " " + System.identityHashCode(this.dest));
1613
            return this.getClass().getSimpleName() + " " + System.identityHashCode(this.src) + (this.f == null ? "" : " " + this.f.getName() + " " + System.identityHashCode(this.dest));
1614
        }
1614
        }
1615
    }
1615
    }
1616
 
1616
 
1617
    private static final class StoringLink extends Link {
1617
    private static final class StoringLink extends Link {
1618
 
1618
 
1619
        private Number destID;
1619
        private Number destID;
1620
 
1620
 
1621
        private StoringLink(Link l) {
1621
        private StoringLink(Link l) {
1622
            super(l.getSrc(), l.getField(), l.getDest());
1622
            super(l.getSrc(), l.getField(), l.getDest());
1623
            this.destID = null;
1623
            this.destID = null;
1624
        }
1624
        }
1625
 
1625
 
1626
        public final boolean canStore() {
1626
        public final boolean canStore() {
1627
            return this.getDest() == null || this.destID != null;
1627
            return this.getDest() == null || this.destID != null;
1628
        }
1628
        }
1629
 
1629
 
1630
        @Override
1630
        @Override
1631
        public String toString() {
1631
        public String toString() {
1632
            return super.toString() + " destID: " + this.destID;
1632
            return super.toString() + " destID: " + this.destID;
1633
        }
1633
        }
1634
    }
1634
    }
1635
 
1635
 
1636
    public static final class StoreResult {
1636
    public static final class StoreResult {
1637
        private final Map<SQLRowValues, Node> nodes;
1637
        private final Map<SQLRowValues, Node> nodes;
1638
 
1638
 
1639
        public StoreResult(final Map<SQLRowValues, Node> nodes) {
1639
        public StoreResult(final Map<SQLRowValues, Node> nodes) {
1640
            this.nodes = nodes;
1640
            this.nodes = nodes;
1641
        }
1641
        }
1642
 
1642
 
1643
        final SQLRowValues getRowValuesFor(final SQLRow stored) {
1643
        final SQLRowValues getRowValuesFor(final SQLRow stored) {
1644
            for (final Entry<SQLRowValues, Node> e : this.nodes.entrySet()) {
1644
            for (final Entry<SQLRowValues, Node> e : this.nodes.entrySet()) {
1645
                if (e.getValue().getStoredRow().equals(stored))
1645
                if (e.getValue().getStoredRow().equals(stored))
1646
                    return e.getKey();
1646
                    return e.getKey();
1647
            }
1647
            }
1648
            return null;
1648
            return null;
1649
        }
1649
        }
1650
 
1650
 
1651
        /**
1651
        /**
1652
         * Get the set of rows at the time the {@link SQLRowValuesCluster#store(StoreMode)} was
1652
         * Get the set of rows at the time the {@link SQLRowValuesCluster#store(StoreMode)} was
1653
         * performed. NOTE : the field values of the returned rows might have changed since the
1653
         * performed. NOTE : the field values of the returned rows might have changed since the
1654
         * store but the other methods of this class use the instance identity.
1654
         * store but the other methods of this class use the instance identity.
1655
         * 
1655
         * 
1656
         * @return the rows that were stored.
1656
         * @return the rows that were stored.
1657
         */
1657
         */
1658
        final Set<SQLRowValues> getRowValues() {
1658
        final Set<SQLRowValues> getRowValues() {
1659
            return Collections.unmodifiableSet(this.nodes.keySet());
1659
            return Collections.unmodifiableSet(this.nodes.keySet());
1660
        }
1660
        }
1661
 
1661
 
1662
        public final int getStoredCount() {
1662
        public final int getStoredCount() {
1663
            return this.nodes.size();
1663
            return this.nodes.size();
1664
        }
1664
        }
1665
 
1665
 
1666
        public final SQLRow getStoredRow(SQLRowValues vals) {
1666
        public final SQLRow getStoredRow(SQLRowValues vals) {
1667
            return this.nodes.get(vals).getStoredRow();
1667
            return this.nodes.get(vals).getStoredRow();
1668
        }
1668
        }
1669
 
1669
 
1670
        public final SQLRowValues getStoredValues(SQLRowValues vals) {
1670
        public final SQLRowValues getStoredValues(SQLRowValues vals) {
1671
            return this.nodes.get(vals).getStoredValues();
1671
            return this.nodes.get(vals).getStoredValues();
1672
        }
1672
        }
1673
 
1673
 
1674
        final List<SQLTableEvent> getEvents(SQLRowValues vals) {
1674
        final List<SQLTableEvent> getEvents(SQLRowValues vals) {
1675
            return this.nodes.get(vals).getEvents();
1675
            return this.nodes.get(vals).getEvents();
1676
        }
1676
        }
1677
    }
1677
    }
1678
 
1678
 
1679
    private static final class Node {
1679
    private static final class Node {
1680
 
1680
 
1681
        // don't use noLink since it might contains foreigns if store() was just called
1681
        // don't use noLink since it might contains foreigns if store() was just called
1682
        // or it might be out of sync with vals since the graph is only recreated on foreign change
1682
        // or it might be out of sync with vals since the graph is only recreated on foreign change
1683
        /** vals without any links */
1683
        /** vals without any links */
1684
        private final SQLRowValues noLink;
1684
        private final SQLRowValues noLink;
1685
        private final List<SQLTableEvent> modif;
1685
        private final List<SQLTableEvent> modif;
1686
 
1686
 
1687
        private Node(final SQLRowValues vals) {
1687
        private Node(final SQLRowValues vals) {
1688
            this.modif = new ArrayList<SQLTableEvent>();
1688
            this.modif = new ArrayList<SQLTableEvent>();
1689
            this.noLink = new SQLRowValues(vals, ForeignCopyMode.NO_COPY);
1689
            this.noLink = new SQLRowValues(vals, ForeignCopyMode.NO_COPY);
1690
        }
1690
        }
1691
 
1691
 
1692
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode) throws SQLException {
1692
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode) throws SQLException {
1693
            return this.store(fetchStoredRow, mode, true);
1693
            return this.store(fetchStoredRow, mode, true);
1694
        }
1694
        }
1695
 
1695
 
1696
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode, final boolean setRowValues) throws SQLException {
1696
        private SQLTableEvent store(final boolean fetchStoredRow, StoreMode mode, final boolean setRowValues) throws SQLException {
1697
            assert !this.isStored();
1697
            assert !this.isStored();
1698
            final SQLTableEvent evt = this.addEvent(mode.execOn(this.noLink, fetchStoredRow));
1698
            final SQLTableEvent evt = this.addEvent(mode.execOn(this.noLink, fetchStoredRow));
1699
            if (fetchStoredRow && evt.getRow() != null && setRowValues)
1699
            if (fetchStoredRow && evt.getRow() != null && setRowValues)
1700
                evt.setRowValues(evt.getRow().asRowValues());
1700
                evt.setRowValues(evt.getRow().asRowValues());
1701
            return evt;
1701
            return evt;
1702
        }
1702
        }
1703
 
1703
 
1704
        private SQLTableEvent update(final boolean fetchStoredRow) throws SQLException {
1704
        private SQLTableEvent update(final boolean fetchStoredRow) throws SQLException {
1705
            assert this.isStored();
1705
            assert this.isStored();
1706
 
1706
 
1707
            // fields that have been updated since last store
1707
            // fields that have been updated since last store
1708
            final Set<String> fieldsToUpdate = new HashSet<String>(this.noLink.getFields());
1708
            final Set<String> fieldsToUpdate = new HashSet<String>(this.noLink.getFields());
1709
            fieldsToUpdate.removeAll(this.getEvent().getFieldNames());
1709
            fieldsToUpdate.removeAll(this.getEvent().getFieldNames());
1710
            assert fieldsToUpdate.size() > 0;
1710
            assert fieldsToUpdate.size() > 0;
1711
 
1711
 
1712
            final SQLRowValues updatingVals = this.getStoredRow().createEmptyUpdateRow();
1712
            final SQLRowValues updatingVals = this.getStoredRow().createEmptyUpdateRow();
1713
            updatingVals.load(this.noLink, fieldsToUpdate);
1713
            updatingVals.load(this.noLink, fieldsToUpdate);
1714
 
1714
 
1715
            final SQLTableEvent evt = new Node(updatingVals).store(fetchStoredRow, StoreMode.COMMIT, false);
1715
            final SQLTableEvent evt = new Node(updatingVals).store(fetchStoredRow, StoreMode.COMMIT, false);
1716
            // Update previous rowValues, and use it for the new event
1716
            // Update previous rowValues, and use it for the new event
1717
            // that way there's only one graph of rowValues (with the final values) for all events.
1717
            // that way there's only one graph of rowValues (with the final values) for all events.
1718
            // Load all fields since updating 1 field might change the value of another (e.g.
1718
            // Load all fields since updating 1 field might change the value of another (e.g.
1719
            // with a trigger).
1719
            // with a trigger).
1720
            if (fetchStoredRow && evt.getRow() != null) {
1720
            if (fetchStoredRow && evt.getRow() != null) {
1721
                this.getStoredValues().load(evt.getRow(), null);
1721
                this.getStoredValues().load(evt.getRow(), null);
1722
                evt.setRowValues(this.getStoredValues());
1722
                evt.setRowValues(this.getStoredValues());
1723
            }
1723
            }
1724
            return this.addEvent(evt);
1724
            return this.addEvent(evt);
1725
        }
1725
        }
1726
 
1726
 
1727
        public final boolean isStored() {
1727
        public final boolean isStored() {
1728
            return this.modif.size() > 0;
1728
            return this.modif.size() > 0;
1729
        }
1729
        }
1730
 
1730
 
1731
        // the last stored row, can be null for non-rowable tables
1731
        // the last stored row, can be null for non-rowable tables
1732
        public final SQLRow getStoredRow() {
1732
        public final SQLRow getStoredRow() {
1733
            return this.getEvent() == null ? null : this.getEvent().getRow();
1733
            return this.getEvent() == null ? null : this.getEvent().getRow();
1734
        }
1734
        }
1735
 
1735
 
1736
        public final SQLRowValues getStoredValues() {
1736
        public final SQLRowValues getStoredValues() {
1737
            // all events have the same values
1737
            // all events have the same values
1738
            return this.getEvent() == null ? null : this.getEvent().getRowValues();
1738
            return this.getEvent() == null ? null : this.getEvent().getRowValues();
1739
        }
1739
        }
1740
 
1740
 
1741
        // the last event
1741
        // the last event
1742
        private final SQLTableEvent getEvent() {
1742
        private final SQLTableEvent getEvent() {
1743
            return CollectionUtils.getLast(this.modif);
1743
            return CollectionUtils.getLast(this.modif);
1744
        }
1744
        }
1745
 
1745
 
1746
        final List<SQLTableEvent> getEvents() {
1746
        final List<SQLTableEvent> getEvents() {
1747
            return Collections.unmodifiableList(this.modif);
1747
            return Collections.unmodifiableList(this.modif);
1748
        }
1748
        }
1749
 
1749
 
1750
        private final SQLTableEvent addEvent(SQLTableEvent evt) {
1750
        private final SQLTableEvent addEvent(SQLTableEvent evt) {
1751
            if (evt == null)
1751
            if (evt == null)
1752
                throw new IllegalStateException("Couldn't update missing row  " + this.noLink);
1752
                throw new IllegalStateException("Couldn't update missing row  " + this.noLink);
1753
            this.modif.add(evt);
1753
            this.modif.add(evt);
1754
            return evt;
1754
            return evt;
1755
        }
1755
        }
1756
 
1756
 
1757
        @Override
1757
        @Override
1758
        public String toString() {
1758
        public String toString() {
1759
            return this.getClass().getSimpleName() + " " + this.noLink;
1759
            return this.getClass().getSimpleName() + " " + this.noLink;
1760
        }
1760
        }
1761
    }
1761
    }
1762
 
1762
 
1763
    /**
1763
    /**
1764
     * What to do on each rowVals.
1764
     * What to do on each rowVals.
1765
     * 
1765
     * 
1766
     * @author Sylvain
1766
     * @author Sylvain
1767
     */
1767
     */
1768
    public static abstract class StoreMode {
1768
    public static abstract class StoreMode {
1769
        abstract SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException;
1769
        abstract SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException;
1770
 
1770
 
1771
        public static final StoreMode COMMIT = new Commit();
1771
        public static final StoreMode COMMIT = new Commit();
1772
        public static final StoreMode INSERT = new Insert(false, false);
1772
        public static final StoreMode INSERT = new Insert(false, false);
1773
        public static final StoreMode INSERT_VERBATIM = new Insert(true, true);
1773
        public static final StoreMode INSERT_VERBATIM = new Insert(true, true);
1774
    }
1774
    }
1775
 
1775
 
1776
    public static class Insert extends StoreMode {
1776
    public static class Insert extends StoreMode {
1777
 
1777
 
1778
        private final boolean insertPK;
1778
        private final boolean insertPK;
1779
        private final boolean insertOrder;
1779
        private final boolean insertOrder;
1780
 
1780
 
1781
        public Insert(boolean insertPK, boolean insertOrder) {
1781
        public Insert(boolean insertPK, boolean insertOrder) {
1782
            super();
1782
            super();
1783
            this.insertPK = insertPK;
1783
            this.insertPK = insertPK;
1784
            this.insertOrder = insertOrder;
1784
            this.insertOrder = insertOrder;
1785
        }
1785
        }
1786
 
1786
 
1787
        @Override
1787
        @Override
1788
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
1788
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
1789
            final Set<SQLField> autoFields = new HashSet<SQLField>();
1789
            final Set<SQLField> autoFields = new HashSet<SQLField>();
1790
            if (!this.insertPK)
1790
            if (!this.insertPK)
1791
                autoFields.addAll(vals.getTable().getPrimaryKeys());
1791
                autoFields.addAll(vals.getTable().getPrimaryKeys());
1792
            if (!this.insertOrder)
1792
            if (!this.insertOrder)
1793
                autoFields.add(vals.getTable().getOrderField());
1793
                autoFields.add(vals.getTable().getOrderField());
1794
            return vals.insertJustThis(fetchStoredRow, autoFields);
1794
            return vals.insertJustThis(fetchStoredRow, autoFields);
1795
        }
1795
        }
1796
    }
1796
    }
1797
 
1797
 
1798
    public static class Commit extends StoreMode {
1798
    public static class Commit extends StoreMode {
1799
 
1799
 
1800
        @Override
1800
        @Override
1801
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
1801
        SQLTableEvent execOn(SQLRowValues vals, final boolean fetchStoredRow) throws SQLException {
1802
            return vals.commitJustThis(fetchStoredRow);
1802
            return vals.commitJustThis(fetchStoredRow);
1803
        }
1803
        }
1804
    }
1804
    }
1805
 
1805
 
1806
    // * listeners
1806
    // * listeners
1807
 
1807
 
1808
    // create if necessary
1808
    // create if necessary
1809
    private final Map<SQLRowValues, List<ValueChangeListener>> getListeners() {
1809
    private final Map<SQLRowValues, List<ValueChangeListener>> getListeners() {
1810
        if (this.listeners == null)
1810
        if (this.listeners == null)
1811
            this.listeners = new IdentityHashMap<SQLRowValues, List<ValueChangeListener>>(4);
1811
            this.listeners = new IdentityHashMap<SQLRowValues, List<ValueChangeListener>>(4);
1812
        return this.listeners;
1812
        return this.listeners;
1813
    }
1813
    }
1814
 
1814
 
1815
    final void addValueListener(final SQLRowValues vals, ValueChangeListener l) {
1815
    final void addValueListener(final SQLRowValues vals, ValueChangeListener l) {
1816
        this.containsCheck(vals);
1816
        this.containsCheck(vals);
1817
        List<ValueChangeListener> list = this.getListeners().get(vals);
1817
        List<ValueChangeListener> list = this.getListeners().get(vals);
1818
        if (list == null) {
1818
        if (list == null) {
1819
            list = new ArrayList<ValueChangeListener>();
1819
            list = new ArrayList<ValueChangeListener>();
1820
            this.getListeners().put(vals, list);
1820
            this.getListeners().put(vals, list);
1821
        }
1821
        }
1822
        list.add(l);
1822
        list.add(l);
1823
    }
1823
    }
1824
 
1824
 
1825
    final void removeValueListener(final SQLRowValues vals, ValueChangeListener l) {
1825
    final void removeValueListener(final SQLRowValues vals, ValueChangeListener l) {
1826
        if (this.listeners != null && this.listeners.containsKey(vals)) {
1826
        if (this.listeners != null && this.listeners.containsKey(vals)) {
1827
            final List<ValueChangeListener> list = this.listeners.get(vals);
1827
            final List<ValueChangeListener> list = this.listeners.get(vals);
1828
            list.remove(l);
1828
            list.remove(l);
1829
            // never leave an empty list so that hasListeners() works
1829
            // never leave an empty list so that hasListeners() works
1830
            if (list.size() == 0)
1830
            if (list.size() == 0)
1831
                this.listeners.remove(vals);
1831
                this.listeners.remove(vals);
1832
        }
1832
        }
1833
    }
1833
    }
1834
 
1834
 
1835
    final void fireModification(SQLRowValues vals, String fieldName, Object newValue) {
1835
    final void fireModification(SQLRowValues vals, String fieldName, Object newValue) {
1836
        if (hasListeners())
1836
        if (hasListeners())
1837
            fireModification(new ValueChangeEvent(vals, fieldName, newValue));
1837
            fireModification(new ValueChangeEvent(vals, fieldName, newValue));
1838
    }
1838
    }
1839
 
1839
 
1840
    final void fireModification(SQLRowValues vals, Map<String, ?> m) {
1840
    final void fireModification(SQLRowValues vals, Map<String, ?> m) {
1841
        if (hasListeners())
1841
        if (hasListeners())
1842
            fireModification(new ValueChangeEvent(vals, m));
1842
            fireModification(new ValueChangeEvent(vals, m));
1843
    }
1843
    }
1844
 
1844
 
1845
    final void fireModification(SQLRowValues vals, Set<String> removed) {
1845
    final void fireModification(SQLRowValues vals, Set<String> removed) {
1846
        if (hasListeners())
1846
        if (hasListeners())
1847
            fireModification(new ValueChangeEvent(vals, removed));
1847
            fireModification(new ValueChangeEvent(vals, removed));
1848
    }
1848
    }
1849
 
1849
 
1850
    private final void fireModification(final ValueChangeEvent evt) {
1850
    private final void fireModification(final ValueChangeEvent evt) {
1851
        for (final List<ValueChangeListener> list : this.listeners.values())
1851
        for (final List<ValueChangeListener> list : this.listeners.values())
1852
            for (ValueChangeListener l : list)
1852
            for (ValueChangeListener l : list)
1853
                l.valueChange(evt);
1853
                l.valueChange(evt);
1854
    }
1854
    }
1855
 
1855
 
1856
    final void fireModification(final ReferentChangeEvent evt) {
1856
    final void fireModification(final ReferentChangeEvent evt) {
1857
        if (referentFireNeeded(evt.isAddition())) {
1857
        if (referentFireNeeded(evt.isAddition())) {
1858
            for (final List<ValueChangeListener> list : this.listeners.values())
1858
            for (final List<ValueChangeListener> list : this.listeners.values())
1859
                for (ValueChangeListener l : list)
1859
                for (ValueChangeListener l : list)
1860
                    l.referentChange(evt);
1860
                    l.referentChange(evt);
1861
        }
1861
        }
1862
    }
1862
    }
1863
 
1863
 
1864
    final boolean referentFireNeeded(final boolean put) {
1864
    final boolean referentFireNeeded(final boolean put) {
1865
        // if isAddition there will be a valueChange() for the same put()
1865
        // if isAddition there will be a valueChange() for the same put()
1866
        return this.hasListeners() && !put;
1866
        return this.hasListeners() && !put;
1867
    }
1867
    }
1868
 
1868
 
1869
    final boolean hasListeners() {
1869
    final boolean hasListeners() {
1870
        return this.listeners != null && this.listeners.size() > 0;
1870
        return this.listeners != null && this.listeners.size() > 0;
1871
    }
1871
    }
1872
 
1872
 
1873
    public static class ValueChangeEvent extends EventObject {
1873
    public static class ValueChangeEvent extends EventObject {
1874
 
1874
 
1875
        private final Map<String, ?> added;
1875
        private final Map<String, ?> added;
1876
        private final Set<String> removed;
1876
        private final Set<String> removed;
1877
 
1877
 
1878
        private ValueChangeEvent(final SQLRowValues vals, final Map<String, ?> m) {
1878
        private ValueChangeEvent(final SQLRowValues vals, final Map<String, ?> m) {
1879
            super(vals);
1879
            super(vals);
1880
            this.added = Collections.unmodifiableMap(m);
1880
            this.added = Collections.unmodifiableMap(m);
1881
            this.removed = Collections.emptySet();
1881
            this.removed = Collections.emptySet();
1882
        }
1882
        }
1883
 
1883
 
1884
        public ValueChangeEvent(SQLRowValues vals, String fieldName, Object newValue) {
1884
        public ValueChangeEvent(SQLRowValues vals, String fieldName, Object newValue) {
1885
            super(vals);
1885
            super(vals);
1886
            this.added = Collections.singletonMap(fieldName, newValue);
1886
            this.added = Collections.singletonMap(fieldName, newValue);
1887
            this.removed = Collections.emptySet();
1887
            this.removed = Collections.emptySet();
1888
        }
1888
        }
1889
 
1889
 
1890
        public ValueChangeEvent(SQLRowValues vals, Set<String> removed) {
1890
        public ValueChangeEvent(SQLRowValues vals, Set<String> removed) {
1891
            super(vals);
1891
            super(vals);
1892
            this.added = Collections.emptyMap();
1892
            this.added = Collections.emptyMap();
1893
            this.removed = Collections.unmodifiableSet(removed);
1893
            this.removed = Collections.unmodifiableSet(removed);
1894
        }
1894
        }
1895
 
1895
 
1896
        @Override
1896
        @Override
1897
        public SQLRowValues getSource() {
1897
        public SQLRowValues getSource() {
1898
            return (SQLRowValues) super.getSource();
1898
            return (SQLRowValues) super.getSource();
1899
        }
1899
        }
1900
 
1900
 
1901
        public final Set<String> getAddedFields() {
1901
        public final Set<String> getAddedFields() {
1902
            return this.added.keySet();
1902
            return this.added.keySet();
1903
        }
1903
        }
1904
 
1904
 
1905
        public final Set<String> getRemovedFields() {
1905
        public final Set<String> getRemovedFields() {
1906
            return this.removed;
1906
            return this.removed;
1907
        }
1907
        }
1908
 
1908
 
1909
        public final Map<String, ?> getAddedValues() {
1909
        public final Map<String, ?> getAddedValues() {
1910
            return this.added;
1910
            return this.added;
1911
        }
1911
        }
1912
 
1912
 
1913
        @Override
1913
        @Override
1914
        public String toString() {
1914
        public String toString() {
1915
            return super.toString() + " added : " + getAddedFields() + " removed: " + getRemovedFields();
1915
            return super.toString() + " added : " + getAddedFields() + " removed: " + getRemovedFields();
1916
        }
1916
        }
1917
    }
1917
    }
1918
 
1918
 
1919
    /**
1919
    /**
1920
     * This listener is notified whenever a rowValues changes. Note that
1920
     * This listener is notified whenever a rowValues changes. Note that
1921
     * {@link ValueChangeListener#referentChange(ReferentChangeEvent)} is only called for
1921
     * {@link ValueChangeListener#referentChange(ReferentChangeEvent)} is only called for
1922
     * {@link ReferentChangeEvent#isRemoval() removals} since for addition there will be a
1922
     * {@link ReferentChangeEvent#isRemoval() removals} since for addition there will be a
1923
     * {@link ValueChangeListener#valueChange(ValueChangeEvent)}.
1923
     * {@link ValueChangeListener#valueChange(ValueChangeEvent)}.
1924
     * 
1924
     * 
1925
     * @author Sylvain CUAZ
1925
     * @author Sylvain CUAZ
1926
     */
1926
     */
1927
    public static interface ValueChangeListener extends ReferentChangeListener {
1927
    public static interface ValueChangeListener extends ReferentChangeListener {
1928
 
1928
 
1929
        void valueChange(ValueChangeEvent evt);
1929
        void valueChange(ValueChangeEvent evt);
1930
 
1930
 
1931
    }
1931
    }
1932
}
1932
}