OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 174 Rev 180
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 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.element;
14
 package org.openconcerto.sql.element;
15
 
15
 
16
import static org.openconcerto.sql.TM.getTM;
16
import static org.openconcerto.sql.TM.getTM;
17
 
17
 
18
import org.openconcerto.sql.Configuration;
18
import org.openconcerto.sql.Configuration;
19
import org.openconcerto.sql.FieldExpander;
19
import org.openconcerto.sql.FieldExpander;
20
import org.openconcerto.sql.Log;
20
import org.openconcerto.sql.Log;
21
import org.openconcerto.sql.PropsConfiguration;
21
import org.openconcerto.sql.PropsConfiguration;
22
import org.openconcerto.sql.TM;
22
import org.openconcerto.sql.TM;
23
import org.openconcerto.sql.element.SQLElementLink.LinkType;
23
import org.openconcerto.sql.element.SQLElementLink.LinkType;
24
import org.openconcerto.sql.element.TreesOfSQLRows.LinkToCut;
24
import org.openconcerto.sql.element.TreesOfSQLRows.LinkToCut;
25
import org.openconcerto.sql.model.DBStructureItemNotFound;
25
import org.openconcerto.sql.model.DBStructureItemNotFound;
26
import org.openconcerto.sql.model.SQLField;
26
import org.openconcerto.sql.model.SQLField;
27
import org.openconcerto.sql.model.SQLRow;
27
import org.openconcerto.sql.model.SQLRow;
28
import org.openconcerto.sql.model.SQLRowAccessor;
28
import org.openconcerto.sql.model.SQLRowAccessor;
29
import org.openconcerto.sql.model.SQLRowMode;
29
import org.openconcerto.sql.model.SQLRowMode;
30
import org.openconcerto.sql.model.SQLRowValues;
30
import org.openconcerto.sql.model.SQLRowValues;
31
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
31
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
32
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
32
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
33
import org.openconcerto.sql.model.SQLRowValuesCluster;
33
import org.openconcerto.sql.model.SQLRowValuesCluster;
34
import org.openconcerto.sql.model.SQLRowValuesCluster.DiffResult;
34
import org.openconcerto.sql.model.SQLRowValuesCluster.DiffResult;
35
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
35
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
36
import org.openconcerto.sql.model.SQLRowValuesCluster.StopRecurseException;
36
import org.openconcerto.sql.model.SQLRowValuesCluster.StopRecurseException;
37
import org.openconcerto.sql.model.SQLRowValuesCluster.StoreMode;
37
import org.openconcerto.sql.model.SQLRowValuesCluster.StoreMode;
38
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
38
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
39
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
39
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
40
import org.openconcerto.sql.model.SQLSelect;
40
import org.openconcerto.sql.model.SQLSelect;
41
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
41
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
42
import org.openconcerto.sql.model.SQLSelect.LockStrength;
42
import org.openconcerto.sql.model.SQLSelect.LockStrength;
43
import org.openconcerto.sql.model.SQLSyntax;
43
import org.openconcerto.sql.model.SQLSyntax;
44
import org.openconcerto.sql.model.SQLTable;
44
import org.openconcerto.sql.model.SQLTable;
45
import org.openconcerto.sql.model.SQLTable.FieldGroup;
45
import org.openconcerto.sql.model.SQLTable.FieldGroup;
46
import org.openconcerto.sql.model.SQLTable.VirtualFields;
46
import org.openconcerto.sql.model.SQLTable.VirtualFields;
47
import org.openconcerto.sql.model.Where;
47
import org.openconcerto.sql.model.Where;
48
import org.openconcerto.sql.model.graph.Link;
48
import org.openconcerto.sql.model.graph.Link;
49
import org.openconcerto.sql.model.graph.Link.Direction;
49
import org.openconcerto.sql.model.graph.Link.Direction;
50
import org.openconcerto.sql.model.graph.Path;
50
import org.openconcerto.sql.model.graph.Path;
51
import org.openconcerto.sql.model.graph.PathBuilder;
51
import org.openconcerto.sql.model.graph.PathBuilder;
52
import org.openconcerto.sql.model.graph.SQLKey;
52
import org.openconcerto.sql.model.graph.SQLKey;
53
import org.openconcerto.sql.model.graph.SQLKey.Type;
53
import org.openconcerto.sql.model.graph.SQLKey.Type;
54
import org.openconcerto.sql.model.graph.Step;
54
import org.openconcerto.sql.model.graph.Step;
55
import org.openconcerto.sql.request.ComboSQLRequest;
55
import org.openconcerto.sql.request.ComboSQLRequest;
56
import org.openconcerto.sql.request.ListSQLRequest;
56
import org.openconcerto.sql.request.ListSQLRequest;
57
import org.openconcerto.sql.request.SQLCache;
57
import org.openconcerto.sql.request.SQLCache;
58
import org.openconcerto.sql.request.SQLFieldTranslator;
58
import org.openconcerto.sql.request.SQLFieldTranslator;
-
 
59
import org.openconcerto.sql.sqlobject.SQLRequestComboBox;
59
import org.openconcerto.sql.sqlobject.SQLTextCombo;
60
import org.openconcerto.sql.sqlobject.SQLTextCombo;
60
import org.openconcerto.sql.ui.light.CustomRowEditor;
61
import org.openconcerto.sql.ui.light.CustomRowEditor;
61
import org.openconcerto.sql.ui.light.GroupToLightUIConvertor;
62
import org.openconcerto.sql.ui.light.GroupToLightUIConvertor;
62
import org.openconcerto.sql.ui.light.LightEditFrame;
63
import org.openconcerto.sql.ui.light.LightEditFrame;
63
import org.openconcerto.sql.ui.light.LightUIPanelFiller;
64
import org.openconcerto.sql.ui.light.LightUIPanelFiller;
64
import org.openconcerto.sql.users.rights.UserRightsManager;
65
import org.openconcerto.sql.users.rights.UserRightsManager;
65
import org.openconcerto.sql.utils.SQLUtils;
66
import org.openconcerto.sql.utils.SQLUtils;
66
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
67
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
67
import org.openconcerto.sql.view.EditFrame;
68
import org.openconcerto.sql.view.EditFrame;
68
import org.openconcerto.sql.view.EditPanel.EditMode;
69
import org.openconcerto.sql.view.EditPanel.EditMode;
69
import org.openconcerto.sql.view.list.IListeAction;
70
import org.openconcerto.sql.view.list.IListeAction;
70
import org.openconcerto.sql.view.list.SQLTableModelColumn;
71
import org.openconcerto.sql.view.list.SQLTableModelColumn;
71
import org.openconcerto.sql.view.list.SQLTableModelColumnPath;
72
import org.openconcerto.sql.view.list.SQLTableModelColumnPath;
72
import org.openconcerto.sql.view.list.SQLTableModelSource;
73
import org.openconcerto.sql.view.list.SQLTableModelSource;
73
import org.openconcerto.sql.view.list.SQLTableModelSourceOffline;
74
import org.openconcerto.sql.view.list.SQLTableModelSourceOffline;
74
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
75
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
75
import org.openconcerto.ui.group.Group;
76
import org.openconcerto.ui.group.Group;
76
import org.openconcerto.ui.light.ComboValueConvertor;
77
import org.openconcerto.ui.light.ComboValueConvertor;
77
import org.openconcerto.ui.light.IntValueConvertor;
78
import org.openconcerto.ui.light.IntValueConvertor;
78
import org.openconcerto.ui.light.LightUIComboBox;
79
import org.openconcerto.ui.light.LightUIComboBox;
79
import org.openconcerto.ui.light.LightUIElement;
80
import org.openconcerto.ui.light.LightUIElement;
80
import org.openconcerto.ui.light.LightUIFrame;
81
import org.openconcerto.ui.light.LightUIFrame;
81
import org.openconcerto.ui.light.LightUIPanel;
82
import org.openconcerto.ui.light.LightUIPanel;
82
import org.openconcerto.ui.light.StringValueConvertor;
83
import org.openconcerto.ui.light.StringValueConvertor;
83
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
84
import org.openconcerto.utils.CollectionMap2Itf.SetMapItf;
84
import org.openconcerto.utils.CollectionUtils;
85
import org.openconcerto.utils.CollectionUtils;
85
import org.openconcerto.utils.CompareUtils;
86
import org.openconcerto.utils.CompareUtils;
86
import org.openconcerto.utils.ExceptionHandler;
87
import org.openconcerto.utils.ExceptionHandler;
87
import org.openconcerto.utils.ExceptionUtils;
88
import org.openconcerto.utils.ExceptionUtils;
88
import org.openconcerto.utils.LinkedListMap;
89
import org.openconcerto.utils.LinkedListMap;
89
import org.openconcerto.utils.ListMap;
90
import org.openconcerto.utils.ListMap;
90
import org.openconcerto.utils.NumberUtils;
91
import org.openconcerto.utils.NumberUtils;
91
import org.openconcerto.utils.RTInterruptedException;
92
import org.openconcerto.utils.RTInterruptedException;
92
import org.openconcerto.utils.RecursionType;
93
import org.openconcerto.utils.RecursionType;
93
import org.openconcerto.utils.ReflectUtils;
94
import org.openconcerto.utils.ReflectUtils;
94
import org.openconcerto.utils.SetMap;
95
import org.openconcerto.utils.SetMap;
95
import org.openconcerto.utils.Tuple2;
96
import org.openconcerto.utils.Tuple2;
96
import org.openconcerto.utils.Value;
97
import org.openconcerto.utils.Value;
97
import org.openconcerto.utils.cache.CacheResult;
98
import org.openconcerto.utils.cache.CacheResult;
98
import org.openconcerto.utils.cc.IClosure;
99
import org.openconcerto.utils.cc.IClosure;
99
import org.openconcerto.utils.cc.ITransformer;
100
import org.openconcerto.utils.cc.ITransformer;
100
import org.openconcerto.utils.cc.Transformer;
101
import org.openconcerto.utils.cc.Transformer;
101
import org.openconcerto.utils.change.ListChangeIndex;
102
import org.openconcerto.utils.change.ListChangeIndex;
102
import org.openconcerto.utils.change.ListChangeRecorder;
103
import org.openconcerto.utils.change.ListChangeRecorder;
103
import org.openconcerto.utils.i18n.Grammar;
104
import org.openconcerto.utils.i18n.Grammar;
104
import org.openconcerto.utils.i18n.Grammar_fr;
105
import org.openconcerto.utils.i18n.Grammar_fr;
105
import org.openconcerto.utils.i18n.NounClass;
106
import org.openconcerto.utils.i18n.NounClass;
106
import org.openconcerto.utils.i18n.Phrase;
107
import org.openconcerto.utils.i18n.Phrase;
107
 
108
 
108
import java.awt.Component;
109
import java.awt.Component;
109
import java.lang.reflect.Constructor;
110
import java.lang.reflect.Constructor;
110
import java.math.BigDecimal;
111
import java.math.BigDecimal;
111
import java.sql.SQLException;
112
import java.sql.SQLException;
112
import java.util.ArrayList;
113
import java.util.ArrayList;
113
import java.util.Arrays;
114
import java.util.Arrays;
114
import java.util.Collection;
115
import java.util.Collection;
115
import java.util.Collections;
116
import java.util.Collections;
116
import java.util.HashMap;
117
import java.util.HashMap;
117
import java.util.HashSet;
118
import java.util.HashSet;
118
import java.util.IdentityHashMap;
119
import java.util.IdentityHashMap;
119
import java.util.Iterator;
120
import java.util.Iterator;
120
import java.util.LinkedHashMap;
121
import java.util.LinkedHashMap;
121
import java.util.LinkedList;
122
import java.util.LinkedList;
122
import java.util.List;
123
import java.util.List;
123
import java.util.ListIterator;
124
import java.util.ListIterator;
124
import java.util.Map;
125
import java.util.Map;
125
import java.util.Map.Entry;
126
import java.util.Map.Entry;
126
import java.util.Set;
127
import java.util.Set;
127
import java.util.SortedMap;
128
import java.util.SortedMap;
128
import java.util.concurrent.atomic.AtomicReference;
129
import java.util.concurrent.atomic.AtomicReference;
129
import java.util.logging.Level;
130
import java.util.logging.Level;
-
 
131
import java.util.function.Supplier;
130
 
132
 
131
import javax.swing.JComponent;
133
import javax.swing.JComponent;
132
import javax.swing.JOptionPane;
134
import javax.swing.JOptionPane;
133
import javax.swing.text.JTextComponent;
135
import javax.swing.text.JTextComponent;
134
 
136
 
135
import net.jcip.annotations.GuardedBy;
137
import net.jcip.annotations.GuardedBy;
136
import net.jcip.annotations.Immutable;
138
import net.jcip.annotations.Immutable;
137
 
139
 
138
/**
140
/**
139
 * Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
141
 * Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
140
 * privateForeignField).
142
 * privateForeignField).
141
 * 
143
 * 
142
 * @author ilm
144
 * @author ilm
143
 */
145
 */
144
public abstract class SQLElement {
146
public abstract class SQLElement {
145
 
147
 
146
    private static Phrase createPhrase(String singular, String plural) {
148
    private static Phrase createPhrase(String singular, String plural) {
147
        final NounClass nounClass;
149
        final NounClass nounClass;
148
        final String base;
150
        final String base;
149
        if (singular.startsWith("une ")) {
151
        if (singular.startsWith("une ")) {
150
            nounClass = NounClass.FEMININE;
152
            nounClass = NounClass.FEMININE;
151
            base = singular.substring(4);
153
            base = singular.substring(4);
152
        } else if (singular.startsWith("un ")) {
154
        } else if (singular.startsWith("un ")) {
153
            nounClass = NounClass.MASCULINE;
155
            nounClass = NounClass.MASCULINE;
154
            base = singular.substring(3);
156
            base = singular.substring(3);
155
        } else {
157
        } else {
156
            nounClass = null;
158
            nounClass = null;
157
            base = singular;
159
            base = singular;
158
        }
160
        }
159
        final Phrase res = new Phrase(Grammar_fr.getInstance(), base, nounClass);
161
        final Phrase res = new Phrase(Grammar_fr.getInstance(), base, nounClass);
160
        if (nounClass != null)
162
        if (nounClass != null)
161
            res.putVariantIfDifferent(Grammar.INDEFINITE_ARTICLE_SINGULAR, singular);
163
            res.putVariantIfDifferent(Grammar.INDEFINITE_ARTICLE_SINGULAR, singular);
162
        res.putVariantIfDifferent(Grammar.PLURAL, plural);
164
        res.putVariantIfDifferent(Grammar.PLURAL, plural);
163
        return res;
165
        return res;
164
    }
166
    }
165
 
167
 
166
    // from the most loss of information to the least.
168
    // from the most loss of information to the least.
167
    public static enum ReferenceAction {
169
    public static enum ReferenceAction {
168
        /** If a referenced row is archived, empty the foreign field */
170
        /** If a referenced row is archived, empty the foreign field */
169
        SET_EMPTY,
171
        SET_EMPTY,
170
        /** If a referenced row is archived, archive this row too */
172
        /** If a referenced row is archived, archive this row too */
171
        CASCADE,
173
        CASCADE,
172
        /** If a referenced row is to be archived, abort the operation */
174
        /** If a referenced row is to be archived, abort the operation */
173
        RESTRICT
175
        RESTRICT
174
    }
176
    }
175
 
177
 
176
    static final public String DEFAULT_COMP_ID = "default component code";
178
    static final public String DEFAULT_COMP_ID = "default component code";
177
    /**
179
    /**
178
     * If this value is passed to the constructor, {@link #createCode()} will only be called the
180
     * If this value is passed to the constructor, {@link #createCode()} will only be called the
179
     * first time {@link #getCode()} is. This allow the method to use objects passed to the
181
     * first time {@link #getCode()} is. This allow the method to use objects passed to the
180
     * constructor of a subclass.
182
     * constructor of a subclass.
181
     */
183
     */
182
    static final public String DEFERRED_CODE = new String("deferred code");
184
    static final public String DEFERRED_CODE = new String("deferred code");
183
 
185
 
184
    @GuardedBy("this")
186
    @GuardedBy("this")
185
    private SQLElementDirectory directory;
187
    private SQLElementDirectory directory;
186
    private Phrase defaultName;
188
    private Phrase defaultName;
187
    private final SQLTable primaryTable;
189
    private final SQLTable primaryTable;
188
    // used as a key in SQLElementDirectory so it should be immutable
190
    // used as a key in SQLElementDirectory so it should be immutable
189
    private String code;
191
    private String code;
190
    private ComboSQLRequest combo;
192
    private ComboSQLRequest combo;
191
    private ListSQLRequest list;
193
    private ListSQLRequest list;
192
    private SQLTableModelSourceOnline tableSrc;
194
    private SQLTableModelSourceOnline tableSrc;
193
    private final ListChangeRecorder<IListeAction> rowActions;
195
    private final ListChangeRecorder<IListeAction> rowActions;
194
    private final LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
196
    private final LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
195
    // links
197
    // links
196
    private SQLElementLinks ownedLinks;
198
    private SQLElementLinks ownedLinks;
197
    private SQLElementLinks otherLinks;
199
    private SQLElementLinks otherLinks;
198
    // keep it for now as joins are disallowed (see initFF())
200
    // keep it for now as joins are disallowed (see initFF())
199
    private String parentFF;
201
    private String parentFF;
200
 
202
 
201
    // lazy creation
203
    // lazy creation
202
    @GuardedBy("this")
204
    @GuardedBy("this")
203
    private SQLCache<SQLRow, Object> modelCache;
205
    private SQLCache<SQLRow, Object> modelCache;
204
 
206
 
205
    private final Map<String, JComponent> additionalFields;
207
    private final Map<String, Supplier<? extends JComponent>> additionalFields;
206
    private final List<SQLTableModelColumn> additionalListCols;
208
    private final List<SQLTableModelColumn> additionalListCols;
207
    @GuardedBy("this")
209
    @GuardedBy("this")
208
    private List<String> mdPath;
210
    private List<String> mdPath;
209
 
211
 
210
    private Group defaultGroup;
212
    private Group defaultGroup;
211
    private Group groupForCreation;
213
    private Group groupForCreation;
212
    private Group groupForModification;
214
    private Group groupForModification;
213
 
215
 
214
    @Deprecated
216
    @Deprecated
215
    public SQLElement(String singular, String plural, SQLTable primaryTable) {
217
    public SQLElement(String singular, String plural, SQLTable primaryTable) {
216
        this(primaryTable, createPhrase(singular, plural));
218
        this(primaryTable, createPhrase(singular, plural));
217
    }
219
    }
218
 
220
 
219
    public SQLElement(SQLTable primaryTable) {
221
    public SQLElement(SQLTable primaryTable) {
220
        this(primaryTable, null);
222
        this(primaryTable, null);
221
    }
223
    }
222
 
224
 
223
    public SQLElement(final SQLTable primaryTable, final Phrase name) {
225
    public SQLElement(final SQLTable primaryTable, final Phrase name) {
224
        this(primaryTable, name, null);
226
        this(primaryTable, name, null);
225
    }
227
    }
226
 
228
 
227
    public SQLElement(final SQLTable primaryTable, final Phrase name, final String code) {
229
    public SQLElement(final SQLTable primaryTable, final Phrase name, final String code) {
228
        super();
230
        super();
229
        if (primaryTable == null) {
231
        if (primaryTable == null) {
230
            throw new DBStructureItemNotFound("table is null for " + this.getClass());
232
            throw new DBStructureItemNotFound("table is null for " + this.getClass());
231
        }
233
        }
232
        this.primaryTable = primaryTable;
234
        this.primaryTable = primaryTable;
233
        this.setDefaultName(name);
235
        this.setDefaultName(name);
234
        this.code = code == null ? createCode() : code;
236
        this.code = code == null ? createCode() : code;
235
        this.combo = null;
237
        this.combo = null;
236
        this.list = null;
238
        this.list = null;
237
        this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>());
239
        this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>());
238
        this.resetRelationships();
240
        this.resetRelationships();
239
 
241
 
240
        this.components = new LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>();
242
        this.components = new LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>();
241
 
243
 
242
        this.modelCache = null;
244
        this.modelCache = null;
243
 
245
 
244
        // the components should always be in the same order
246
        // the components should always be in the same order
245
        this.additionalFields = new LinkedHashMap<String, JComponent>();
247
        this.additionalFields = new LinkedHashMap<>();
246
        this.additionalListCols = new ArrayList<SQLTableModelColumn>();
248
        this.additionalListCols = new ArrayList<SQLTableModelColumn>();
247
        this.mdPath = Collections.emptyList();
249
        this.mdPath = Collections.emptyList();
248
    }
250
    }
249
 
251
 
250
    public void destroy() {
252
    public void destroy() {
251
    }
253
    }
252
 
254
 
253
    /**
255
    /**
254
     * Should return the code for this element. This method is only called if the <code>code</code>
256
     * Should return the code for this element. This method is only called if the <code>code</code>
255
     * parameter of the constructor is <code>null</code>. This implementation returns a string
257
     * parameter of the constructor is <code>null</code>. This implementation returns a string
256
     * containing the {@link Class#getName() full name} of the class and the {@link #getTable()
258
     * containing the {@link Class#getName() full name} of the class and the {@link #getTable()
257
     * table} name to handle a single class being used for multiple tables. NOTE: this method is
259
     * table} name to handle a single class being used for multiple tables. NOTE: this method is
258
     * also needed, since a subclass constructor cannot pass <code>this.getClass().getName()</code>
260
     * also needed, since a subclass constructor cannot pass <code>this.getClass().getName()</code>
259
     * as the code parameter to <code>super</code>.
261
     * as the code parameter to <code>super</code>.
260
     * 
262
     * 
261
     * @return the default code for this element.
263
     * @return the default code for this element.
262
     */
264
     */
263
    protected String createCode() {
265
    protected String createCode() {
264
        return getClass().getName() + "-" + getTable().getName();
266
        return getClass().getName() + "-" + getTable().getName();
265
    }
267
    }
266
 
268
 
267
    public Group getGroupForCreation() {
269
    public Group getGroupForCreation() {
268
        if (this.groupForCreation != null) {
270
        if (this.groupForCreation != null) {
269
            return this.groupForCreation;
271
            return this.groupForCreation;
270
        }
272
        }
271
        return getDefaultGroup();
273
        return getDefaultGroup();
272
    }
274
    }
273
 
275
 
274
    public Group getGroupForModification() {
276
    public Group getGroupForModification() {
275
        if (this.groupForModification != null) {
277
        if (this.groupForModification != null) {
276
            return this.groupForModification;
278
            return this.groupForModification;
277
        }
279
        }
278
        return getDefaultGroup();
280
        return getDefaultGroup();
279
    }
281
    }
280
 
282
 
281
    public Group getDefaultGroup() {
283
    public Group getDefaultGroup() {
282
        return this.defaultGroup;
284
        return this.defaultGroup;
283
    }
285
    }
284
 
286
 
285
    public void setDefaultGroup(Group defaultGroup) {
287
    public void setDefaultGroup(Group defaultGroup) {
286
        this.defaultGroup = defaultGroup;
288
        this.defaultGroup = defaultGroup;
287
    }
289
    }
288
 
290
 
289
    /**
291
    /**
290
     * Get the group based on the edit mode
292
     * Get the group based on the edit mode
291
     * 
293
     * 
292
     * @param editMode
294
     * @param editMode
293
     * @return
295
     * @return
294
     */
296
     */
295
    public Group getEditGroup(final EditMode editMode) {
297
    public Group getEditGroup(final EditMode editMode) {
296
        if (editMode.equals(EditMode.CREATION)) {
298
        if (editMode.equals(EditMode.CREATION)) {
297
            return this.getGroupForCreation();
299
            return this.getGroupForCreation();
298
        } else {
300
        } else {
299
            return this.getGroupForModification();
301
            return this.getGroupForModification();
300
        }
302
        }
301
    }
303
    }
302
 
304
 
303
    /**
305
    /**
304
     * Override this function in an element to show default values in edit frame
306
     * Override this function in an element to show default values in edit frame
305
     * 
307
     * 
306
     * @param token - The security token of session
308
     * @param token - The security token of session
307
     * 
309
     * 
308
     * @return a default SQLRowValues
310
     * @return a default SQLRowValues
309
     */
311
     */
310
    public SQLRowValues createDefaultRowValues(final String token) {
312
    public SQLRowValues createDefaultRowValues(final String token) {
311
        return new SQLRowValues(getTable());
313
        return new SQLRowValues(getTable());
312
    }
314
    }
313
 
315
 
314
    /**
316
    /**
315
     * Create the edition frame for this SQLElement
317
     * Create the edition frame for this SQLElement
316
     * 
318
     * 
317
     * @param configuration current configuration
319
     * @param configuration current configuration
318
     * @param parentFrame parent frame of the edit frame
320
     * @param parentFrame parent frame of the edit frame
319
     * @param editMode edition mode (CREATION, MODIFICATION, READONLY)
321
     * @param editMode edition mode (CREATION, MODIFICATION, READONLY)
320
     * @param sqlRow SQLRowAccessor use for fill the edition frame
322
     * @param sqlRow SQLRowAccessor use for fill the edition frame
321
     * @param sessionSecurityToken String, use for find session with an instance of LightServer
323
     * @param sessionSecurityToken String, use for find session with an instance of LightServer
322
     * @return the edition frame of this SQLElement
324
     * @return the edition frame of this SQLElement
323
     */
325
     */
324
    public LightEditFrame createEditFrame(final PropsConfiguration configuration, final LightUIFrame parentFrame, final EditMode editMode, final SQLRowAccessor sqlRow,
326
    public LightEditFrame createEditFrame(final PropsConfiguration configuration, final LightUIFrame parentFrame, final EditMode editMode, final SQLRowAccessor sqlRow,
325
            final String sessionSecurityToken) {
327
            final String sessionSecurityToken) {
326
        final Group editGroup = this.getEditGroup(editMode);
328
        final Group editGroup = this.getEditGroup(editMode);
327
        if (editGroup == null) {
329
        if (editGroup == null) {
328
            Log.get().severe("The edit group is null for this element : " + this);
330
            Log.get().severe("The edit group is null for this element : " + this);
329
            return null;
331
            return null;
330
        }
332
        }
331
 
333
 
332
        final GroupToLightUIConvertor convertor = this.getGroupToLightUIConvertor(configuration, editMode, sqlRow, sessionSecurityToken);
334
        final GroupToLightUIConvertor convertor = this.getGroupToLightUIConvertor(configuration, editMode, sqlRow, sessionSecurityToken);
333
        final LightEditFrame editFrame = convertor.convert(editGroup, sqlRow, parentFrame, editMode);
335
        final LightEditFrame editFrame = convertor.convert(editGroup, sqlRow, parentFrame, editMode);
334
 
336
 
335
        if (editMode.equals(EditMode.CREATION)) {
337
        if (editMode.equals(EditMode.CREATION)) {
336
            editFrame.createTitlePanel(this.getCreationFrameTitle());
338
            editFrame.createTitlePanel(this.getCreationFrameTitle());
337
        } else if (editMode.equals(EditMode.MODIFICATION)) {
339
        } else if (editMode.equals(EditMode.MODIFICATION)) {
338
            editFrame.createTitlePanel(this.getModificationFrameTitle(sqlRow));
340
            editFrame.createTitlePanel(this.getModificationFrameTitle(sqlRow));
339
            new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
341
            new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
340
        } else if (editMode.equals(EditMode.READONLY)) {
342
        } else if (editMode.equals(EditMode.READONLY)) {
341
            editFrame.createTitlePanel(this.getReadOnlyFrameTitle(sqlRow));
343
            editFrame.createTitlePanel(this.getReadOnlyFrameTitle(sqlRow));
342
            new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
344
            new LightUIPanelFiller(editFrame.getContentPanel()).fillFromRow(configuration, this, sqlRow, sessionSecurityToken);
343
        }
345
        }
344
 
346
 
345
        this.setEditFrameModifiers(editFrame, sessionSecurityToken);
347
        this.setEditFrameModifiers(editFrame, sessionSecurityToken);
346
 
348
 
347
        return editFrame;
349
        return editFrame;
348
    }
350
    }
349
 
351
 
350
    /**
352
    /**
351
     * Get title for read only mode
353
     * Get title for read only mode
352
     * 
354
     * 
353
     * @param sqlRow - SQLRowValues use for fill the edition frame
355
     * @param sqlRow - SQLRowValues use for fill the edition frame
354
     * @return The title for read only mode
356
     * @return The title for read only mode
355
     */
357
     */
356
    protected String getReadOnlyFrameTitle(final SQLRowAccessor sqlRow) {
358
    protected String getReadOnlyFrameTitle(final SQLRowAccessor sqlRow) {
357
        return EditFrame.getReadOnlyMessage(this);
359
        return EditFrame.getReadOnlyMessage(this);
358
    }
360
    }
359
 
361
 
360
    /**
362
    /**
361
     * Get title for modification mode
363
     * Get title for modification mode
362
     * 
364
     * 
363
     * @param sqlRow - SQLRowValues use for fill the edition frame
365
     * @param sqlRow - SQLRowValues use for fill the edition frame
364
     * @return The title for read only mode
366
     * @return The title for read only mode
365
     */
367
     */
366
    protected String getModificationFrameTitle(final SQLRowAccessor sqlRow) {
368
    protected String getModificationFrameTitle(final SQLRowAccessor sqlRow) {
367
        return EditFrame.getModifyMessage(this);
369
        return EditFrame.getModifyMessage(this);
368
    }
370
    }
369
 
371
 
370
    /**
372
    /**
371
     * Get title for creation mode
373
     * Get title for creation mode
372
     * 
374
     * 
373
     * @param sqlRow - SQLRowValues use for fill the edition frame
375
     * @param sqlRow - SQLRowValues use for fill the edition frame
374
     * @return The title for read only mode
376
     * @return The title for read only mode
375
     */
377
     */
376
    protected String getCreationFrameTitle() {
378
    protected String getCreationFrameTitle() {
377
        return EditFrame.getCreateMessage(this);
379
        return EditFrame.getCreateMessage(this);
378
    }
380
    }
379
 
381
 
380
    /**
382
    /**
381
     * 
383
     * 
382
     * @param configuration - The user SQL configuration
384
     * @param configuration - The user SQL configuration
383
     * @param editMode - Edit mode of the frame
385
     * @param editMode - Edit mode of the frame
384
     * @param sqlRow - The row to update
386
     * @param sqlRow - The row to update
385
     * @param token - The session security token
387
     * @param token - The session security token
386
     * 
388
     * 
387
     * @return An initialized GroupToLightUIConvertor
389
     * @return An initialized GroupToLightUIConvertor
388
     */
390
     */
389
    public GroupToLightUIConvertor getGroupToLightUIConvertor(final PropsConfiguration configuration, final EditMode editMode, final SQLRowAccessor sqlRow, final String token) {
391
    public GroupToLightUIConvertor getGroupToLightUIConvertor(final PropsConfiguration configuration, final EditMode editMode, final SQLRowAccessor sqlRow, final String token) {
390
        final GroupToLightUIConvertor convertor = new GroupToLightUIConvertor(configuration);
392
        final GroupToLightUIConvertor convertor = new GroupToLightUIConvertor(configuration);
391
        // if (editMode.equals(EditMode.CREATION)) {
393
        // if (editMode.equals(EditMode.CREATION)) {
392
        convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, token));
394
        convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, token));
393
        // } else {
395
        // } else {
394
        // convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, sqlRow,
396
        // convertor.putAllCustomEditorProvider(this.getCustomRowEditors(configuration, sqlRow,
395
        // token));
397
        // token));
396
        // }
398
        // }
397
        return convertor;
399
        return convertor;
398
    }
400
    }
399
 
401
 
400
    /**
402
    /**
401
     * Override this function in an element and put new value in map for use ComboValueConvertor.
403
     * Override this function in an element and put new value in map for use ComboValueConvertor.
402
     * This one allow you to change value store in database by an other one.
404
     * This one allow you to change value store in database by an other one.
403
     * 
405
     * 
404
     * @return Map which contains all ComboValueConvertors use for this SQLElement edition. Key: ID
406
     * @return Map which contains all ComboValueConvertors use for this SQLElement edition. Key: ID
405
     *         of group item / Value: ComboValueConvertor
407
     *         of group item / Value: ComboValueConvertor
406
     */
408
     */
407
    // TODO: use renderer instead of ValueConvertor
409
    // TODO: use renderer instead of ValueConvertor
408
    public Map<String, ComboValueConvertor<?>> getComboConvertors() {
410
    public Map<String, ComboValueConvertor<?>> getComboConvertors() {
409
        return new HashMap<String, ComboValueConvertor<?>>();
411
        return new HashMap<String, ComboValueConvertor<?>>();
410
    }
412
    }
411
 
413
 
412
    /**
414
    /**
413
     * Override this function in an element and put new value in map for use ConvertorModifier. This
415
     * Override this function in an element and put new value in map for use ConvertorModifier. This
414
     * one allow you to apply some change on LightUIElement before send it to client
416
     * one allow you to apply some change on LightUIElement before send it to client
415
     * 
417
     * 
416
     */
418
     */
417
    // TODO: implement with IClosure
419
    // TODO: implement with IClosure
418
    public void setEditFrameModifiers(final LightEditFrame frame, final String sessionToken) {
420
    public void setEditFrameModifiers(final LightEditFrame frame, final String sessionToken) {
419
    }
421
    }
420
 
422
 
421
    public List<CustomRowEditor> getCustomRowEditors(final Configuration configuration, final String sessionToken) {
423
    public List<CustomRowEditor> getCustomRowEditors(final Configuration configuration, final String sessionToken) {
422
        final Map<String, ComboValueConvertor<?>> comboConvertors = this.getComboConvertors();
424
        final Map<String, ComboValueConvertor<?>> comboConvertors = this.getComboConvertors();
423
        final List<CustomRowEditor> result = new ArrayList<>();
425
        final List<CustomRowEditor> result = new ArrayList<>();
424
        for (final Entry<String, ComboValueConvertor<?>> entry : comboConvertors.entrySet()) {
426
        for (final Entry<String, ComboValueConvertor<?>> entry : comboConvertors.entrySet()) {
425
 
427
 
426
            final String itemId = entry.getKey();
428
            final String itemId = entry.getKey();
427
            result.add(new CustomRowEditor(itemId) {
429
            result.add(new CustomRowEditor(itemId) {
428
                final ComboValueConvertor<?> convertor = entry.getValue();
430
                final ComboValueConvertor<?> convertor = entry.getValue();
429
 
431
 
430
                @Override
432
                @Override
431
                public LightUIElement createUIElement() {
433
                public LightUIElement createUIElement() {
432
                    final LightUIComboBox uiCombo = new LightUIComboBox(getItemId());
434
                    final LightUIComboBox uiCombo = new LightUIComboBox(getItemId());
433
                    this.convertor.fillCombo(uiCombo, null);
435
                    this.convertor.fillCombo(uiCombo, null);
434
                    return uiCombo;
436
                    return uiCombo;
435
                }
437
                }
436
 
438
 
437
                @Override
439
                @Override
438
                public void fillFrom(LightUIElement uiElement, SQLRowAccessor sqlRow) {
440
                public void fillFrom(LightUIElement uiElement, SQLRowAccessor sqlRow) {
439
                    final LightUIComboBox uiCombo = (LightUIComboBox) uiElement;
441
                    final LightUIComboBox uiCombo = (LightUIComboBox) uiElement;
440
                    final SQLField field = configuration.getFieldMapper().getSQLFieldForItem(getItemId());
442
                    final SQLField field = configuration.getFieldMapper().getSQLFieldForItem(getItemId());
441
                    if (this.convertor instanceof StringValueConvertor) {
443
                    if (this.convertor instanceof StringValueConvertor) {
442
                        ((StringValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getString(field.getFieldName()));
444
                        ((StringValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getString(field.getFieldName()));
443
                    } else if (this.convertor instanceof IntValueConvertor) {
445
                    } else if (this.convertor instanceof IntValueConvertor) {
444
                        if (sqlRow.getObject(field.getFieldName()) != null) {
446
                        if (sqlRow.getObject(field.getFieldName()) != null) {
445
                            ((IntValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getInt(field.getFieldName()));
447
                            ((IntValueConvertor) this.convertor).fillCombo(uiCombo, sqlRow.getInt(field.getFieldName()));
446
                        }
448
                        }
447
                    }
449
                    }
448
 
450
 
449
                }
451
                }
450
 
452
 
451
                @Override
453
                @Override
452
                public void store(LightUIElement uiElement, SQLRowValues sqlRow) {
454
                public void store(LightUIElement uiElement, SQLRowValues sqlRow) {
453
                    final LightUIComboBox combobox = (LightUIComboBox) uiElement;
455
                    final LightUIComboBox combobox = (LightUIComboBox) uiElement;
454
                    final String fieldName = configuration.getFieldMapper().getSQLFieldForItem(getItemId()).getName();
456
                    final String fieldName = configuration.getFieldMapper().getSQLFieldForItem(getItemId()).getName();
455
                    if (combobox.hasSelectedValue()) {
457
                    if (combobox.hasSelectedValue()) {
456
                        if (this.convertor instanceof StringValueConvertor) {
458
                        if (this.convertor instanceof StringValueConvertor) {
457
                            sqlRow.put(fieldName, ((StringValueConvertor) this.convertor).getIdFromIndex(combobox.getSelectedValue().getId()));
459
                            sqlRow.put(fieldName, ((StringValueConvertor) this.convertor).getIdFromIndex(combobox.getSelectedValue().getId()));
458
                        } else if (this.convertor instanceof IntValueConvertor) {
460
                        } else if (this.convertor instanceof IntValueConvertor) {
459
                            sqlRow.put(fieldName, combobox.getSelectedValue().getId());
461
                            sqlRow.put(fieldName, combobox.getSelectedValue().getId());
460
                        } else {
462
                        } else {
461
                            throw new IllegalArgumentException("the save is not implemented for the class: " + this.convertor.getClass().getName() + " - ui id: " + getItemId());
463
                            throw new IllegalArgumentException("the save is not implemented for the class: " + this.convertor.getClass().getName() + " - ui id: " + getItemId());
462
                        }
464
                        }
463
                    } else {
465
                    } else {
464
                        sqlRow.put(fieldName, null);
466
                        sqlRow.put(fieldName, null);
465
                    }
467
                    }
466
 
468
 
467
                }
469
                }
468
            });
470
            });
469
        }
471
        }
470
        return result;
472
        return result;
471
    }
473
    }
472
 
474
 
473
    /**
475
    /**
474
     * Override this function in an element to execute some code just after inserted new row in
476
     * Override this function in an element to execute some code just after inserted new row in
475
     * database
477
     * database
476
     *
478
     *
477
     * @param editFrame - The edit frame of this SQLRow
479
     * @param editFrame - The edit frame of this SQLRow
478
     * @param sqlRow - The row which was just inserted
480
     * @param sqlRow - The row which was just inserted
479
     * @param sessionToken Security token of session which allow to find session in LightServer
481
     * @param sessionToken Security token of session which allow to find session in LightServer
480
     *        instance
482
     *        instance
481
     * 
483
     * 
482
     * @throws Exception
484
     * @throws Exception
483
     */
485
     */
484
    public void doAfterLightInsert(final LightEditFrame editFrame, final SQLRow sqlRow, final String sessionToken) throws Exception {
486
    public void doAfterLightInsert(final LightEditFrame editFrame, final SQLRow sqlRow, final String sessionToken) throws Exception {
485
 
487
 
486
    }
488
    }
487
 
489
 
488
    /**
490
    /**
489
     * Override this function in an element to execute some code just after deleted a row in
491
     * Override this function in an element to execute some code just after deleted a row in
490
     * database
492
     * database
491
     *
493
     *
492
     * @param frame - The current frame
494
     * @param frame - The current frame
493
     * @param sqlRow - The row which was deleted
495
     * @param sqlRow - The row which was deleted
494
     * @param sessionToken Security token of session which allow to find session in LightServer
496
     * @param sessionToken Security token of session which allow to find session in LightServer
495
     *        instance
497
     *        instance
496
     * 
498
     * 
497
     * @throws Exception
499
     * @throws Exception
498
     */
500
     */
499
    public void doAfterLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
501
    public void doAfterLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
500
 
502
 
501
    }
503
    }
502
 
504
 
503
    /**
505
    /**
504
     * Override this function in an element to execute some code before inserted new row in database
506
     * Override this function in an element to execute some code before inserted new row in database
505
     *
507
     *
506
     * @param frame - The current frame
508
     * @param frame - The current frame
507
     * @param sqlRow - The row which will be deleted
509
     * @param sqlRow - The row which will be deleted
508
     * @param sessionToken - Security token of session which allow to find session in LightServer
510
     * @param sessionToken - Security token of session which allow to find session in LightServer
509
     *        instance
511
     *        instance
510
     * 
512
     * 
511
     * @throws Exception
513
     * @throws Exception
512
     */
514
     */
513
    public void doBeforeLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
515
    public void doBeforeLightDelete(final LightUIFrame frame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
514
 
516
 
515
    }
517
    }
516
 
518
 
517
    /**
519
    /**
518
     * Override this function in an element to execute some code before inserted new row in database
520
     * Override this function in an element to execute some code before inserted new row in database
519
     * 
521
     * 
520
     * @param editFrame - The edit frame of this SQLRowValues
522
     * @param editFrame - The edit frame of this SQLRowValues
521
     * @param sqlRow - The row which was just inserted
523
     * @param sqlRow - The row which was just inserted
522
     * @param sessionToken - Security token of session which allow to find session in LightServer
524
     * @param sessionToken - Security token of session which allow to find session in LightServer
523
     *        instance
525
     *        instance
524
     * 
526
     * 
525
     * @throws Exception
527
     * @throws Exception
526
     */
528
     */
527
    public void doBeforeLightInsert(final LightEditFrame editFrame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
529
    public void doBeforeLightInsert(final LightEditFrame editFrame, final SQLRowValues sqlRow, final String sessionToken) throws Exception {
528
 
530
 
529
    }
531
    }
530
 
532
 
531
    /**
533
    /**
532
     * Get ShowAs values of this SQLElement
534
     * Get ShowAs values of this SQLElement
533
     * 
535
     * 
534
     * @param id - The id which you want to expand
536
     * @param id - The id which you want to expand
535
     * 
537
     * 
536
     * @return A SQLRowValues with data
538
     * @return A SQLRowValues with data
537
     */
539
     */
538
    public SQLRowValues getValuesOfShowAs(final Number id) {
540
    public SQLRowValues getValuesOfShowAs(final Number id) {
539
        final SQLRowValues tmp = new SQLRowValues(this.getTable());
541
        final SQLRowValues tmp = new SQLRowValues(this.getTable());
540
        final ListMap<String, String> showAs = this.getShowAs();
542
        final ListMap<String, String> showAs = this.getShowAs();
541
 
543
 
542
        for (final List<String> listStr : showAs.values()) {
544
        for (final List<String> listStr : showAs.values()) {
543
            tmp.putNulls(listStr);
545
            tmp.putNulls(listStr);
544
        }
546
        }
545
        this.getDirectory().getShowAs().expand(tmp);
547
        this.getDirectory().getShowAs().expand(tmp);
546
 
548
 
547
        final SQLRowValues fetched = SQLRowValuesListFetcher.create(tmp).fetchOne(id);
549
        final SQLRowValues fetched = SQLRowValuesListFetcher.create(tmp).fetchOne(id);
548
        if (fetched == null) {
550
        if (fetched == null) {
549
            throw new IllegalArgumentException("Impossible to find Row in database - table: " + this.getTable().getName() + ", id: " + id);
551
            throw new IllegalArgumentException("Impossible to find Row in database - table: " + this.getTable().getName() + ", id: " + id);
550
        }
552
        }
551
 
553
 
552
        return fetched;
554
        return fetched;
553
    }
555
    }
554
 
556
 
555
    /**
557
    /**
556
     * Must be called if foreign/referent keys are added or removed.
558
     * Must be called if foreign/referent keys are added or removed.
557
     */
559
     */
558
    public synchronized void resetRelationships() {
560
    public synchronized void resetRelationships() {
559
        if (this.areRelationshipsInited()) {
561
        if (this.areRelationshipsInited()) {
560
            // if we remove links, notify owned elements
562
            // if we remove links, notify owned elements
561
            for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
563
            for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
562
                l.getOwned().resetRelationshipsOf(this);
564
                l.getOwned().resetRelationshipsOf(this);
563
            }
565
            }
564
        }
566
        }
565
        this.ownedLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
567
        this.ownedLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
566
        this.otherLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
568
        this.otherLinks = this instanceof JoinSQLElement ? new SQLElementLinks(SetMap.<LinkType, SQLElementLink> empty()) : null;
567
        this.parentFF = null;
569
        this.parentFF = null;
568
    }
570
    }
569
 
571
 
570
    private synchronized void resetRelationshipsOf(final SQLElement changed) {
572
    private synchronized void resetRelationshipsOf(final SQLElement changed) {
571
        // MAYBE optimize and only remove links for the passed argument
573
        // MAYBE optimize and only remove links for the passed argument
572
        this.otherLinks = null;
574
        this.otherLinks = null;
573
    }
575
    }
574
 
576
 
575
    protected synchronized final boolean areRelationshipsInited() {
577
    protected synchronized final boolean areRelationshipsInited() {
576
        return this.ownedLinks != null;
578
        return this.ownedLinks != null;
577
    }
579
    }
578
 
580
 
579
    // return Path from owner to owned
581
    // return Path from owner to owned
580
    private final Set<Path> createPaths(final boolean wantedOwned) {
582
    private final Set<Path> createPaths(final boolean wantedOwned) {
581
        // joins cannot have SQLElementLink
583
        // joins cannot have SQLElementLink
582
        if (this instanceof JoinSQLElement)
584
        if (this instanceof JoinSQLElement)
583
            return Collections.emptySet();
585
            return Collections.emptySet();
584
 
586
 
585
        final SQLTable thisTable = this.getTable();
587
        final SQLTable thisTable = this.getTable();
586
        final Set<Link> allLinks = thisTable.getDBSystemRoot().getGraph().getAllLinks(getTable());
588
        final Set<Link> allLinks = thisTable.getDBSystemRoot().getGraph().getAllLinks(getTable());
587
        final Set<Path> res = new HashSet<Path>();
589
        final Set<Path> res = new HashSet<Path>();
588
        for (final Link l : allLinks) {
590
        for (final Link l : allLinks) {
589
            final boolean owned;
591
            final boolean owned;
590
            final Path pathFromOwner;
592
            final Path pathFromOwner;
591
            final SQLElement sourceElem = this.getElementLenient(l.getSource());
593
            final SQLElement sourceElem = this.getElementLenient(l.getSource());
592
            if (sourceElem instanceof JoinSQLElement) {
594
            if (sourceElem instanceof JoinSQLElement) {
593
                final JoinSQLElement joinElem = (JoinSQLElement) sourceElem;
595
                final JoinSQLElement joinElem = (JoinSQLElement) sourceElem;
594
                pathFromOwner = joinElem.getPathFromOwner();
596
                pathFromOwner = joinElem.getPathFromOwner();
595
                // ATTN when source == target, the same path will both owned and not
597
                // ATTN when source == target, the same path will both owned and not
596
                owned = joinElem.getLinkToOwner().equals(l);
598
                owned = joinElem.getLinkToOwner().equals(l);
597
            } else if (l.getSource() == l.getTarget()) {
599
            } else if (l.getSource() == l.getTarget()) {
598
                owned = wantedOwned;
600
                owned = wantedOwned;
599
                pathFromOwner = new PathBuilder(l.getSource()).add(l, Direction.FOREIGN).build();
601
                pathFromOwner = new PathBuilder(l.getSource()).add(l, Direction.FOREIGN).build();
600
            } else {
602
            } else {
601
                owned = l.getSource() == thisTable;
603
                owned = l.getSource() == thisTable;
602
                pathFromOwner = new PathBuilder(l.getSource()).add(l).build();
604
                pathFromOwner = new PathBuilder(l.getSource()).add(l).build();
603
            }
605
            }
604
            if (owned == wantedOwned)
606
            if (owned == wantedOwned)
605
                res.add(pathFromOwner);
607
                res.add(pathFromOwner);
606
        }
608
        }
607
        return res;
609
        return res;
608
    }
610
    }
609
 
611
 
610
    // this implementation uses getParentFFName() and assumes that links to privates are COMPOSITION
612
    // this implementation uses getParentFFName() and assumes that links to privates are COMPOSITION
611
    final SetMap<LinkType, Path> getDefaultLinkTypes() {
613
    final SetMap<LinkType, Path> getDefaultLinkTypes() {
612
        final Set<Path> ownedPaths = createPaths(true);
614
        final Set<Path> ownedPaths = createPaths(true);
613
        final SetMap<LinkType, Path> res = new SetMap<LinkType, Path>();
615
        final SetMap<LinkType, Path> res = new SetMap<LinkType, Path>();
614
        final String parentFFName = getParentFFName();
616
        final String parentFFName = getParentFFName();
615
        if (parentFFName != null) {
617
        if (parentFFName != null) {
616
            final Path pathToParent = new PathBuilder(getTable()).addForeignField(parentFFName).build();
618
            final Path pathToParent = new PathBuilder(getTable()).addForeignField(parentFFName).build();
617
            if (!ownedPaths.remove(pathToParent))
619
            if (!ownedPaths.remove(pathToParent))
618
                throw new IllegalStateException("getParentFFName() " + pathToParent + " isn't in " + ownedPaths);
620
                throw new IllegalStateException("getParentFFName() " + pathToParent + " isn't in " + ownedPaths);
619
            res.add(LinkType.PARENT, pathToParent);
621
            res.add(LinkType.PARENT, pathToParent);
620
        }
622
        }
621
        final List<String> privateFields = this.getPrivateFields();
623
        final List<String> privateFields = this.getPrivateFields();
622
        if (!privateFields.isEmpty())
624
        if (!privateFields.isEmpty())
623
            Log.get().warning("getPrivateFields() is deprecated use setupLinks(), " + this + " : " + privateFields);
625
            Log.get().warning("getPrivateFields() is deprecated use setupLinks(), " + this + " : " + privateFields);
624
        // links to private are COMPOSITION by default (normal links to privates are few)
626
        // links to private are COMPOSITION by default (normal links to privates are few)
625
        final Iterator<Path> iter = ownedPaths.iterator();
627
        final Iterator<Path> iter = ownedPaths.iterator();
626
        while (iter.hasNext()) {
628
        while (iter.hasNext()) {
627
            final Path ownedPath = iter.next();
629
            final Path ownedPath = iter.next();
628
            if (getElement(ownedPath.getLast()).isPrivate()) {
630
            if (getElement(ownedPath.getLast()).isPrivate()) {
629
                iter.remove();
631
                iter.remove();
630
                res.add(LinkType.COMPOSITION, ownedPath);
632
                res.add(LinkType.COMPOSITION, ownedPath);
631
            } else if (ownedPath.length() == 1 && ownedPath.isSingleField() && privateFields.contains(ownedPath.getSingleField(0).getName())) {
633
            } else if (ownedPath.length() == 1 && ownedPath.isSingleField() && privateFields.contains(ownedPath.getSingleField(0).getName())) {
632
                throw new IllegalStateException("getPrivateFields() contains " + ownedPath + " which points to an element which isn't private");
634
                throw new IllegalStateException("getPrivateFields() contains " + ownedPath + " which points to an element which isn't private");
633
            }
635
            }
634
        }
636
        }
635
        res.addAll(LinkType.ASSOCIATION, ownedPaths);
637
        res.addAll(LinkType.ASSOCIATION, ownedPaths);
636
        return res;
638
        return res;
637
    }
639
    }
638
 
640
 
639
    final List<ReferenceAction> getPossibleActions(final LinkType lt, final SQLElement targetElem) {
641
    final List<ReferenceAction> getPossibleActions(final LinkType lt, final SQLElement targetElem) {
640
        // MAYBE move required fields to SQLElement and use RESTRICT
642
        // MAYBE move required fields to SQLElement and use RESTRICT
641
 
643
 
642
        final List<ReferenceAction> res;
644
        final List<ReferenceAction> res;
643
        if (lt == LinkType.PARENT) {
645
        if (lt == LinkType.PARENT) {
644
            // SET_EMPTY would create an orphan
646
            // SET_EMPTY would create an orphan
645
            res = Arrays.asList(ReferenceAction.CASCADE, ReferenceAction.RESTRICT);
647
            res = Arrays.asList(ReferenceAction.CASCADE, ReferenceAction.RESTRICT);
646
        } else if (lt == LinkType.COMPOSITION) {
648
        } else if (lt == LinkType.COMPOSITION) {
647
            res = Arrays.asList(ReferenceAction.SET_EMPTY, ReferenceAction.RESTRICT);
649
            res = Arrays.asList(ReferenceAction.SET_EMPTY, ReferenceAction.RESTRICT);
648
        } else {
650
        } else {
649
            assert lt == LinkType.ASSOCIATION;
651
            assert lt == LinkType.ASSOCIATION;
650
            if (targetElem.isShared()) {
652
            if (targetElem.isShared()) {
651
                res = Arrays.asList(ReferenceAction.RESTRICT, ReferenceAction.SET_EMPTY);
653
                res = Arrays.asList(ReferenceAction.RESTRICT, ReferenceAction.SET_EMPTY);
652
            } else {
654
            } else {
653
                res = Arrays.asList(ReferenceAction.values());
655
                res = Arrays.asList(ReferenceAction.values());
654
            }
656
            }
655
        }
657
        }
656
        return res;
658
        return res;
657
    }
659
    }
658
 
660
 
659
    private synchronized void initFF() {
661
    private synchronized void initFF() {
660
        if (areRelationshipsInited())
662
        if (areRelationshipsInited())
661
            return;
663
            return;
662
 
664
 
663
        final SQLElementLinksSetup paths = new SQLElementLinksSetup(this);
665
        final SQLElementLinksSetup paths = new SQLElementLinksSetup(this);
664
        setupLinks(paths);
666
        setupLinks(paths);
665
        this.ownedLinks = new SQLElementLinks(paths.getResult());
667
        this.ownedLinks = new SQLElementLinks(paths.getResult());
666
 
668
 
667
        // try to fill old attributes
669
        // try to fill old attributes
668
        final SQLElementLink parentLink = this.getParentLink();
670
        final SQLElementLink parentLink = this.getParentLink();
669
        if (parentLink != null) {
671
        if (parentLink != null) {
670
            if (parentLink.getSingleField() != null)
672
            if (parentLink.getSingleField() != null)
671
                this.parentFF = parentLink.getSingleField().getName();
673
                this.parentFF = parentLink.getSingleField().getName();
672
            else
674
            else
673
                throw new UnsupportedOperationException("Parent field name not supported : " + parentLink);
675
                throw new UnsupportedOperationException("Parent field name not supported : " + parentLink);
674
        } else {
676
        } else {
675
            this.parentFF = null;
677
            this.parentFF = null;
676
        }
678
        }
677
        assert assertPrivateDefaultValues();
679
        assert assertPrivateDefaultValues();
678
 
680
 
679
        // if we added links, let the owned know
681
        // if we added links, let the owned know
680
        final Set<SQLElement> toReset = new HashSet<SQLElement>();
682
        final Set<SQLElement> toReset = new HashSet<SQLElement>();
681
        for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
683
        for (final SQLElementLink l : this.ownedLinks.getByPath().values()) {
682
            toReset.add(l.getOwned());
684
            toReset.add(l.getOwned());
683
        }
685
        }
684
        for (final SQLElement e : toReset) {
686
        for (final SQLElement e : toReset) {
685
            e.resetRelationshipsOf(this);
687
            e.resetRelationshipsOf(this);
686
        }
688
        }
687
 
689
 
688
        this.ffInited();
690
        this.ffInited();
689
    }
691
    }
690
 
692
 
691
    // since by definition private cannot be shared, the default value must be empty
693
    // since by definition private cannot be shared, the default value must be empty
692
    private final boolean assertPrivateDefaultValues() {
694
    private final boolean assertPrivateDefaultValues() {
693
        final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
695
        final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
694
        for (final SQLElementLink e : privates) {
696
        for (final SQLElementLink e : privates) {
695
            if (!e.isJoin()) {
697
            if (!e.isJoin()) {
696
                final SQLField singleField = e.getSingleField();
698
                final SQLField singleField = e.getSingleField();
697
                final Number privateDefault = (Number) singleField.getParsedDefaultValue().getValue();
699
                final Number privateDefault = (Number) singleField.getParsedDefaultValue().getValue();
698
                final Number foreignUndef = e.getPath().getLast().getUndefinedIDNumber();
700
                final Number foreignUndef = e.getPath().getLast().getUndefinedIDNumber();
699
                assert NumberUtils.areNumericallyEqual(privateDefault, foreignUndef) : singleField + " not empty : " + privateDefault;
701
                assert NumberUtils.areNumericallyEqual(privateDefault, foreignUndef) : singleField + " not empty : " + privateDefault;
700
            }
702
            }
701
        }
703
        }
702
        return true;
704
        return true;
703
    }
705
    }
704
 
706
 
705
    public boolean isPrivate() {
707
    public boolean isPrivate() {
706
        return false;
708
        return false;
707
    }
709
    }
708
 
710
 
709
    /**
711
    /**
710
     * Set {@link LinkType type} and other information for each owned link of this element.
712
     * Set {@link LinkType type} and other information for each owned link of this element.
711
     * 
713
     * 
712
     * @param links the setup object.
714
     * @param links the setup object.
713
     */
715
     */
714
    protected void setupLinks(SQLElementLinksSetup links) {
716
    protected void setupLinks(SQLElementLinksSetup links) {
715
    }
717
    }
716
 
718
 
717
    /**
719
    /**
718
     * Was used to set the action of an {@link SQLElementLink}.
720
     * Was used to set the action of an {@link SQLElementLink}.
719
     * 
721
     * 
720
     * @deprecated use {@link SQLElementLinkSetup#setType(LinkType, ReferenceAction)}
722
     * @deprecated use {@link SQLElementLinkSetup#setType(LinkType, ReferenceAction)}
721
     */
723
     */
722
    protected void ffInited() {
724
    protected void ffInited() {
723
        // MAYBE use DELETE_RULE of Link
725
        // MAYBE use DELETE_RULE of Link
724
    }
726
    }
725
 
727
 
726
    private final Set<SQLField> getSingleFields(final SQLElementLinks links, final LinkType type) {
728
    private final Set<SQLField> getSingleFields(final SQLElementLinks links, final LinkType type) {
727
        final Set<SQLField> res = new HashSet<SQLField>();
729
        final Set<SQLField> res = new HashSet<SQLField>();
728
        for (final SQLElementLink l : links.getByType(type)) {
730
        for (final SQLElementLink l : links.getByType(type)) {
729
            final SQLField singleField = l.getSingleField();
731
            final SQLField singleField = l.getSingleField();
730
            if (singleField == null)
732
            if (singleField == null)
731
                throw new IllegalStateException("Not single field : " + l);
733
                throw new IllegalStateException("Not single field : " + l);
732
            res.add(singleField);
734
            res.add(singleField);
733
        }
735
        }
734
        return res;
736
        return res;
735
    }
737
    }
736
 
738
 
737
    private synchronized void initRF() {
739
    private synchronized void initRF() {
738
        if (this.otherLinks != null)
740
        if (this.otherLinks != null)
739
            return;
741
            return;
740
 
742
 
741
        final Set<Path> otherPaths = this.createPaths(false);
743
        final Set<Path> otherPaths = this.createPaths(false);
742
        if (otherPaths.isEmpty()) {
744
        if (otherPaths.isEmpty()) {
743
            this.otherLinks = SQLElementLinks.empty();
745
            this.otherLinks = SQLElementLinks.empty();
744
        } else {
746
        } else {
745
            final SetMap<LinkType, SQLElementLink> tmp = new SetMap<LinkType, SQLElementLink>();
747
            final SetMap<LinkType, SQLElementLink> tmp = new SetMap<LinkType, SQLElementLink>();
746
            for (final Path p : otherPaths) {
748
            for (final Path p : otherPaths) {
747
                final SQLElement refElem = this.getElementLenient(p.getFirst());
749
                final SQLElement refElem = this.getElementLenient(p.getFirst());
748
                final SQLElementLink elementLink;
750
                final SQLElementLink elementLink;
749
                if (refElem == null) {
751
                if (refElem == null) {
750
                    // RESTRICT : play it safe
752
                    // RESTRICT : play it safe
751
                    elementLink = new SQLElementLink(null, p, this, LinkType.ASSOCIATION, null, ReferenceAction.RESTRICT);
753
                    elementLink = new SQLElementLink(null, p, this, LinkType.ASSOCIATION, null, ReferenceAction.RESTRICT);
752
                } else {
754
                } else {
753
                    elementLink = refElem.getOwnedLinks().getByPath(p);
755
                    elementLink = refElem.getOwnedLinks().getByPath(p);
754
                    assert elementLink.getOwned() == this;
756
                    assert elementLink.getOwned() == this;
755
                }
757
                }
756
                tmp.add(elementLink.getLinkType(), elementLink);
758
                tmp.add(elementLink.getLinkType(), elementLink);
757
            }
759
            }
758
            this.otherLinks = new SQLElementLinks(tmp);
760
            this.otherLinks = new SQLElementLinks(tmp);
759
        }
761
        }
760
    }
762
    }
761
 
763
 
762
    final void setDirectory(final SQLElementDirectory directory) {
764
    final void setDirectory(final SQLElementDirectory directory) {
763
        // since this method should only be called at the end of SQLElementDirectory.addSQLElement()
765
        // since this method should only be called at the end of SQLElementDirectory.addSQLElement()
764
        assert directory == null || directory.getElement(this.getTable()) == this;
766
        assert directory == null || directory.getElement(this.getTable()) == this;
765
        synchronized (this) {
767
        synchronized (this) {
766
            if (this.directory != directory) {
768
            if (this.directory != directory) {
767
                if (this.areRelationshipsInited())
769
                if (this.areRelationshipsInited())
768
                    this.resetRelationships();
770
                    this.resetRelationships();
769
                this.directory = directory;
771
                this.directory = directory;
770
            }
772
            }
771
        }
773
        }
772
    }
774
    }
773
 
775
 
774
    public synchronized final SQLElementDirectory getDirectory() {
776
    public synchronized final SQLElementDirectory getDirectory() {
775
        return this.directory;
777
        return this.directory;
776
    }
778
    }
777
 
779
 
778
    final SQLElement getElement(SQLTable table) {
780
    final SQLElement getElement(SQLTable table) {
779
        final SQLElement res = getElementLenient(table);
781
        final SQLElement res = getElementLenient(table);
780
        if (res == null)
782
        if (res == null)
781
            throw new IllegalStateException("no element for " + table.getSQLName());
783
            throw new IllegalStateException("no element for " + table.getSQLName());
782
        return res;
784
        return res;
783
    }
785
    }
784
 
786
 
785
    final SQLElement getElementLenient(SQLTable table) {
787
    final SQLElement getElementLenient(SQLTable table) {
786
        synchronized (this) {
788
        synchronized (this) {
787
            return this.getDirectory().getElement(table);
789
            return this.getDirectory().getElement(table);
788
        }
790
        }
789
    }
791
    }
790
 
792
 
791
    public final SQLElement getForeignElement(String foreignField) {
793
    public final SQLElement getForeignElement(String foreignField) {
792
        try {
794
        try {
793
            return this.getElement(this.getForeignTable(foreignField));
795
            return this.getElement(this.getForeignTable(foreignField));
794
        } catch (RuntimeException e) {
796
        } catch (RuntimeException e) {
795
            throw new IllegalStateException("no element for " + foreignField + " in " + this, e);
797
            throw new IllegalStateException("no element for " + foreignField + " in " + this, e);
796
        }
798
        }
797
    }
799
    }
798
 
800
 
799
    private final SQLTable getForeignTable(String foreignField) {
801
    private final SQLTable getForeignTable(String foreignField) {
800
        return this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(foreignField));
802
        return this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(foreignField));
801
    }
803
    }
802
 
804
 
803
    /**
805
    /**
804
     * Set the default name, used if no translations could be found.
806
     * Set the default name, used if no translations could be found.
805
     * 
807
     * 
806
     * @param name the default name, if <code>null</code> the {@link #getTable() table} name will be
808
     * @param name the default name, if <code>null</code> the {@link #getTable() table} name will be
807
     *        used.
809
     *        used.
808
     */
810
     */
809
    public final synchronized void setDefaultName(Phrase name) {
811
    public final synchronized void setDefaultName(Phrase name) {
810
        this.defaultName = name != null ? name : Phrase.getInvariant(getTable().getName());
812
        this.defaultName = name != null ? name : Phrase.getInvariant(getTable().getName());
811
    }
813
    }
812
 
814
 
813
    /**
815
    /**
814
     * The default name.
816
     * The default name.
815
     * 
817
     * 
816
     * @return the default name, never <code>null</code>.
818
     * @return the default name, never <code>null</code>.
817
     */
819
     */
818
    public final synchronized Phrase getDefaultName() {
820
    public final synchronized Phrase getDefaultName() {
819
        return this.defaultName;
821
        return this.defaultName;
820
    }
822
    }
821
 
823
 
822
    /**
824
    /**
823
     * The name of this element in the current locale.
825
     * The name of this element in the current locale.
824
     * 
826
     * 
825
     * @return the name of this, {@link #getDefaultName()} if there's no {@link #getDirectory()
827
     * @return the name of this, {@link #getDefaultName()} if there's no {@link #getDirectory()
826
     *         directory} or if it hasn't a name for this.
828
     *         directory} or if it hasn't a name for this.
827
     * @see SQLElementDirectory#getName(SQLElement)
829
     * @see SQLElementDirectory#getName(SQLElement)
828
     */
830
     */
829
    public final Phrase getName() {
831
    public final Phrase getName() {
830
        final SQLElementDirectory dir = this.getDirectory();
832
        final SQLElementDirectory dir = this.getDirectory();
831
        final SQLFieldTranslator trns = dir == null ? null : dir.getTranslator();
833
        final SQLFieldTranslator trns = dir == null ? null : dir.getTranslator();
832
        final Phrase res = trns == null ? null : trns.getElementName(this);
834
        final Phrase res = trns == null ? null : trns.getElementName(this);
833
        return res == null ? this.getDefaultName() : res;
835
        return res == null ? this.getDefaultName() : res;
834
    }
836
    }
835
 
837
 
836
    public String getPluralName() {
838
    public String getPluralName() {
837
        return this.getName().getVariant(Grammar.PLURAL);
839
        return this.getName().getVariant(Grammar.PLURAL);
838
    }
840
    }
839
 
841
 
840
    public String getSingularName() {
842
    public String getSingularName() {
841
        return this.getName().getVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR);
843
        return this.getName().getVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR);
842
    }
844
    }
843
 
845
 
844
    public ListMap<String, String> getShowAs() {
846
    public ListMap<String, String> getShowAs() {
845
        // nothing by default
847
        // nothing by default
846
        return null;
848
        return null;
847
    }
849
    }
848
 
850
 
849
    /**
851
    /**
850
     * Fields that can neither be inserted nor updated.
852
     * Fields that can neither be inserted nor updated.
851
     * 
853
     * 
852
     * @return fields that cannot be modified.
854
     * @return fields that cannot be modified.
853
     */
855
     */
854
    public Set<String> getReadOnlyFields() {
856
    public Set<String> getReadOnlyFields() {
855
        return Collections.emptySet();
857
        return Collections.emptySet();
856
    }
858
    }
857
 
859
 
858
    /**
860
    /**
859
     * Fields that can only be set on insertion.
861
     * Fields that can only be set on insertion.
860
     * 
862
     * 
861
     * @return fields that cannot be modified.
863
     * @return fields that cannot be modified.
862
     */
864
     */
863
    public Set<String> getInsertOnlyFields() {
865
    public Set<String> getInsertOnlyFields() {
864
        return Collections.emptySet();
866
        return Collections.emptySet();
865
    }
867
    }
866
 
868
 
867
    private synchronized final SQLCache<SQLRow, Object> getModelCache() {
869
    private synchronized final SQLCache<SQLRow, Object> getModelCache() {
868
        if (this.modelCache == null)
870
        if (this.modelCache == null)
869
            this.modelCache = new SQLCache<SQLRow, Object>(60, -1, "modelObjects of " + this.getCode());
871
            this.modelCache = new SQLCache<SQLRow, Object>(60, -1, "modelObjects of " + this.getCode());
870
        return this.modelCache;
872
        return this.modelCache;
871
    }
873
    }
872
 
874
 
873
    // *** update
875
    // *** update
874
 
876
 
875
    /**
877
    /**
876
     * Compute the necessary steps to transform <code>from</code> into <code>to</code>.
878
     * Compute the necessary steps to transform <code>from</code> into <code>to</code>.
877
     * 
879
     * 
878
     * @param from the row currently in the db.
880
     * @param from the row currently in the db.
879
     * @param to the new values.
881
     * @param to the new values.
880
     * @return the script transforming <code>from</code> into <code>to</code>.
882
     * @return the script transforming <code>from</code> into <code>to</code>.
881
     */
883
     */
882
    public final UpdateScript update(SQLRowValues from, SQLRowValues to) {
884
    public final UpdateScript update(SQLRowValues from, SQLRowValues to) {
883
        return this.update(from, to, false);
885
        return this.update(from, to, false);
884
    }
886
    }
885
 
887
 
886
    public final UpdateScript update(final SQLRowValues from, final SQLRowValues to, final boolean allowedToChangeTo) {
888
    public final UpdateScript update(final SQLRowValues from, final SQLRowValues to, final boolean allowedToChangeTo) {
887
        return this.update(from, to, allowedToChangeTo, Transformer.<SQLRowValues> nopTransformer());
889
        return this.update(from, to, allowedToChangeTo, Transformer.<SQLRowValues> nopTransformer());
888
    }
890
    }
889
 
891
 
890
    private final UpdateScript update(final SQLRowValues from, SQLRowValues to, boolean allowedToChangeTo, ITransformer<SQLRowValues, SQLRowValues> copy2originalRows) {
892
    private final UpdateScript update(final SQLRowValues from, SQLRowValues to, boolean allowedToChangeTo, ITransformer<SQLRowValues, SQLRowValues> copy2originalRows) {
891
        check(from);
893
        check(from);
892
        check(to);
894
        check(to);
893
 
895
 
894
        for (final SQLRowValues v : from.getGraph().getItems()) {
896
        for (final SQLRowValues v : from.getGraph().getItems()) {
895
            if (!v.hasID())
897
            if (!v.hasID())
896
                throw new IllegalArgumentException("missing id in " + v + " : " + from.printGraph());
898
                throw new IllegalArgumentException("missing id in " + v + " : " + from.printGraph());
897
        }
899
        }
898
        if (!to.hasID()) {
900
        if (!to.hasID()) {
899
            if (!allowedToChangeTo) {
901
            if (!allowedToChangeTo) {
900
                final Map<SQLRowValues, SQLRowValues> copied = to.getGraph().deepCopy(false);
902
                final Map<SQLRowValues, SQLRowValues> copied = to.getGraph().deepCopy(false);
901
                to = copied.get(to);
903
                to = copied.get(to);
902
                allowedToChangeTo = true;
904
                allowedToChangeTo = true;
903
                copy2originalRows = Transformer.fromMap(CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), copied));
905
                copy2originalRows = Transformer.fromMap(CollectionUtils.invertMap(new IdentityHashMap<SQLRowValues, SQLRowValues>(), copied));
904
            }
906
            }
905
            // from already exists in the DB, so if we're re-using it for another row, all
907
            // from already exists in the DB, so if we're re-using it for another row, all
906
            // non-provided fields must be reset
908
            // non-provided fields must be reset
907
            to.fillWith(SQLRowValues.SQL_DEFAULT, false);
909
            to.fillWith(SQLRowValues.SQL_DEFAULT, false);
908
            to.setPrimaryKey(from);
910
            to.setPrimaryKey(from);
909
        }
911
        }
910
        if (from.getID() != to.getID())
912
        if (from.getID() != to.getID())
911
            throw new IllegalArgumentException("not the same row: " + from + " != " + to);
913
            throw new IllegalArgumentException("not the same row: " + from + " != " + to);
912
 
914
 
913
        final UpdateScript res = new UpdateScript(this, from, copy2originalRows.transformChecked(to));
915
        final UpdateScript res = new UpdateScript(this, from, copy2originalRows.transformChecked(to));
914
        // local values and foreign links
916
        // local values and foreign links
915
        for (final FieldGroup group : to.getFieldGroups()) {
917
        for (final FieldGroup group : to.getFieldGroups()) {
916
            if (group.getKeyType() != Type.FOREIGN_KEY) {
918
            if (group.getKeyType() != Type.FOREIGN_KEY) {
917
                // i.e. primary key or normal field
919
                // i.e. primary key or normal field
918
                res.getUpdateRow().putAll(to.getAbsolutelyAll(), group.getFields());
920
                res.getUpdateRow().putAll(to.getAbsolutelyAll(), group.getFields());
919
            } else {
921
            } else {
920
                final SQLKey k = group.getKey();
922
                final SQLKey k = group.getKey();
921
                if (k.getFields().size() > 1)
923
                if (k.getFields().size() > 1)
922
                    throw new IllegalStateException("Multi-field not supported : " + k);
924
                    throw new IllegalStateException("Multi-field not supported : " + k);
923
                final String field = group.getSingleField();
925
                final String field = group.getSingleField();
924
                assert field != null;
926
                assert field != null;
925
 
927
 
926
                final Path p = new PathBuilder(getTable()).add(k.getForeignLink(), Direction.FOREIGN).build();
928
                final Path p = new PathBuilder(getTable()).add(k.getForeignLink(), Direction.FOREIGN).build();
927
                final SQLElementLink elemLink = this.getOwnedLinks().getByPath(p);
929
                final SQLElementLink elemLink = this.getOwnedLinks().getByPath(p);
928
                if (elemLink.getLinkType() == LinkType.COMPOSITION) {
930
                if (elemLink.getLinkType() == LinkType.COMPOSITION) {
929
                    final SQLElement privateElem = elemLink.getOwned();
931
                    final SQLElement privateElem = elemLink.getOwned();
930
                    final Object fromPrivate = from.getObject(field);
932
                    final Object fromPrivate = from.getObject(field);
931
                    final Object toPrivate = to.getObject(field);
933
                    final Object toPrivate = to.getObject(field);
932
                    assert !from.isDefault(field) : "A row in the DB cannot have DEFAULT";
934
                    assert !from.isDefault(field) : "A row in the DB cannot have DEFAULT";
933
                    final boolean fromIsEmpty = from.isForeignEmpty(field);
935
                    final boolean fromIsEmpty = from.isForeignEmpty(field);
934
                    // as checked in initFF() the default for a private is empty
936
                    // as checked in initFF() the default for a private is empty
935
                    final boolean toIsEmpty = to.isDefault(field) || to.isForeignEmpty(field);
937
                    final boolean toIsEmpty = to.isDefault(field) || to.isForeignEmpty(field);
936
                    if (fromIsEmpty && toIsEmpty) {
938
                    if (fromIsEmpty && toIsEmpty) {
937
                        // nothing to do, don't add to v
939
                        // nothing to do, don't add to v
938
                    } else if (fromIsEmpty) {
940
                    } else if (fromIsEmpty) {
939
                        final SQLRowValues toPR = (SQLRowValues) toPrivate;
941
                        final SQLRowValues toPR = (SQLRowValues) toPrivate;
940
                        // insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouillé"}
942
                        // insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouillé"}
941
                        // clear referents otherwise we will merge the updateRow with the to
943
                        // clear referents otherwise we will merge the updateRow with the to
942
                        // graph (toPR being a private is pointed to by its owner, which itself
944
                        // graph (toPR being a private is pointed to by its owner, which itself
943
                        // points to others, but we just want the private)
945
                        // points to others, but we just want the private)
944
                        assert CollectionUtils.getSole(toPR.getReferentRows(elemLink.getSingleField())) == to : "Shared private " + toPR.printGraph();
946
                        assert CollectionUtils.getSole(toPR.getReferentRows(elemLink.getSingleField())) == to : "Shared private " + toPR.printGraph();
945
                        final SQLRowValues copy = toPR.deepCopy().removeReferents(elemLink.getSingleField());
947
                        final SQLRowValues copy = toPR.deepCopy().removeReferents(elemLink.getSingleField());
946
                        res.getUpdateRow().put(field, copy);
948
                        res.getUpdateRow().put(field, copy);
947
                        res.mapRow(copy2originalRows.transformChecked(toPR), copy);
949
                        res.mapRow(copy2originalRows.transformChecked(toPR), copy);
948
                    } else if (toIsEmpty) {
950
                    } else if (toIsEmpty) {
949
                        // cut and archive
951
                        // cut and archive
950
                        res.getUpdateRow().putEmptyLink(field);
952
                        res.getUpdateRow().putEmptyLink(field);
951
                        res.addToArchive(privateElem, from.getForeign(field));
953
                        res.addToArchive(privateElem, from.getForeign(field));
952
                    } else {
954
                    } else {
953
                        // neither is empty
955
                        // neither is empty
954
                        final Number fromForeignID = from.getForeignIDNumber(field);
956
                        final Number fromForeignID = from.getForeignIDNumber(field);
955
                        if (fromForeignID == null)
957
                        if (fromForeignID == null)
956
                            throw new IllegalArgumentException("Non-empty private in old row, but null ID for " + elemLink);
958
                            throw new IllegalArgumentException("Non-empty private in old row, but null ID for " + elemLink);
957
                        if (toPrivate == null)
959
                        if (toPrivate == null)
958
                            throw new IllegalArgumentException("Non-empty private in new row, but null value for " + elemLink);
960
                            throw new IllegalArgumentException("Non-empty private in new row, but null value for " + elemLink);
959
                        assert toPrivate instanceof Number || toPrivate instanceof SQLRowValues;
961
                        assert toPrivate instanceof Number || toPrivate instanceof SQLRowValues;
960
                        // with the above check, toForeignID is null if and only if toPrivate is an
962
                        // with the above check, toForeignID is null if and only if toPrivate is an
961
                        // SQLRowValues without ID
963
                        // SQLRowValues without ID
962
                        final Number toForeignID = to.getForeignIDNumber(field);
964
                        final Number toForeignID = to.getForeignIDNumber(field);
963
 
965
 
964
                        // if it's desired in the future, don't forget to only re-use ID if there's
966
                        // if it's desired in the future, don't forget to only re-use ID if there's
965
                        // no reference to this private
967
                        // no reference to this private
966
                        if (toForeignID != null && !NumberUtils.areNumericallyEqual(fromForeignID, toForeignID))
968
                        if (toForeignID != null && !NumberUtils.areNumericallyEqual(fromForeignID, toForeignID))
967
                            throw new IllegalArgumentException("private have changed for " + field + " : " + fromPrivate + " != " + toPrivate);
969
                            throw new IllegalArgumentException("private have changed for " + field + " : " + fromPrivate + " != " + toPrivate);
968
                        if (toPrivate instanceof SQLRowValues) {
970
                        if (toPrivate instanceof SQLRowValues) {
969
                            if (!(fromPrivate instanceof SQLRowValues))
971
                            if (!(fromPrivate instanceof SQLRowValues))
970
                                throw new IllegalArgumentException("Asymetric graph, old row doesn't contain a row for " + elemLink + " : " + fromPrivate);
972
                                throw new IllegalArgumentException("Asymetric graph, old row doesn't contain a row for " + elemLink + " : " + fromPrivate);
971
                            final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
973
                            final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
972
                            final SQLRowValues toPR = (SQLRowValues) toPrivate;
974
                            final SQLRowValues toPR = (SQLRowValues) toPrivate;
973
                            // must have same ID
975
                            // must have same ID
974
                            res.put(field, privateElem.update(fromPR, toPR, allowedToChangeTo, copy2originalRows));
976
                            res.put(field, privateElem.update(fromPR, toPR, allowedToChangeTo, copy2originalRows));
975
                        } else {
977
                        } else {
976
                            // if toPrivate is just an ID and the same as fromPrivate, nothing to do
978
                            // if toPrivate is just an ID and the same as fromPrivate, nothing to do
977
                            assert toPrivate instanceof Number && toForeignID != null;
979
                            assert toPrivate instanceof Number && toForeignID != null;
978
                        }
980
                        }
979
                    }
981
                    }
980
                } else if (to.isDefault(field)) {
982
                } else if (to.isDefault(field)) {
981
                    res.getUpdateRow().putDefault(field);
983
                    res.getUpdateRow().putDefault(field);
982
                } else {
984
                } else {
983
                    res.getUpdateRow().put(field, to.getForeignIDNumber(field));
985
                    res.getUpdateRow().put(field, to.getForeignIDNumber(field));
984
                }
986
                }
985
            }
987
            }
986
        }
988
        }
987
        // now owned referents
989
        // now owned referents
988
        for (final SQLElementLink elemLink : this.getOwnedLinks().getByPath().values()) {
990
        for (final SQLElementLink elemLink : this.getOwnedLinks().getByPath().values()) {
989
            if (elemLink.isJoin()) {
991
            if (elemLink.isJoin()) {
990
                final Path pathToFK = elemLink.getPath().minusLast();
992
                final Path pathToFK = elemLink.getPath().minusLast();
991
                final Set<String> joinTableLocalContentFields = pathToFK.getLast().getFieldsNames(VirtualFields.LOCAL_CONTENT);
993
                final Set<String> joinTableLocalContentFields = pathToFK.getLast().getFieldsNames(VirtualFields.LOCAL_CONTENT);
992
                if (elemLink.getLinkType() == LinkType.COMPOSITION) {
994
                if (elemLink.getLinkType() == LinkType.COMPOSITION) {
993
                    final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> fromPrivatesTuple = indexRows(from.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false));
995
                    final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> fromPrivatesTuple = indexRows(from.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false));
994
                    // already checked at the start of the method
996
                    // already checked at the start of the method
995
                    assert fromPrivatesTuple.get0().isEmpty() : "Existing rows without ID : " + fromPrivatesTuple.get0();
997
                    assert fromPrivatesTuple.get0().isEmpty() : "Existing rows without ID : " + fromPrivatesTuple.get0();
996
                    final Map<Number, SQLRowValues> fromPrivates = fromPrivatesTuple.get1();
998
                    final Map<Number, SQLRowValues> fromPrivates = fromPrivatesTuple.get1();
997
                    BigDecimal minOrder = null, maxOrder = null;
999
                    BigDecimal minOrder = null, maxOrder = null;
998
                    for (final SQLRowValues fromJoin : from.followPath(pathToFK, CreateMode.CREATE_NONE, false)) {
1000
                    for (final SQLRowValues fromJoin : from.followPath(pathToFK, CreateMode.CREATE_NONE, false)) {
999
                        final BigDecimal order = fromJoin.getOrder();
1001
                        final BigDecimal order = fromJoin.getOrder();
1000
                        assert order != null;
1002
                        assert order != null;
1001
                        if (minOrder == null || minOrder.compareTo(order) > 0)
1003
                        if (minOrder == null || minOrder.compareTo(order) > 0)
1002
                            minOrder = order;
1004
                            minOrder = order;
1003
                        if (maxOrder == null || maxOrder.compareTo(order) < 0)
1005
                        if (maxOrder == null || maxOrder.compareTo(order) < 0)
1004
                            maxOrder = order;
1006
                            maxOrder = order;
1005
                    }
1007
                    }
1006
 
1008
 
1007
                    final Collection<SQLRowValues> toValues = to.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false);
1009
                    final Collection<SQLRowValues> toValues = to.followPath(elemLink.getPath(), CreateMode.CREATE_NONE, false);
1008
                    final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> toPrivatesTuple = indexRows(toValues);
1010
                    final Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> toPrivatesTuple = indexRows(toValues);
1009
                    final Map<Number, SQLRowValues> toPrivates = toPrivatesTuple.get1();
1011
                    final Map<Number, SQLRowValues> toPrivates = toPrivatesTuple.get1();
1010
                    /*
1012
                    /*
1011
                     * Order of joined rows are dictated by their join rows. To avoid needing
1013
                     * Order of joined rows are dictated by their join rows. To avoid needing
1012
                     * knowledge from the entire table, join orders are unique only among their
1014
                     * knowledge from the entire table, join orders are unique only among their
1013
                     * owner.
1015
                     * owner.
1014
                     */
1016
                     */
1015
                    /*
1017
                    /*
1016
                     * Since almost no DB has deferrable constraints, use non-overlapping order : if
1018
                     * Since almost no DB has deferrable constraints, use non-overlapping order : if
1017
                     * there's space before minOrder, then use it, otherwise use space after
1019
                     * there's space before minOrder, then use it, otherwise use space after
1018
                     * maxOrder.
1020
                     * maxOrder.
1019
                     */
1021
                     */
1020
                    BigDecimal toOrder;
1022
                    BigDecimal toOrder;
1021
                    if (minOrder == null || BigDecimal.valueOf(toValues.size()).compareTo(minOrder) < 0) {
1023
                    if (minOrder == null || BigDecimal.valueOf(toValues.size()).compareTo(minOrder) < 0) {
1022
                        toOrder = BigDecimal.ONE;
1024
                        toOrder = BigDecimal.ONE;
1023
                    } else {
1025
                    } else {
1024
                        assert maxOrder != null : "Minimum order isn't null but maximum order is";
1026
                        assert maxOrder != null : "Minimum order isn't null but maximum order is";
1025
                        toOrder = maxOrder.add(BigDecimal.ONE);
1027
                        toOrder = maxOrder.add(BigDecimal.ONE);
1026
                    }
1028
                    }
1027
                    final Map<SQLRowValues, BigDecimal> toPrivatesOrder = new IdentityHashMap<>();
1029
                    final Map<SQLRowValues, BigDecimal> toPrivatesOrder = new IdentityHashMap<>();
1028
                    for (final SQLRowValues toVals : toValues) {
1030
                    for (final SQLRowValues toVals : toValues) {
1029
                        toPrivatesOrder.put(toVals, toOrder);
1031
                        toPrivatesOrder.put(toVals, toOrder);
1030
                        toOrder = toOrder.add(BigDecimal.ONE);
1032
                        toOrder = toOrder.add(BigDecimal.ONE);
1031
                    }
1033
                    }
1032
 
1034
 
1033
                    final List<Number> onlyInFrom = new ArrayList<Number>(fromPrivates.keySet());
1035
                    final List<Number> onlyInFrom = new ArrayList<Number>(fromPrivates.keySet());
1034
                    onlyInFrom.removeAll(toPrivates.keySet());
1036
                    onlyInFrom.removeAll(toPrivates.keySet());
1035
                    final Set<Number> onlyInTo = new HashSet<Number>(toPrivates.keySet());
1037
                    final Set<Number> onlyInTo = new HashSet<Number>(toPrivates.keySet());
1036
                    onlyInTo.removeAll(fromPrivates.keySet());
1038
                    onlyInTo.removeAll(fromPrivates.keySet());
1037
                    final Set<Number> inFromAndTo = new HashSet<Number>(toPrivates.keySet());
1039
                    final Set<Number> inFromAndTo = new HashSet<Number>(toPrivates.keySet());
1038
                    inFromAndTo.retainAll(fromPrivates.keySet());
1040
                    inFromAndTo.retainAll(fromPrivates.keySet());
1039
 
1041
 
1040
                    if (!onlyInTo.isEmpty())
1042
                    if (!onlyInTo.isEmpty())
1041
                        throw new IllegalStateException("Unknown IDs : " + onlyInTo + " for " + elemLink + " from IDs : " + fromPrivates);
1043
                        throw new IllegalStateException("Unknown IDs : " + onlyInTo + " for " + elemLink + " from IDs : " + fromPrivates);
1042
 
1044
 
1043
                    // pair of rows (old row then new row) with the same ID or with the new row
1045
                    // pair of rows (old row then new row) with the same ID or with the new row
1044
                    // lacking and ID
1046
                    // lacking and ID
1045
                    final List<SQLRowValues> matchedPrivates = new ArrayList<SQLRowValues>();
1047
                    final List<SQLRowValues> matchedPrivates = new ArrayList<SQLRowValues>();
1046
                    for (final Number inBoth : inFromAndTo) {
1048
                    for (final Number inBoth : inFromAndTo) {
1047
                        matchedPrivates.add(fromPrivates.get(inBoth));
1049
                        matchedPrivates.add(fromPrivates.get(inBoth));
1048
                        matchedPrivates.add(toPrivates.get(inBoth));
1050
                        matchedPrivates.add(toPrivates.get(inBoth));
1049
                    }
1051
                    }
1050
 
1052
 
1051
                    final SQLElement privateElem = elemLink.getOwned();
1053
                    final SQLElement privateElem = elemLink.getOwned();
1052
                    final boolean hasReferences = !privateElem.getLinksOwnedByOthers().getByType(LinkType.ASSOCIATION).isEmpty();
1054
                    final boolean hasReferences = !privateElem.getLinksOwnedByOthers().getByType(LinkType.ASSOCIATION).isEmpty();
1053
                    final SQLField toMainField = elemLink.getPath().getStep(0).getSingleField();
1055
                    final SQLField toMainField = elemLink.getPath().getStep(0).getSingleField();
1054
                    final SQLField toPrivateField = elemLink.getPath().getStep(-1).getSingleField();
1056
                    final SQLField toPrivateField = elemLink.getPath().getStep(-1).getSingleField();
1055
                    for (final SQLRowValues privateSansID : toPrivatesTuple.get0()) {
1057
                    for (final SQLRowValues privateSansID : toPrivatesTuple.get0()) {
1056
                        // don't re-use existing ID if there can be rows referencing it
1058
                        // don't re-use existing ID if there can be rows referencing it
1057
                        if (!hasReferences && !onlyInFrom.isEmpty()) {
1059
                        if (!hasReferences && !onlyInFrom.isEmpty()) {
1058
                            matchedPrivates.add(fromPrivates.get(onlyInFrom.remove(0)));
1060
                            matchedPrivates.add(fromPrivates.get(onlyInFrom.remove(0)));
1059
                            matchedPrivates.add(privateSansID);
1061
                            matchedPrivates.add(privateSansID);
1060
                        } else {
1062
                        } else {
1061
                            // insert new, always creating the join row
1063
                            // insert new, always creating the join row
1062
                            final SQLRowValues copy = privateSansID.deepCopy().removeReferents(toPrivateField);
1064
                            final SQLRowValues copy = privateSansID.deepCopy().removeReferents(toPrivateField);
1063
                            res.getUpdateRow().put(elemLink.getPath(), true, copy);
1065
                            res.getUpdateRow().put(elemLink.getPath(), true, copy);
1064
                            final SQLRowValues toJoinRow = CollectionUtils.getSole(privateSansID.getReferentRows(toPrivateField));
1066
                            final SQLRowValues toJoinRow = CollectionUtils.getSole(privateSansID.getReferentRows(toPrivateField));
1065
                            final SQLRowValues joinRow = CollectionUtils.getSole(copy.getReferentRows(toPrivateField));
1067
                            final SQLRowValues joinRow = CollectionUtils.getSole(copy.getReferentRows(toPrivateField));
1066
                            setContentFields(joinTableLocalContentFields, joinRow, toJoinRow);
1068
                            setContentFields(joinTableLocalContentFields, joinRow, toJoinRow);
1067
                            setOrder(joinRow, toPrivatesOrder.get(privateSansID));
1069
                            setOrder(joinRow, toPrivatesOrder.get(privateSansID));
1068
                            res.mapRow(copy2originalRows.transformChecked(privateSansID), copy);
1070
                            res.mapRow(copy2originalRows.transformChecked(privateSansID), copy);
1069
                        }
1071
                        }
1070
                    }
1072
                    }
1071
 
1073
 
1072
                    final Iterator<SQLRowValues> iter = matchedPrivates.iterator();
1074
                    final Iterator<SQLRowValues> iter = matchedPrivates.iterator();
1073
                    while (iter.hasNext()) {
1075
                    while (iter.hasNext()) {
1074
                        final SQLRowValues fromPrivate = iter.next();
1076
                        final SQLRowValues fromPrivate = iter.next();
1075
                        final SQLRowValues toPrivate = iter.next();
1077
                        final SQLRowValues toPrivate = iter.next();
1076
 
1078
 
1077
                        final SQLRowValues fromJoin = CollectionUtils.getSole(fromPrivate.getReferentRows(toPrivateField));
1079
                        final SQLRowValues fromJoin = CollectionUtils.getSole(fromPrivate.getReferentRows(toPrivateField));
1078
                        if (fromJoin == null)
1080
                        if (fromJoin == null)
1079
                            throw new IllegalStateException("Shared private " + fromPrivate.printGraph());
1081
                            throw new IllegalStateException("Shared private " + fromPrivate.printGraph());
1080
                        final SQLRowValues toJoin = CollectionUtils.getSole(toPrivate.getReferentRows(toPrivateField));
1082
                        final SQLRowValues toJoin = CollectionUtils.getSole(toPrivate.getReferentRows(toPrivateField));
1081
                        final UpdateScript updateScript = privateElem.update(fromPrivate, toPrivate, allowedToChangeTo, copy2originalRows);
1083
                        final UpdateScript updateScript = privateElem.update(fromPrivate, toPrivate, allowedToChangeTo, copy2originalRows);
1082
 
1084
 
1083
                        final SQLRowValues joinCopy = new SQLRowValues(fromJoin.getTable());
1085
                        final SQLRowValues joinCopy = new SQLRowValues(fromJoin.getTable());
1084
                        joinCopy.setID(fromJoin.getIDNumber());
1086
                        joinCopy.setID(fromJoin.getIDNumber());
1085
                        assert joinCopy.getGraphSize() == 1;
1087
                        assert joinCopy.getGraphSize() == 1;
1086
                        setContentFields(joinTableLocalContentFields, joinCopy, toJoin);
1088
                        setContentFields(joinTableLocalContentFields, joinCopy, toJoin);
1087
                        setOrder(joinCopy, toPrivatesOrder.get(toPrivate));
1089
                        setOrder(joinCopy, toPrivatesOrder.get(toPrivate));
1088
                        joinCopy.put(toMainField.getName(), res.getUpdateRow());
1090
                        joinCopy.put(toMainField.getName(), res.getUpdateRow());
1089
                        joinCopy.put(toPrivateField.getName(), updateScript.getUpdateRow());
1091
                        joinCopy.put(toPrivateField.getName(), updateScript.getUpdateRow());
1090
                        res.add(updateScript);
1092
                        res.add(updateScript);
1091
                    }
1093
                    }
1092
 
1094
 
1093
                    for (final Number id : onlyInFrom) {
1095
                    for (final Number id : onlyInFrom) {
1094
                        // this will also cut the link from the main row
1096
                        // this will also cut the link from the main row
1095
                        res.addToArchive(privateElem, fromPrivates.get(id));
1097
                        res.addToArchive(privateElem, fromPrivates.get(id));
1096
                    }
1098
                    }
1097
                } else {
1099
                } else {
1098
                    final Step fkStep = elemLink.getPath().getStep(-1);
1100
                    final Step fkStep = elemLink.getPath().getStep(-1);
1099
                    final String fkField = fkStep.getSingleField().getName();
1101
                    final String fkField = fkStep.getSingleField().getName();
1100
                    final List<SQLRowValues> fromFKs = new ArrayList<SQLRowValues>(from.followPath(pathToFK, CreateMode.CREATE_NONE, false));
1102
                    final List<SQLRowValues> fromFKs = new ArrayList<SQLRowValues>(from.followPath(pathToFK, CreateMode.CREATE_NONE, false));
1101
                    final List<SQLRowValues> toFKs = new ArrayList<SQLRowValues>(to.followPath(pathToFK, CreateMode.CREATE_NONE, false));
1103
                    final List<SQLRowValues> toFKs = new ArrayList<SQLRowValues>(to.followPath(pathToFK, CreateMode.CREATE_NONE, false));
1102
 
1104
 
1103
                    for (final SQLRowValues rowWithFK : toFKs) {
1105
                    for (final SQLRowValues rowWithFK : toFKs) {
1104
                        final int ownedID = rowWithFK.getForeignID(fkField);
1106
                        final int ownedID = rowWithFK.getForeignID(fkField);
1105
                        final SQLRowValues toUse;
1107
                        final SQLRowValues toUse;
1106
                        if (fromFKs.isEmpty()) {
1108
                        if (fromFKs.isEmpty()) {
1107
                            toUse = res.getUpdateRow().putRowValues(pathToFK, true);
1109
                            toUse = res.getUpdateRow().putRowValues(pathToFK, true);
1108
                        } else {
1110
                        } else {
1109
                            // take first available join
1111
                            // take first available join
1110
                            final SQLRowValues fromJoin = fromFKs.remove(0);
1112
                            final SQLRowValues fromJoin = fromFKs.remove(0);
1111
                            // if its values are what is needed don't update the DB (don't try to
1113
                            // if its values are what is needed don't update the DB (don't try to
1112
                            // compare local field values)
1114
                            // compare local field values)
1113
                            if (ownedID == fromJoin.getForeignID(fkField) && joinTableLocalContentFields.isEmpty()) {
1115
                            if (ownedID == fromJoin.getForeignID(fkField) && joinTableLocalContentFields.isEmpty()) {
1114
                                toUse = null;
1116
                                toUse = null;
1115
                            } else {
1117
                            } else {
1116
                                // copy existing join ID to avoid inserting a new join in the DB
1118
                                // copy existing join ID to avoid inserting a new join in the DB
1117
                                toUse = new SQLRowValues(fromJoin.getTable()).setID(fromJoin.getIDNumber());
1119
                                toUse = new SQLRowValues(fromJoin.getTable()).setID(fromJoin.getIDNumber());
1118
                                res.getUpdateRow().put(elemLink.getPath().getStep(0), toUse);
1120
                                res.getUpdateRow().put(elemLink.getPath().getStep(0), toUse);
1119
                            }
1121
                            }
1120
                        }
1122
                        }
1121
                        if (toUse != null) {
1123
                        if (toUse != null) {
1122
                            setContentFields(joinTableLocalContentFields, toUse, rowWithFK);
1124
                            setContentFields(joinTableLocalContentFields, toUse, rowWithFK);
1123
                            toUse.put(fkField, ownedID);
1125
                            toUse.put(fkField, ownedID);
1124
                            res.mapRow(copy2originalRows.transformChecked(rowWithFK), toUse);
1126
                            res.mapRow(copy2originalRows.transformChecked(rowWithFK), toUse);
1125
                        }
1127
                        }
1126
                    }
1128
                    }
1127
 
1129
 
1128
                    // lastly, delete remaining join rows (don't just archive otherwise if the main
1130
                    // lastly, delete remaining join rows (don't just archive otherwise if the main
1129
                    // row is unarchived it will get back all links from every modification)
1131
                    // row is unarchived it will get back all links from every modification)
1130
                    for (final SQLRowValues rowWithFK : fromFKs) {
1132
                    for (final SQLRowValues rowWithFK : fromFKs) {
1131
                        res.addToDelete(rowWithFK);
1133
                        res.addToDelete(rowWithFK);
1132
                    }
1134
                    }
1133
                }
1135
                }
1134
            } // else foreign link already handled above
1136
            } // else foreign link already handled above
1135
        }
1137
        }
1136
 
1138
 
1137
        return res;
1139
        return res;
1138
    }
1140
    }
1139
 
1141
 
1140
    // first rows without IDs, then those with IDs
1142
    // first rows without IDs, then those with IDs
1141
    static private Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> indexRows(final Collection<SQLRowValues> rows) {
1143
    static private Tuple2<List<SQLRowValues>, Map<Number, SQLRowValues>> indexRows(final Collection<SQLRowValues> rows) {
1142
        final List<SQLRowValues> sansID = new ArrayList<SQLRowValues>();
1144
        final List<SQLRowValues> sansID = new ArrayList<SQLRowValues>();
1143
        final Map<Number, SQLRowValues> map = new HashMap<Number, SQLRowValues>();
1145
        final Map<Number, SQLRowValues> map = new HashMap<Number, SQLRowValues>();
1144
        for (final SQLRowValues r : rows) {
1146
        for (final SQLRowValues r : rows) {
1145
            if (r.hasID()) {
1147
            if (r.hasID()) {
1146
                final SQLRowValues previous = map.put(r.getIDNumber(), r);
1148
                final SQLRowValues previous = map.put(r.getIDNumber(), r);
1147
                if (previous != null)
1149
                if (previous != null)
1148
                    throw new IllegalStateException("Duplicate " + r.asRow());
1150
                    throw new IllegalStateException("Duplicate " + r.asRow());
1149
            } else {
1151
            } else {
1150
                sansID.add(r);
1152
                sansID.add(r);
1151
            }
1153
            }
1152
        }
1154
        }
1153
        return Tuple2.create(sansID, map);
1155
        return Tuple2.create(sansID, map);
1154
    }
1156
    }
1155
 
1157
 
1156
    static private void setOrder(final SQLRowValues row, final BigDecimal order) {
1158
    static private void setOrder(final SQLRowValues row, final BigDecimal order) {
1157
        assert order != null;
1159
        assert order != null;
1158
        row.put(row.getTable().getOrderField().getName(), order);
1160
        row.put(row.getTable().getOrderField().getName(), order);
1159
    }
1161
    }
1160
 
1162
 
1161
    static private void setContentFields(final Set<String> joinTableLocalContentFields, final SQLRowValues newJoinRow, final SQLRowValues rowToStore) {
1163
    static private void setContentFields(final Set<String> joinTableLocalContentFields, final SQLRowValues newJoinRow, final SQLRowValues rowToStore) {
1162
        if (!joinTableLocalContentFields.isEmpty()) {
1164
        if (!joinTableLocalContentFields.isEmpty()) {
1163
            // copy passed LOCAL_CONTENT fields (e.g. label)
1165
            // copy passed LOCAL_CONTENT fields (e.g. label)
1164
            newJoinRow.putAll(rowToStore.getValues(joinTableLocalContentFields));
1166
            newJoinRow.putAll(rowToStore.getValues(joinTableLocalContentFields));
1165
            // reset those not passed
1167
            // reset those not passed
1166
            if (newJoinRow.hasID())
1168
            if (newJoinRow.hasID())
1167
                newJoinRow.fill(joinTableLocalContentFields, SQLRowValues.SQL_DEFAULT, false, true);
1169
                newJoinRow.fill(joinTableLocalContentFields, SQLRowValues.SQL_DEFAULT, false, true);
1168
        }
1170
        }
1169
    }
1171
    }
1170
 
1172
 
1171
    public final void unarchiveNonRec(int id) throws SQLException {
1173
    public final void unarchiveNonRec(int id) throws SQLException {
1172
        this.unarchive(this.getTable().getRow(id), false);
1174
        this.unarchive(this.getTable().getRow(id), false);
1173
    }
1175
    }
1174
 
1176
 
1175
    public final void unarchive(int id) throws SQLException {
1177
    public final void unarchive(int id) throws SQLException {
1176
        this.unarchive(this.getTable().getRow(id));
1178
        this.unarchive(this.getTable().getRow(id));
1177
    }
1179
    }
1178
 
1180
 
1179
    public final void unarchive(final SQLRow row) throws SQLException {
1181
    public final void unarchive(final SQLRow row) throws SQLException {
1180
        this.unarchive(row, true);
1182
        this.unarchive(row, true);
1181
    }
1183
    }
1182
 
1184
 
1183
    public void unarchive(final SQLRow row, final boolean desc) throws SQLException {
1185
    public void unarchive(final SQLRow row, final boolean desc) throws SQLException {
1184
        checkUndefined(row);
1186
        checkUndefined(row);
1185
        // don't test row.isArchived() (it is done by getTree())
1187
        // don't test row.isArchived() (it is done by getTree())
1186
        // to allow an unarchived parent to unarchive all its descendants.
1188
        // to allow an unarchived parent to unarchive all its descendants.
1187
 
1189
 
1188
        // make sure that all fields are loaded
1190
        // make sure that all fields are loaded
1189
        final SQLRow upToDate = row.getTable().getRow(row.getID());
1191
        final SQLRow upToDate = row.getTable().getRow(row.getID());
1190
        // nos descendants
1192
        // nos descendants
1191
        final SQLRowValues descsAndMe = desc ? this.getTree(upToDate, true) : upToDate.asRowValues();
1193
        final SQLRowValues descsAndMe = desc ? this.getTree(upToDate, true) : upToDate.asRowValues();
1192
        final SQLRowValues connectedRows = new ArchivedGraph(this.getDirectory(), descsAndMe).expand();
1194
        final SQLRowValues connectedRows = new ArchivedGraph(this.getDirectory(), descsAndMe).expand();
1193
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
1195
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
1194
            @Override
1196
            @Override
1195
            public Object create() throws SQLException {
1197
            public Object create() throws SQLException {
1196
                setArchive(Collections.singletonList(connectedRows.getGraph()), false);
1198
                setArchive(Collections.singletonList(connectedRows.getGraph()), false);
1197
                return null;
1199
                return null;
1198
            }
1200
            }
1199
        });
1201
        });
1200
    }
1202
    }
1201
 
1203
 
1202
    public final void archive(int id) throws SQLException {
1204
    public final void archive(int id) throws SQLException {
1203
        this.archiveIDs(Collections.singleton(id));
1205
        this.archiveIDs(Collections.singleton(id));
1204
    }
1206
    }
1205
 
1207
 
1206
    public final void archiveIDs(final Collection<? extends Number> ids) throws SQLException {
1208
    public final void archiveIDs(final Collection<? extends Number> ids) throws SQLException {
1207
        this.archive(TreesOfSQLRows.createFromIDs(this, ids), true);
1209
        this.archive(TreesOfSQLRows.createFromIDs(this, ids), true);
1208
    }
1210
    }
1209
 
1211
 
1210
    public final void archive(final Collection<? extends SQLRowAccessor> rows) throws SQLException {
1212
    public final void archive(final Collection<? extends SQLRowAccessor> rows) throws SQLException {
1211
        // rows checked by TreesOfSQLRows
1213
        // rows checked by TreesOfSQLRows
1212
        this.archive(new TreesOfSQLRows(this, rows), true);
1214
        this.archive(new TreesOfSQLRows(this, rows), true);
1213
    }
1215
    }
1214
 
1216
 
1215
    public final void archive(SQLRow row) throws SQLException {
1217
    public final void archive(SQLRow row) throws SQLException {
1216
        this.archive(row, true);
1218
        this.archive(row, true);
1217
    }
1219
    }
1218
 
1220
 
1219
    /**
1221
    /**
1220
     * Archive la ligne demandée et tous ses descendants mais ne cherche pas à couper les références
1222
     * Archive la ligne demandée et tous ses descendants mais ne cherche pas à couper les références
1221
     * pointant sur ceux-ci. ATTN peut donc laisser la base dans un état inconsistent, à n'utiliser
1223
     * pointant sur ceux-ci. ATTN peut donc laisser la base dans un état inconsistent, à n'utiliser
1222
     * que si aucun lien ne pointe sur ceux ci. En revanche, accélère grandement (par exemple pour
1224
     * que si aucun lien ne pointe sur ceux ci. En revanche, accélère grandement (par exemple pour
1223
     * OBSERVATION) car pas besoin de chercher toutes les références.
1225
     * OBSERVATION) car pas besoin de chercher toutes les références.
1224
     * 
1226
     * 
1225
     * @param id la ligne voulue.
1227
     * @param id la ligne voulue.
1226
     * @throws SQLException if pb while archiving.
1228
     * @throws SQLException if pb while archiving.
1227
     */
1229
     */
1228
    public final void archiveNoCut(int id) throws SQLException {
1230
    public final void archiveNoCut(int id) throws SQLException {
1229
        this.archive(this.getTable().getRow(id), false);
1231
        this.archive(this.getTable().getRow(id), false);
1230
    }
1232
    }
1231
 
1233
 
1232
    protected void archive(final SQLRow row, final boolean cutLinks) throws SQLException {
1234
    protected void archive(final SQLRow row, final boolean cutLinks) throws SQLException {
1233
        this.archive(new TreesOfSQLRows(this, row), cutLinks);
1235
        this.archive(new TreesOfSQLRows(this, row), cutLinks);
1234
    }
1236
    }
1235
 
1237
 
1236
    protected void archive(final TreesOfSQLRows trees, final boolean cutLinks) throws SQLException {
1238
    protected void archive(final TreesOfSQLRows trees, final boolean cutLinks) throws SQLException {
1237
        if (trees.getElem() != this)
1239
        if (trees.getElem() != this)
1238
            throw new IllegalArgumentException(this + " != " + trees.getElem());
1240
            throw new IllegalArgumentException(this + " != " + trees.getElem());
1239
        if ((trees.isFetched() ? trees.getTrees().keySet() : trees.getRows()).isEmpty())
1241
        if ((trees.isFetched() ? trees.getTrees().keySet() : trees.getRows()).isEmpty())
1240
            return;
1242
            return;
1241
        for (final SQLRow row : trees.getRows())
1243
        for (final SQLRow row : trees.getRows())
1242
            checkUndefined(row);
1244
            checkUndefined(row);
1243
 
1245
 
1244
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
1246
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
1245
            @Override
1247
            @Override
1246
            public Object create() throws SQLException {
1248
            public Object create() throws SQLException {
1247
                if (!trees.isFetched())
1249
                if (!trees.isFetched())
1248
                    trees.fetch(LockStrength.UPDATE);
1250
                    trees.fetch(LockStrength.UPDATE);
1249
                // reference
1251
                // reference
1250
                // d'abord couper les liens qui pointent sur les futurs archivés
1252
                // d'abord couper les liens qui pointent sur les futurs archivés
1251
                if (cutLinks) {
1253
                if (cutLinks) {
1252
                    // TODO prend bcp de temps
1254
                    // TODO prend bcp de temps
1253
                    // FIXME update tableau pour chaque observation, ecrase les changements
1255
                    // FIXME update tableau pour chaque observation, ecrase les changements
1254
                    // faire : 'La base à changée voulez vous recharger ou garder vos modifs ?'
1256
                    // faire : 'La base à changée voulez vous recharger ou garder vos modifs ?'
1255
                    final Map<SQLElementLink, ? extends Collection<SQLRowValues>> externReferences = trees.getExternReferences().getMap();
1257
                    final Map<SQLElementLink, ? extends Collection<SQLRowValues>> externReferences = trees.getExternReferences().getMap();
1256
                    // avoid toString() which might make requests to display rows (eg archived)
1258
                    // avoid toString() which might make requests to display rows (eg archived)
1257
                    if (Log.get().isLoggable(Level.FINEST))
1259
                    if (Log.get().isLoggable(Level.FINEST))
1258
                        Log.get().finest("will cut : " + externReferences);
1260
                        Log.get().finest("will cut : " + externReferences);
1259
                    for (final Entry<SQLElementLink, ? extends Collection<SQLRowValues>> e : externReferences.entrySet()) {
1261
                    for (final Entry<SQLElementLink, ? extends Collection<SQLRowValues>> e : externReferences.entrySet()) {
1260
                        final SQLElementLink linkToCut = e.getKey();
1262
                        final SQLElementLink linkToCut = e.getKey();
1261
                        try {
1263
                        try {
1262
                            if (linkToCut.isJoin()) {
1264
                            if (linkToCut.isJoin()) {
1263
                                final Path joinPath = linkToCut.getPath();
1265
                                final Path joinPath = linkToCut.getPath();
1264
                                final Path toJoinTable = joinPath.minusLast();
1266
                                final Path toJoinTable = joinPath.minusLast();
1265
                                final SQLTable joinTable = toJoinTable.getLast();
1267
                                final SQLTable joinTable = toJoinTable.getLast();
1266
                                assert getElement(joinTable) instanceof JoinSQLElement;
1268
                                assert getElement(joinTable) instanceof JoinSQLElement;
1267
                                final Set<Number> ids = new HashSet<Number>();
1269
                                final Set<Number> ids = new HashSet<Number>();
1268
                                for (final SQLRowValues joinRow : e.getValue()) {
1270
                                for (final SQLRowValues joinRow : e.getValue()) {
1269
                                    assert joinRow.getTable() == joinTable;
1271
                                    assert joinRow.getTable() == joinTable;
1270
                                    ids.add(joinRow.getIDNumber());
1272
                                    ids.add(joinRow.getIDNumber());
1271
                                }
1273
                                }
1272
                                // MAYBE instead of losing the information (as with simple foreign
1274
                                // MAYBE instead of losing the information (as with simple foreign
1273
                                // key), archive it
1275
                                // key), archive it
1274
                                final String query = "DELETE FROM " + joinTable.getSQLName() + " WHERE " + new Where(joinTable.getKey(), ids);
1276
                                final String query = "DELETE FROM " + joinTable.getSQLName() + " WHERE " + new Where(joinTable.getKey(), ids);
1275
                                getTable().getDBSystemRoot().getDataSource().execute(query);
1277
                                getTable().getDBSystemRoot().getDataSource().execute(query);
1276
                                for (final Number id : ids)
1278
                                for (final Number id : ids)
1277
                                    joinTable.fireRowDeleted(id.intValue());
1279
                                    joinTable.fireRowDeleted(id.intValue());
1278
                            } else {
1280
                            } else {
1279
                                final Link refKey = linkToCut.getSingleLink();
1281
                                final Link refKey = linkToCut.getSingleLink();
1280
                                for (final SQLRowAccessor ref : e.getValue()) {
1282
                                for (final SQLRowAccessor ref : e.getValue()) {
1281
                                    ref.createEmptyUpdateRow().putEmptyLink(refKey.getSingleField().getName()).update();
1283
                                    ref.createEmptyUpdateRow().putEmptyLink(refKey.getSingleField().getName()).update();
1282
                                }
1284
                                }
1283
                            }
1285
                            }
1284
                        } catch (Exception e1) {
1286
                        } catch (Exception e1) {
1285
                            throw new SQLException("Couldn't cut " + linkToCut + " in " + trees, e1);
1287
                            throw new SQLException("Couldn't cut " + linkToCut + " in " + trees, e1);
1286
                        }
1288
                        }
1287
                    }
1289
                    }
1288
                    Log.get().finest("done cutting links");
1290
                    Log.get().finest("done cutting links");
1289
                }
1291
                }
1290
 
1292
 
1291
                // on archive tous nos descendants
1293
                // on archive tous nos descendants
1292
                setArchive(trees.getClusters(), true);
1294
                setArchive(trees.getClusters(), true);
1293
 
1295
 
1294
                return null;
1296
                return null;
1295
            }
1297
            }
1296
        });
1298
        });
1297
    }
1299
    }
1298
 
1300
 
1299
    static private final SQLRowValues setArchive(SQLRowValues r, final boolean archive) throws SQLException {
1301
    static private final SQLRowValues setArchive(SQLRowValues r, final boolean archive) throws SQLException {
1300
        final SQLField archiveField = r.getTable().getArchiveField();
1302
        final SQLField archiveField = r.getTable().getArchiveField();
1301
        final Object newVal;
1303
        final Object newVal;
1302
        if (Boolean.class.equals(archiveField.getType().getJavaType()))
1304
        if (Boolean.class.equals(archiveField.getType().getJavaType()))
1303
            newVal = archive;
1305
            newVal = archive;
1304
        else
1306
        else
1305
            newVal = archive ? 1 : 0;
1307
            newVal = archive ? 1 : 0;
1306
        r.put(archiveField.getName(), newVal);
1308
        r.put(archiveField.getName(), newVal);
1307
        return r;
1309
        return r;
1308
    }
1310
    }
1309
 
1311
 
1310
    // all rows will be either archived or unarchived (handling cycles)
1312
    // all rows will be either archived or unarchived (handling cycles)
1311
    static private void setArchive(final Collection<SQLRowValuesCluster> clustersToArchive, final boolean archive) throws SQLException {
1313
    static private void setArchive(final Collection<SQLRowValuesCluster> clustersToArchive, final boolean archive) throws SQLException {
1312
        final Set<SQLRowValues> toArchive = Collections.newSetFromMap(new IdentityHashMap<SQLRowValues, Boolean>());
1314
        final Set<SQLRowValues> toArchive = Collections.newSetFromMap(new IdentityHashMap<SQLRowValues, Boolean>());
1313
        for (final SQLRowValuesCluster c : clustersToArchive)
1315
        for (final SQLRowValuesCluster c : clustersToArchive)
1314
            toArchive.addAll(c.getItems());
1316
            toArchive.addAll(c.getItems());
1315
 
1317
 
1316
        final Map<SQLRow, SQLRowValues> linksCut = new HashMap<SQLRow, SQLRowValues>();
1318
        final Map<SQLRow, SQLRowValues> linksCut = new HashMap<SQLRow, SQLRowValues>();
1317
        while (!toArchive.isEmpty()) {
1319
        while (!toArchive.isEmpty()) {
1318
            // archive the maximum without referents
1320
            // archive the maximum without referents
1319
            // or unarchive the maximum without foreigns
1321
            // or unarchive the maximum without foreigns
1320
            int archivedCount = -1;
1322
            int archivedCount = -1;
1321
            while (archivedCount != 0) {
1323
            while (archivedCount != 0) {
1322
                archivedCount = 0;
1324
                archivedCount = 0;
1323
                final Iterator<SQLRowValues> iter = toArchive.iterator();
1325
                final Iterator<SQLRowValues> iter = toArchive.iterator();
1324
                while (iter.hasNext()) {
1326
                while (iter.hasNext()) {
1325
                    final SQLRowValues desc = iter.next();
1327
                    final SQLRowValues desc = iter.next();
1326
                    final boolean correct;
1328
                    final boolean correct;
1327
                    if (desc.isArchived() == archive) {
1329
                    if (desc.isArchived() == archive) {
1328
                        // all already correct rows should be removed in the first loop, so they
1330
                        // all already correct rows should be removed in the first loop, so they
1329
                        // cannot be in linksCut
1331
                        // cannot be in linksCut
1330
                        assert !linksCut.containsKey(desc.asRow());
1332
                        assert !linksCut.containsKey(desc.asRow());
1331
                        correct = true;
1333
                        correct = true;
1332
                    } else if (archive && !desc.hasReferents() || !archive && !desc.hasForeigns()) {
1334
                    } else if (archive && !desc.hasReferents() || !archive && !desc.hasForeigns()) {
1333
                        SQLRowValues updateVals = linksCut.remove(desc.asRow());
1335
                        SQLRowValues updateVals = linksCut.remove(desc.asRow());
1334
                        if (updateVals == null)
1336
                        if (updateVals == null)
1335
                            updateVals = new SQLRowValues(desc.getTable());
1337
                            updateVals = new SQLRowValues(desc.getTable());
1336
                        // ne pas faire les fire après sinon qd on efface plusieurs éléments
1338
                        // ne pas faire les fire après sinon qd on efface plusieurs éléments
1337
                        // de la même table :
1339
                        // de la même table :
1338
                        // on fire pour le 1er => updateSearchList => IListe.select(userID)
1340
                        // on fire pour le 1er => updateSearchList => IListe.select(userID)
1339
                        // hors si userID a aussi été archivé (mais il n'y a pas eu son fire
1341
                        // hors si userID a aussi été archivé (mais il n'y a pas eu son fire
1340
                        // correspondant), le component va lancer un RowNotFound
1342
                        // correspondant), le component va lancer un RowNotFound
1341
                        setArchive(updateVals, archive).setID(desc.getIDNumber());
1343
                        setArchive(updateVals, archive).setID(desc.getIDNumber());
1342
                        // don't check validity since table events might have not already be
1344
                        // don't check validity since table events might have not already be
1343
                        // fired
1345
                        // fired
1344
                        assert updateVals.getGraphSize() == 1 : "Archiving a graph : " + updateVals.printGraph();
1346
                        assert updateVals.getGraphSize() == 1 : "Archiving a graph : " + updateVals.printGraph();
1345
                        updateVals.getGraph().store(StoreMode.COMMIT, false);
1347
                        updateVals.getGraph().store(StoreMode.COMMIT, false);
1346
                        correct = true;
1348
                        correct = true;
1347
                    } else {
1349
                    } else {
1348
                        correct = false;
1350
                        correct = false;
1349
                    }
1351
                    }
1350
                    if (correct) {
1352
                    if (correct) {
1351
                        // remove from graph
1353
                        // remove from graph
1352
                        desc.clear();
1354
                        desc.clear();
1353
                        desc.clearReferents();
1355
                        desc.clearReferents();
1354
                        assert desc.getGraphSize() == 1 : "Next loop won't progress : " + desc.printGraph();
1356
                        assert desc.getGraphSize() == 1 : "Next loop won't progress : " + desc.printGraph();
1355
                        archivedCount++;
1357
                        archivedCount++;
1356
                        iter.remove();
1358
                        iter.remove();
1357
                    }
1359
                    }
1358
                }
1360
                }
1359
            }
1361
            }
1360
 
1362
 
1361
            // if not empty there's at least one cycle
1363
            // if not empty there's at least one cycle
1362
            if (!toArchive.isEmpty()) {
1364
            if (!toArchive.isEmpty()) {
1363
                // Identify one cycle, ATTN first might not be itself part of the cycle, like the
1365
                // Identify one cycle, ATTN first might not be itself part of the cycle, like the
1364
                // BATIMENT and the LOCALs :
1366
                // BATIMENT and the LOCALs :
1365
                /**
1367
                /**
1366
                 * <pre>
1368
                 * <pre>
1367
                 * BATIMENT
1369
                 * BATIMENT
1368
                 * |      \
1370
                 * |      \
1369
                 * LOCAL1  LOCAL2
1371
                 * LOCAL1  LOCAL2
1370
                 * |        \
1372
                 * |        \
1371
                 * CPI ---> SOURCE
1373
                 * CPI ---> SOURCE
1372
                 *     <--/
1374
                 *     <--/
1373
                 * </pre>
1375
                 * </pre>
1374
                 */
1376
                 */
1375
                final SQLRowValues first = toArchive.iterator().next();
1377
                final SQLRowValues first = toArchive.iterator().next();
1376
                // Among the rows in the cycle, archive one by cutting links (choose
1378
                // Among the rows in the cycle, archive one by cutting links (choose
1377
                // one with the least of them)
1379
                // one with the least of them)
1378
                final AtomicReference<SQLRowValues> cutLinksRef = new AtomicReference<SQLRowValues>(null);
1380
                final AtomicReference<SQLRowValues> cutLinksRef = new AtomicReference<SQLRowValues>(null);
1379
                first.getGraph().walk(first, null, new ITransformer<State<Object>, Object>() {
1381
                first.getGraph().walk(first, null, new ITransformer<State<Object>, Object>() {
1380
                    @Override
1382
                    @Override
1381
                    public Object transformChecked(State<Object> input) {
1383
                    public Object transformChecked(State<Object> input) {
1382
                        final SQLRowValues last = input.getCurrent();
1384
                        final SQLRowValues last = input.getCurrent();
1383
                        boolean cycleFound = false;
1385
                        boolean cycleFound = false;
1384
                        int minLinksCount = -1;
1386
                        int minLinksCount = -1;
1385
                        SQLRowValues leastLinks = null;
1387
                        SQLRowValues leastLinks = null;
1386
                        final Iterator<SQLRowValues> iter = input.getValsPath().iterator();
1388
                        final Iterator<SQLRowValues> iter = input.getValsPath().iterator();
1387
                        while (iter.hasNext()) {
1389
                        while (iter.hasNext()) {
1388
                            final SQLRowValues v = iter.next();
1390
                            final SQLRowValues v = iter.next();
1389
                            if (!cycleFound) {
1391
                            if (!cycleFound) {
1390
                                // start of cycle found
1392
                                // start of cycle found
1391
                                cycleFound = iter.hasNext() && v == last;
1393
                                cycleFound = iter.hasNext() && v == last;
1392
                            }
1394
                            }
1393
                            if (cycleFound) {
1395
                            if (cycleFound) {
1394
                                // don't use getReferentRows() as it's not the row count but
1396
                                // don't use getReferentRows() as it's not the row count but
1395
                                // the link count that's important
1397
                                // the link count that's important
1396
                                final int linksCount = archive ? v.getReferentsMap().allValues().size() : v.getForeigns().size();
1398
                                final int linksCount = archive ? v.getReferentsMap().allValues().size() : v.getForeigns().size();
1397
                                // otherwise should have been removed above
1399
                                // otherwise should have been removed above
1398
                                assert linksCount > 0;
1400
                                assert linksCount > 0;
1399
                                if (leastLinks == null || linksCount < minLinksCount) {
1401
                                if (leastLinks == null || linksCount < minLinksCount) {
1400
                                    leastLinks = v;
1402
                                    leastLinks = v;
1401
                                    minLinksCount = linksCount;
1403
                                    minLinksCount = linksCount;
1402
                                }
1404
                                }
1403
                            }
1405
                            }
1404
                        }
1406
                        }
1405
                        if (cycleFound) {
1407
                        if (cycleFound) {
1406
                            cutLinksRef.set(leastLinks);
1408
                            cutLinksRef.set(leastLinks);
1407
                            throw new StopRecurseException();
1409
                            throw new StopRecurseException();
1408
                        }
1410
                        }
1409
 
1411
 
1410
                        return null;
1412
                        return null;
1411
                    }
1413
                    }
1412
                }, new WalkOptions(Direction.REFERENT).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false).setCycleAllowed(true));
1414
                }, new WalkOptions(Direction.REFERENT).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false).setCycleAllowed(true));
1413
                final SQLRowValues cutLinks = cutLinksRef.get();
1415
                final SQLRowValues cutLinks = cutLinksRef.get();
1414
 
1416
 
1415
                // if there were no cycles rows would have been removed above
1417
                // if there were no cycles rows would have been removed above
1416
                assert cutLinks != null;
1418
                assert cutLinks != null;
1417
 
1419
 
1418
                // cut links, and store them to be restored
1420
                // cut links, and store them to be restored
1419
                if (archive) {
1421
                if (archive) {
1420
                    for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(cutLinks.getReferentsMap()).entrySet()) {
1422
                    for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(cutLinks.getReferentsMap()).entrySet()) {
1421
                        final String fieldName = e.getKey().getName();
1423
                        final String fieldName = e.getKey().getName();
1422
                        for (final SQLRowValues v : e.getValue()) {
1424
                        for (final SQLRowValues v : e.getValue()) {
1423
                            // store before cutting
1425
                            // store before cutting
1424
                            SQLRowValues cutVals = linksCut.get(v.asRow());
1426
                            SQLRowValues cutVals = linksCut.get(v.asRow());
1425
                            if (cutVals == null) {
1427
                            if (cutVals == null) {
1426
                                cutVals = new SQLRowValues(v.getTable());
1428
                                cutVals = new SQLRowValues(v.getTable());
1427
                                linksCut.put(v.asRow(), cutVals);
1429
                                linksCut.put(v.asRow(), cutVals);
1428
                            }
1430
                            }
1429
                            assert !cutVals.getFields().contains(fieldName) : fieldName + " already cut for " + v;
1431
                            assert !cutVals.getFields().contains(fieldName) : fieldName + " already cut for " + v;
1430
                            assert !v.isForeignEmpty(fieldName) : "Nothing to cut";
1432
                            assert !v.isForeignEmpty(fieldName) : "Nothing to cut";
1431
                            cutVals.put(fieldName, v.getForeignIDNumber(fieldName));
1433
                            cutVals.put(fieldName, v.getForeignIDNumber(fieldName));
1432
                            // cut graph
1434
                            // cut graph
1433
                            v.putEmptyLink(fieldName);
1435
                            v.putEmptyLink(fieldName);
1434
                            // cut DB
1436
                            // cut DB
1435
                            new SQLRowValues(v.getTable()).putEmptyLink(fieldName).update(v.getID());
1437
                            new SQLRowValues(v.getTable()).putEmptyLink(fieldName).update(v.getID());
1436
                        }
1438
                        }
1437
                    }
1439
                    }
1438
                } else {
1440
                } else {
1439
                    // store before cutting
1441
                    // store before cutting
1440
                    final Set<String> foreigns = new HashSet<String>(cutLinks.getForeigns().keySet());
1442
                    final Set<String> foreigns = new HashSet<String>(cutLinks.getForeigns().keySet());
1441
                    final SQLRowValues oldVal = linksCut.put(cutLinks.asRow(), new SQLRowValues(cutLinks, ForeignCopyMode.COPY_ID_OR_RM));
1443
                    final SQLRowValues oldVal = linksCut.put(cutLinks.asRow(), new SQLRowValues(cutLinks, ForeignCopyMode.COPY_ID_OR_RM));
1442
                    // can't pass twice, as the first time we clear all foreigns, so the next loop
1444
                    // can't pass twice, as the first time we clear all foreigns, so the next loop
1443
                    // must unarchive it.
1445
                    // must unarchive it.
1444
                    assert oldVal == null : "Already cut";
1446
                    assert oldVal == null : "Already cut";
1445
                    // cut graph
1447
                    // cut graph
1446
                    cutLinks.removeAll(foreigns);
1448
                    cutLinks.removeAll(foreigns);
1447
                    // cut DB
1449
                    // cut DB
1448
                    final SQLRowValues updateVals = new SQLRowValues(cutLinks.getTable());
1450
                    final SQLRowValues updateVals = new SQLRowValues(cutLinks.getTable());
1449
                    for (final String fieldName : foreigns) {
1451
                    for (final String fieldName : foreigns) {
1450
                        updateVals.putEmptyLink(fieldName);
1452
                        updateVals.putEmptyLink(fieldName);
1451
                    }
1453
                    }
1452
                    updateVals.update(cutLinks.getID());
1454
                    updateVals.update(cutLinks.getID());
1453
                }
1455
                }
1454
                // ready to begin another loop
1456
                // ready to begin another loop
1455
                assert archive && !cutLinks.hasReferents() || !archive && !cutLinks.hasForeigns();
1457
                assert archive && !cutLinks.hasReferents() || !archive && !cutLinks.hasForeigns();
1456
            }
1458
            }
1457
        }
1459
        }
1458
        // for unarchive we need to update again the already treated (unarchived) row
1460
        // for unarchive we need to update again the already treated (unarchived) row
1459
        assert !archive || linksCut.isEmpty() : "Some links weren't restored : " + linksCut;
1461
        assert !archive || linksCut.isEmpty() : "Some links weren't restored : " + linksCut;
1460
        if (!archive) {
1462
        if (!archive) {
1461
            for (final Entry<SQLRow, SQLRowValues> e : linksCut.entrySet()) {
1463
            for (final Entry<SQLRow, SQLRowValues> e : linksCut.entrySet()) {
1462
                e.getValue().update(e.getKey().getID());
1464
                e.getValue().update(e.getKey().getID());
1463
            }
1465
            }
1464
        }
1466
        }
1465
    }
1467
    }
1466
 
1468
 
1467
    public void delete(SQLRowAccessor r) throws SQLException {
1469
    public void delete(SQLRowAccessor r) throws SQLException {
1468
        this.check(r);
1470
        this.check(r);
1469
        if (true)
1471
        if (true)
1470
            throw new UnsupportedOperationException("not yet implemented.");
1472
            throw new UnsupportedOperationException("not yet implemented.");
1471
    }
1473
    }
1472
 
1474
 
1473
    public final SQLTable getTable() {
1475
    public final SQLTable getTable() {
1474
        return this.primaryTable;
1476
        return this.primaryTable;
1475
    }
1477
    }
1476
 
1478
 
1477
    /**
1479
    /**
1478
     * A code identifying a specific meaning for the table and fields. I.e. it is used by
1480
     * A code identifying a specific meaning for the table and fields. I.e. it is used by
1479
     * {@link #getName() names} and {@link SQLFieldTranslator item metadata}. E.g. if two
1481
     * {@link #getName() names} and {@link SQLFieldTranslator item metadata}. E.g. if two
1480
     * applications use the same table for different purposes (at different times, of course), their
1482
     * applications use the same table for different purposes (at different times, of course), their
1481
     * elements should not share a code. On the contrary, if one application merely adds a field to
1483
     * elements should not share a code. On the contrary, if one application merely adds a field to
1482
     * an existing table, the new element should keep the same code so that existing name and
1484
     * an existing table, the new element should keep the same code so that existing name and
1483
     * documentation remain.
1485
     * documentation remain.
1484
     * 
1486
     * 
1485
     * @return a code for the table and its meaning.
1487
     * @return a code for the table and its meaning.
1486
     */
1488
     */
1487
    public synchronized final String getCode() {
1489
    public synchronized final String getCode() {
1488
        if (this.code == DEFERRED_CODE) {
1490
        if (this.code == DEFERRED_CODE) {
1489
            final String createCode = this.createCode();
1491
            final String createCode = this.createCode();
1490
            if (createCode == DEFERRED_CODE)
1492
            if (createCode == DEFERRED_CODE)
1491
                throw new IllegalStateException("createCode() returned DEFERRED_CODE");
1493
                throw new IllegalStateException("createCode() returned DEFERRED_CODE");
1492
            this.code = createCode;
1494
            this.code = createCode;
1493
        }
1495
        }
1494
        return this.code;
1496
        return this.code;
1495
    }
1497
    }
1496
 
1498
 
1497
    /**
1499
    /**
1498
     * Is the rows of this element shared, ie rows are unique and must not be copied.
1500
     * Is the rows of this element shared, ie rows are unique and must not be copied.
1499
     * 
1501
     * 
1500
     * @return <code>true</code> if this element is shared.
1502
     * @return <code>true</code> if this element is shared.
1501
     */
1503
     */
1502
    public boolean isShared() {
1504
    public boolean isShared() {
1503
        return false;
1505
        return false;
1504
    }
1506
    }
1505
 
1507
 
1506
    /**
1508
    /**
1507
     * Must the rows of this element be copied when traversing a hierarchy.
1509
     * Must the rows of this element be copied when traversing a hierarchy.
1508
     * 
1510
     * 
1509
     * @return <code>true</code> if the element must not be copied.
1511
     * @return <code>true</code> if the element must not be copied.
1510
     */
1512
     */
1511
    public boolean dontDeepCopy() {
1513
    public boolean dontDeepCopy() {
1512
        return false;
1514
        return false;
1513
    }
1515
    }
1514
 
1516
 
1515
    // *** rf
1517
    // *** rf
1516
 
1518
 
1517
    public final synchronized SQLElementLinks getLinksOwnedByOthers() {
1519
    public final synchronized SQLElementLinks getLinksOwnedByOthers() {
1518
        this.initRF();
1520
        this.initRF();
1519
        return this.otherLinks;
1521
        return this.otherLinks;
1520
    }
1522
    }
1521
 
1523
 
1522
    private final Set<SQLField> getReferentFields(final LinkType type) {
1524
    private final Set<SQLField> getReferentFields(final LinkType type) {
1523
        return getSingleFields(this.getLinksOwnedByOthers(), type);
1525
        return getSingleFields(this.getLinksOwnedByOthers(), type);
1524
    }
1526
    }
1525
 
1527
 
1526
    // not deprecated since joins to parents are unsupported (and unecessary since an SQLElement can
1528
    // not deprecated since joins to parents are unsupported (and unecessary since an SQLElement can
1527
    // only have one parent)
1529
    // only have one parent)
1528
    public final Set<SQLField> getChildrenReferentFields() {
1530
    public final Set<SQLField> getChildrenReferentFields() {
1529
        return this.getReferentFields(LinkType.PARENT);
1531
        return this.getReferentFields(LinkType.PARENT);
1530
    }
1532
    }
1531
 
1533
 
1532
    // *** ff
1534
    // *** ff
1533
 
1535
 
1534
    public synchronized final SQLElementLinks getOwnedLinks() {
1536
    public synchronized final SQLElementLinks getOwnedLinks() {
1535
        this.initFF();
1537
        this.initFF();
1536
        return this.ownedLinks;
1538
        return this.ownedLinks;
1537
    }
1539
    }
1538
 
1540
 
1539
    public final SQLElementLink getOwnedLink(final String fieldName) {
1541
    public final SQLElementLink getOwnedLink(final String fieldName) {
1540
        return this.getOwnedLink(fieldName, null);
1542
        return this.getOwnedLink(fieldName, null);
1541
    }
1543
    }
1542
 
1544
 
1543
    /**
1545
    /**
1544
     * Return the {@link #getOwnedLinks() owned link} that crosses the passed field.
1546
     * Return the {@link #getOwnedLinks() owned link} that crosses the passed field.
1545
     * 
1547
     * 
1546
     * @param fieldName any field of {@link #getTable()}.
1548
     * @param fieldName any field of {@link #getTable()}.
1547
     * @param type the type of the wanted link, <code>null</code> meaning any type.
1549
     * @param type the type of the wanted link, <code>null</code> meaning any type.
1548
     * @return the link matching the parameter.
1550
     * @return the link matching the parameter.
1549
     */
1551
     */
1550
    public final SQLElementLink getOwnedLink(final String fieldName, final LinkType type) {
1552
    public final SQLElementLink getOwnedLink(final String fieldName, final LinkType type) {
1551
        final Link foreignLink = this.getTable().getDBSystemRoot().getGraph().getForeignLink(this.getTable().getField(fieldName));
1553
        final Link foreignLink = this.getTable().getDBSystemRoot().getGraph().getForeignLink(this.getTable().getField(fieldName));
1552
        if (foreignLink == null)
1554
        if (foreignLink == null)
1553
            return null;
1555
            return null;
1554
        return this.getOwnedLinks().getByPath(new PathBuilder(getTable()).add(foreignLink, Direction.FOREIGN).build(), type);
1556
        return this.getOwnedLinks().getByPath(new PathBuilder(getTable()).add(foreignLink, Direction.FOREIGN).build(), type);
1555
    }
1557
    }
1556
 
1558
 
1557
    public final boolean hasOwnedLinks(final LinkType type) {
1559
    public final boolean hasOwnedLinks(final LinkType type) {
1558
        return !this.getOwnedLinks().getByType(type).isEmpty();
1560
        return !this.getOwnedLinks().getByType(type).isEmpty();
1559
    }
1561
    }
1560
 
1562
 
1561
    public final SQLField getParentForeignField() {
1563
    public final SQLField getParentForeignField() {
1562
        return getOptionalField(this.getParentForeignFieldName());
1564
        return getOptionalField(this.getParentForeignFieldName());
1563
    }
1565
    }
1564
 
1566
 
1565
    public final synchronized String getParentForeignFieldName() {
1567
    public final synchronized String getParentForeignFieldName() {
1566
        this.initFF();
1568
        this.initFF();
1567
        return this.parentFF;
1569
        return this.parentFF;
1568
    }
1570
    }
1569
 
1571
 
1570
    public final SQLElementLink getParentLink() {
1572
    public final SQLElementLink getParentLink() {
1571
        return CollectionUtils.getSole(this.getOwnedLinks().getByType(LinkType.PARENT));
1573
        return CollectionUtils.getSole(this.getOwnedLinks().getByType(LinkType.PARENT));
1572
    }
1574
    }
1573
 
1575
 
1574
    public final Set<SQLElementLink> getChildrenLinks() {
1576
    public final Set<SQLElementLink> getChildrenLinks() {
1575
        return this.getLinksOwnedByOthers().getByType(LinkType.PARENT);
1577
        return this.getLinksOwnedByOthers().getByType(LinkType.PARENT);
1576
    }
1578
    }
1577
 
1579
 
1578
    public final SQLElement getChildElement(final String tableName) {
1580
    public final SQLElement getChildElement(final String tableName) {
1579
        final Set<SQLElementLink> links = new HashSet<SQLElementLink>();
1581
        final Set<SQLElementLink> links = new HashSet<SQLElementLink>();
1580
        for (final SQLElementLink childLink : this.getChildrenLinks()) {
1582
        for (final SQLElementLink childLink : this.getChildrenLinks()) {
1581
            if (childLink.getOwner().getTable().getName().equals(tableName))
1583
            if (childLink.getOwner().getTable().getName().equals(tableName))
1582
                links.add(childLink);
1584
                links.add(childLink);
1583
        }
1585
        }
1584
        if (links.size() != 1)
1586
        if (links.size() != 1)
1585
            throw new IllegalStateException("no exactly one child table named " + tableName + " : " + links);
1587
            throw new IllegalStateException("no exactly one child table named " + tableName + " : " + links);
1586
        else
1588
        else
1587
            return links.iterator().next().getOwner();
1589
            return links.iterator().next().getOwner();
1588
    }
1590
    }
1589
 
1591
 
1590
    // optional but if specified it must exist
1592
    // optional but if specified it must exist
1591
    private final SQLField getOptionalField(final String name) {
1593
    private final SQLField getOptionalField(final String name) {
1592
        return name == null ? null : this.getTable().getField(name);
1594
        return name == null ? null : this.getTable().getField(name);
1593
    }
1595
    }
1594
 
1596
 
1595
    // Previously there was another method which listed children but this method is preferred since
1597
    // Previously there was another method which listed children but this method is preferred since
1596
    // it avoids writing IFs to account for customer differences and there's no ambiguity (you
1598
    // it avoids writing IFs to account for customer differences and there's no ambiguity (you
1597
    // return a field of this table instead of a table name that must be searched in roots and then
1599
    // return a field of this table instead of a table name that must be searched in roots and then
1598
    // a foreign key must be found).
1600
    // a foreign key must be found).
1599
    /**
1601
    /**
1600
     * Should be overloaded to specify our parent.
1602
     * Should be overloaded to specify our parent.
1601
     * 
1603
     * 
1602
     * @return <code>null</code> for this implementation.
1604
     * @return <code>null</code> for this implementation.
1603
     */
1605
     */
1604
    protected String getParentFFName() {
1606
    protected String getParentFFName() {
1605
        return null;
1607
        return null;
1606
    }
1608
    }
1607
 
1609
 
1608
    public final SQLElement getParentElement() {
1610
    public final SQLElement getParentElement() {
1609
        if (this.getParentForeignFieldName() == null)
1611
        if (this.getParentForeignFieldName() == null)
1610
            return null;
1612
            return null;
1611
        else
1613
        else
1612
            return this.getForeignElement(this.getParentForeignFieldName());
1614
            return this.getForeignElement(this.getParentForeignFieldName());
1613
    }
1615
    }
1614
 
1616
 
1615
    public final SQLElement getPrivateElement(String foreignField) {
1617
    public final SQLElement getPrivateElement(String foreignField) {
1616
        final SQLElementLink privateLink = this.getOwnedLink(foreignField, LinkType.COMPOSITION);
1618
        final SQLElementLink privateLink = this.getOwnedLink(foreignField, LinkType.COMPOSITION);
1617
        return privateLink == null ? null : privateLink.getOwned();
1619
        return privateLink == null ? null : privateLink.getOwned();
1618
    }
1620
    }
1619
 
1621
 
1620
    /**
1622
    /**
1621
     * The graph of this table and its privates.
1623
     * The graph of this table and its privates.
1622
     * 
1624
     * 
1623
     * @return an SQLRowValues of this element's table filled with
1625
     * @return an SQLRowValues of this element's table filled with
1624
     *         {@link SQLRowValues#setAllToNull() <code>null</code>s} except for private foreign
1626
     *         {@link SQLRowValues#setAllToNull() <code>null</code>s} except for private foreign
1625
     *         fields containing SQLRowValues.
1627
     *         fields containing SQLRowValues.
1626
     * @deprecated renamed to {@link #createGraph()} since there's also join tables and each call
1628
     * @deprecated renamed to {@link #createGraph()} since there's also join tables and each call
1627
     *             creates a new instance.
1629
     *             creates a new instance.
1628
     */
1630
     */
1629
    public final SQLRowValues getPrivateGraph() {
1631
    public final SQLRowValues getPrivateGraph() {
1630
        return this.createGraph();
1632
        return this.createGraph();
1631
    }
1633
    }
1632
 
1634
 
1633
    /**
1635
    /**
1634
     * The graph of this table, its privates and join tables.
1636
     * The graph of this table, its privates and join tables.
1635
     * 
1637
     * 
1636
     * @return a graph of SQLRowValues filled with <code>null</code>s.
1638
     * @return a graph of SQLRowValues filled with <code>null</code>s.
1637
     */
1639
     */
1638
    public final SQLRowValues createGraph() {
1640
    public final SQLRowValues createGraph() {
1639
        return this.createGraph(VirtualFields.ALL);
1641
        return this.createGraph(VirtualFields.ALL);
1640
    }
1642
    }
1641
 
1643
 
1642
    /**
1644
    /**
1643
     * The graph of this table, its privates and join tables.
1645
     * The graph of this table, its privates and join tables.
1644
     * 
1646
     * 
1645
     * @param fields which fields should be included in the graph, not <code>null</code>.
1647
     * @param fields which fields should be included in the graph, not <code>null</code>.
1646
     * @return a graph of SQLRowValues filled with <code>null</code>s according to the
1648
     * @return a graph of SQLRowValues filled with <code>null</code>s according to the
1647
     *         <code>fields</code> parameter.
1649
     *         <code>fields</code> parameter.
1648
     */
1650
     */
1649
    public final SQLRowValues createGraph(final VirtualFields fields) {
1651
    public final SQLRowValues createGraph(final VirtualFields fields) {
1650
        return this.createGraph(fields, PrivateMode.ALL_PRIVATES, true);
1652
        return this.createGraph(fields, PrivateMode.ALL_PRIVATES, true);
1651
    }
1653
    }
1652
 
1654
 
1653
    static public enum PrivateMode {
1655
    static public enum PrivateMode {
1654
        NO_PRIVATES, DEEP_COPIED_PRIVATES, ALL_PRIVATES;
1656
        NO_PRIVATES, DEEP_COPIED_PRIVATES, ALL_PRIVATES;
1655
    }
1657
    }
1656
 
1658
 
1657
    static private final SQLRowValues putNulls(final SQLRowValues res, final VirtualFields fields) {
1659
    static private final SQLRowValues putNulls(final SQLRowValues res, final VirtualFields fields) {
1658
        return res.fill(res.getTable().getFieldsNames(fields), null, false, true);
1660
        return res.fill(res.getTable().getFieldsNames(fields), null, false, true);
1659
    }
1661
    }
1660
 
1662
 
1661
    public final SQLRowValues createGraph(final VirtualFields fields, final PrivateMode privateMode, final boolean includeJoins) {
1663
    public final SQLRowValues createGraph(final VirtualFields fields, final PrivateMode privateMode, final boolean includeJoins) {
1662
        final SQLRowValues res = putNulls(new SQLRowValues(this.getTable()), fields);
1664
        final SQLRowValues res = putNulls(new SQLRowValues(this.getTable()), fields);
1663
        if (includeJoins) {
1665
        if (includeJoins) {
1664
            for (final SQLElementLink link : this.getOwnedLinks().getByPath().values()) {
1666
            for (final SQLElementLink link : this.getOwnedLinks().getByPath().values()) {
1665
                if (link.isJoin()) {
1667
                if (link.isJoin()) {
1666
                    putNulls(res.putRowValues(link.getPath().getStep(0)), fields);
1668
                    putNulls(res.putRowValues(link.getPath().getStep(0)), fields);
1667
                }
1669
                }
1668
            }
1670
            }
1669
        }
1671
        }
1670
        if (privateMode != PrivateMode.NO_PRIVATES) {
1672
        if (privateMode != PrivateMode.NO_PRIVATES) {
1671
            for (final SQLElementLink link : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
1673
            for (final SQLElementLink link : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
1672
                final SQLElement owned = link.getOwned();
1674
                final SQLElement owned = link.getOwned();
1673
                if (privateMode == PrivateMode.DEEP_COPIED_PRIVATES && owned.dontDeepCopy()) {
1675
                if (privateMode == PrivateMode.DEEP_COPIED_PRIVATES && owned.dontDeepCopy()) {
1674
                    res.remove(link.getPath().getStep(0));
1676
                    res.remove(link.getPath().getStep(0));
1675
                } else {
1677
                } else {
1676
                    res.put(link.getPath(), false, owned.createGraph(fields, privateMode, includeJoins));
1678
                    res.put(link.getPath(), false, owned.createGraph(fields, privateMode, includeJoins));
1677
                }
1679
                }
1678
            }
1680
            }
1679
        }
1681
        }
1680
        return res;
1682
        return res;
1681
    }
1683
    }
1682
 
1684
 
1683
    /**
1685
    /**
1684
     * Renvoie les champs qui sont 'privé' càd que les ligne pointées par ce champ ne sont
1686
     * Renvoie les champs qui sont 'privé' càd que les ligne pointées par ce champ ne sont
1685
     * référencées que par une et une seule ligne de cette table. Cette implementation renvoie une
1687
     * référencées que par une et une seule ligne de cette table. Cette implementation renvoie une
1686
     * liste vide. This method is intented for subclasses, call {@link #getPrivateForeignFields()}
1688
     * liste vide. This method is intented for subclasses, call {@link #getPrivateForeignFields()}
1687
     * which does some checks.
1689
     * which does some checks.
1688
     * 
1690
     * 
1689
     * @return la List des noms des champs privés, eg ["ID_OBSERVATION_2"].
1691
     * @return la List des noms des champs privés, eg ["ID_OBSERVATION_2"].
1690
     * @deprecated use {@link #setupLinks(SQLElementLinksSetup)}
1692
     * @deprecated use {@link #setupLinks(SQLElementLinksSetup)}
1691
     */
1693
     */
1692
    protected List<String> getPrivateFields() {
1694
    protected List<String> getPrivateFields() {
1693
        return Collections.emptyList();
1695
        return Collections.emptyList();
1694
    }
1696
    }
1695
 
1697
 
1696
    public final void clearPrivateFields(SQLRowValues rowVals) {
1698
    public final void clearPrivateFields(SQLRowValues rowVals) {
1697
        for (SQLElementLink l : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
1699
        for (SQLElementLink l : this.getOwnedLinks().getByType(LinkType.COMPOSITION)) {
1698
            rowVals.remove(l.getPath().getStep(0));
1700
            rowVals.remove(l.getPath().getStep(0));
1699
        }
1701
        }
1700
    }
1702
    }
1701
 
1703
 
1702
    /**
1704
    /**
1703
     * Specify an action for a normal foreign field.
1705
     * Specify an action for a normal foreign field.
1704
     * 
1706
     * 
1705
     * @param ff the foreign field name.
1707
     * @param ff the foreign field name.
1706
     * @param action what to do if a referenced row must be archived.
1708
     * @param action what to do if a referenced row must be archived.
1707
     * @throws IllegalArgumentException if <code>ff</code> is not a normal foreign field.
1709
     * @throws IllegalArgumentException if <code>ff</code> is not a normal foreign field.
1708
     */
1710
     */
1709
    public final void setAction(final String ff, ReferenceAction action) throws IllegalArgumentException {
1711
    public final void setAction(final String ff, ReferenceAction action) throws IllegalArgumentException {
1710
        final Path p = new PathBuilder(getTable()).addForeignField(ff).build();
1712
        final Path p = new PathBuilder(getTable()).addForeignField(ff).build();
1711
        this.getOwnedLinks().getByPath(p).setAction(action);
1713
        this.getOwnedLinks().getByPath(p).setAction(action);
1712
    }
1714
    }
1713
 
1715
 
1714
    // *** rf and ff
1716
    // *** rf and ff
1715
 
1717
 
1716
    /**
1718
    /**
1717
     * The links towards the parents (either {@link LinkType#PARENT} or {@link LinkType#COMPOSITION}
1719
     * The links towards the parents (either {@link LinkType#PARENT} or {@link LinkType#COMPOSITION}
1718
     * ) of this element.
1720
     * ) of this element.
1719
     * 
1721
     * 
1720
     * @return the links towards the parents of this element.
1722
     * @return the links towards the parents of this element.
1721
     */
1723
     */
1722
    public final SQLElementLinks getContainerLinks() {
1724
    public final SQLElementLinks getContainerLinks() {
1723
        return getContainerLinks(true, true);
1725
        return getContainerLinks(true, true);
1724
    }
1726
    }
1725
 
1727
 
1726
    public final SQLElementLinks getContainerLinks(final boolean privateParent, final boolean parent) {
1728
    public final SQLElementLinks getContainerLinks(final boolean privateParent, final boolean parent) {
1727
        final SetMapItf<LinkType, SQLElementLink> byType = new SetMap<LinkType, SQLElementLink>();
1729
        final SetMapItf<LinkType, SQLElementLink> byType = new SetMap<LinkType, SQLElementLink>();
1728
        if (parent)
1730
        if (parent)
1729
            byType.addAll(LinkType.PARENT, this.getOwnedLinks().getByType(LinkType.PARENT));
1731
            byType.addAll(LinkType.PARENT, this.getOwnedLinks().getByType(LinkType.PARENT));
1730
        if (privateParent)
1732
        if (privateParent)
1731
            byType.addAll(LinkType.COMPOSITION, this.getLinksOwnedByOthers().getByType(LinkType.COMPOSITION));
1733
            byType.addAll(LinkType.COMPOSITION, this.getLinksOwnedByOthers().getByType(LinkType.COMPOSITION));
1732
        final SQLElementLinks res = new SQLElementLinks(byType);
1734
        final SQLElementLinks res = new SQLElementLinks(byType);
1733
        assert res.getByType().size() <= 1 : "Child and private at the same time";
1735
        assert res.getByType().size() <= 1 : "Child and private at the same time";
1734
        return res;
1736
        return res;
1735
    }
1737
    }
1736
 
1738
 
1737
    // *** request
1739
    // *** request
1738
 
1740
 
1739
    public final ComboSQLRequest getComboRequest() {
1741
    public final ComboSQLRequest getComboRequest() {
1740
        return getComboRequest(false);
1742
        return getComboRequest(false);
1741
    }
1743
    }
1742
 
1744
 
1743
    /**
1745
    /**
1744
     * Return a combo request for this element.
1746
     * Return a combo request for this element.
1745
     * 
1747
     * 
1746
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1748
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1747
     *        return a shared instance.
1749
     *        return a shared instance.
1748
     * @return a combo request for this.
1750
     * @return a combo request for this.
1749
     */
1751
     */
1750
    public final ComboSQLRequest getComboRequest(final boolean create) {
1752
    public final ComboSQLRequest getComboRequest(final boolean create) {
1751
        if (!create) {
1753
        if (!create) {
1752
            if (this.combo == null) {
1754
            if (this.combo == null) {
1753
                this.combo = this.createComboRequest();
1755
                this.combo = this.createComboRequest();
1754
            }
1756
            }
1755
            return this.combo;
1757
            return this.combo;
1756
        } else {
1758
        } else {
1757
            return this.createComboRequest();
1759
            return this.createComboRequest();
1758
        }
1760
        }
1759
    }
1761
    }
1760
 
1762
 
1761
    public final ComboSQLRequest createComboRequest() {
1763
    public final ComboSQLRequest createComboRequest() {
1762
        return this.createComboRequest(null, null);
1764
        return this.createComboRequest(null, null);
1763
    }
1765
    }
1764
 
1766
 
1765
    public final ComboSQLRequest createComboRequest(final List<String> fields, final Where w) {
1767
    public final ComboSQLRequest createComboRequest(final List<String> fields, final Where w) {
1766
        final ComboSQLRequest res = new ComboSQLRequest(this.getTable(), fields == null ? this.getComboFields() : fields, w, this.getDirectory());
1768
        final ComboSQLRequest res = new ComboSQLRequest(this.getTable(), fields == null ? this.getComboFields() : fields, w, this.getDirectory());
1767
        this._initComboRequest(res);
1769
        this._initComboRequest(res);
1768
        return res;
1770
        return res;
1769
    }
1771
    }
1770
 
1772
 
1771
    protected void _initComboRequest(final ComboSQLRequest req) {
1773
    protected void _initComboRequest(final ComboSQLRequest req) {
1772
    }
1774
    }
1773
 
1775
 
1774
    // not all elements need to be displayed in combos so don't make this method abstract
1776
    // not all elements need to be displayed in combos so don't make this method abstract
1775
    protected List<String> getComboFields() {
1777
    protected List<String> getComboFields() {
1776
        return this.getListFields();
1778
        return this.getListFields();
1777
    }
1779
    }
1778
 
1780
 
1779
    public final synchronized ListSQLRequest getListRequest() {
1781
    public final synchronized ListSQLRequest getListRequest() {
1780
        if (this.list == null) {
1782
        if (this.list == null) {
1781
            this.list = createListRequest();
1783
            this.list = createListRequest();
1782
        }
1784
        }
1783
        return this.list;
1785
        return this.list;
1784
    }
1786
    }
1785
 
1787
 
1786
    /**
1788
    /**
1787
     * Return the field expander to pass to {@link ListSQLRequest}.
1789
     * Return the field expander to pass to {@link ListSQLRequest}.
1788
     * 
1790
     * 
1789
     * @return the {@link FieldExpander} to pass to {@link ListSQLRequest}.
1791
     * @return the {@link FieldExpander} to pass to {@link ListSQLRequest}.
1790
     * @see #createListRequest(List, Where, FieldExpander)
1792
     * @see #createListRequest(List, Where, FieldExpander)
1791
     */
1793
     */
1792
    protected FieldExpander getListExpander() {
1794
    protected FieldExpander getListExpander() {
1793
        return getDirectory().getShowAs();
1795
        return getDirectory().getShowAs();
1794
    }
1796
    }
1795
 
1797
 
1796
    public final ListSQLRequest createListRequest() {
1798
    public final ListSQLRequest createListRequest() {
1797
        return this.createListRequest(null);
1799
        return this.createListRequest(null);
1798
    }
1800
    }
1799
 
1801
 
1800
    public final ListSQLRequest createListRequest(final List<String> fields) {
1802
    public final ListSQLRequest createListRequest(final List<String> fields) {
1801
        return this.createListRequest(fields, null, null);
1803
        return this.createListRequest(fields, null, null);
1802
    }
1804
    }
1803
 
1805
 
1804
    /**
1806
    /**
1805
     * Create and initialise a new list request with the passed arguments. Pass <code>null</code>
1807
     * Create and initialise a new list request with the passed arguments. Pass <code>null</code>
1806
     * for default arguments.
1808
     * for default arguments.
1807
     * 
1809
     * 
1808
     * @param fields the list fields, <code>null</code> meaning {@link #getListFields()}.
1810
     * @param fields the list fields, <code>null</code> meaning {@link #getListFields()}.
1809
     * @param w the where, can be <code>null</code>.
1811
     * @param w the where, can be <code>null</code>.
1810
     * @param expander the field expander, <code>null</code> meaning {@link #getListExpander()}.
1812
     * @param expander the field expander, <code>null</code> meaning {@link #getListExpander()}.
1811
     * @return a new ready-to-use list request.
1813
     * @return a new ready-to-use list request.
1812
     */
1814
     */
1813
    public final ListSQLRequest createListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
1815
    public final ListSQLRequest createListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
1814
        final ListSQLRequest res = instantiateListRequest(fields == null ? this.getListFields() : fields, w, expander == null ? this.getListExpander() : expander);
1816
        final ListSQLRequest res = instantiateListRequest(fields == null ? this.getListFields() : fields, w, expander == null ? this.getListExpander() : expander);
1815
        this._initListRequest(res);
1817
        this._initListRequest(res);
1816
        return res;
1818
        return res;
1817
    }
1819
    }
1818
 
1820
 
1819
    /**
1821
    /**
1820
     * Must just create a new instance without altering parameters. The parameters are passed by
1822
     * Must just create a new instance without altering parameters. The parameters are passed by
1821
     * {@link #createListRequest(List, Where, FieldExpander)}, if you need to change default values
1823
     * {@link #createListRequest(List, Where, FieldExpander)}, if you need to change default values
1822
     * overload the needed method. This method should only be used if one needs a subclass of
1824
     * overload the needed method. This method should only be used if one needs a subclass of
1823
     * {@link ListSQLRequest}.
1825
     * {@link ListSQLRequest}.
1824
     * 
1826
     * 
1825
     * @param fields the list fields.
1827
     * @param fields the list fields.
1826
     * @param w the where.
1828
     * @param w the where.
1827
     * @param expander the field expander.
1829
     * @param expander the field expander.
1828
     * @return a new uninitialised list request.
1830
     * @return a new uninitialised list request.
1829
     */
1831
     */
1830
    protected ListSQLRequest instantiateListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
1832
    protected ListSQLRequest instantiateListRequest(final List<String> fields, final Where w, final FieldExpander expander) {
1831
        return new ListSQLRequest(this.getTable(), fields, w, expander);
1833
        return new ListSQLRequest(this.getTable(), fields, w, expander);
1832
    }
1834
    }
1833
 
1835
 
1834
    /**
1836
    /**
1835
     * Initialise a new instance. E.g. one can {@link ListSQLRequest#addToGraphToFetch(String...)
1837
     * Initialise a new instance. E.g. one can {@link ListSQLRequest#addToGraphToFetch(String...)
1836
     * add fields} to the fetcher.
1838
     * add fields} to the fetcher.
1837
     * 
1839
     * 
1838
     * @param req the instance to initialise.
1840
     * @param req the instance to initialise.
1839
     */
1841
     */
1840
    protected void _initListRequest(final ListSQLRequest req) {
1842
    protected void _initListRequest(final ListSQLRequest req) {
1841
    }
1843
    }
1842
 
1844
 
1843
    public final SQLTableModelSourceOnline getTableSource() {
1845
    public final SQLTableModelSourceOnline getTableSource() {
1844
        return this.getTableSource(!cacheTableSource());
1846
        return this.getTableSource(!cacheTableSource());
1845
    }
1847
    }
1846
 
1848
 
1847
    /**
1849
    /**
1848
     * Return a table source for this element.
1850
     * Return a table source for this element.
1849
     * 
1851
     * 
1850
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1852
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1851
     *        return a shared instance.
1853
     *        return a shared instance.
1852
     * @return a table source for this.
1854
     * @return a table source for this.
1853
     */
1855
     */
1854
    public final synchronized SQLTableModelSourceOnline getTableSource(final boolean create) {
1856
    public final synchronized SQLTableModelSourceOnline getTableSource(final boolean create) {
1855
        if (!create) {
1857
        if (!create) {
1856
            if (this.tableSrc == null) {
1858
            if (this.tableSrc == null) {
1857
                this.tableSrc = createTableSource();
1859
                this.tableSrc = createTableSource();
1858
            }
1860
            }
1859
            return this.tableSrc;
1861
            return this.tableSrc;
1860
        } else
1862
        } else
1861
            return this.createTableSource();
1863
            return this.createTableSource();
1862
    }
1864
    }
1863
 
1865
 
1864
    public final SQLTableModelSourceOnline createTableSource() {
1866
    public final SQLTableModelSourceOnline createTableSource() {
1865
        return createTableSource((Where) null);
1867
        return createTableSource((Where) null);
1866
    }
1868
    }
1867
 
1869
 
1868
    public final SQLTableModelSourceOnline createTableSource(final List<String> fields) {
1870
    public final SQLTableModelSourceOnline createTableSource(final List<String> fields) {
1869
        return createTableSourceOnline(createListRequest(fields));
1871
        return createTableSourceOnline(createListRequest(fields));
1870
    }
1872
    }
1871
 
1873
 
1872
    public final SQLTableModelSourceOnline createTableSource(final Where w) {
1874
    public final SQLTableModelSourceOnline createTableSource(final Where w) {
1873
        return createTableSourceOnline(createListRequest(null, w, null));
1875
        return createTableSourceOnline(createListRequest(null, w, null));
1874
    }
1876
    }
1875
 
1877
 
1876
    public final SQLTableModelSourceOnline createTableSourceOnline(final ListSQLRequest req) {
1878
    public final SQLTableModelSourceOnline createTableSourceOnline(final ListSQLRequest req) {
1877
        return initTableSource(instantiateTableSourceOnline(req));
1879
        return initTableSource(instantiateTableSourceOnline(req));
1878
    }
1880
    }
1879
 
1881
 
1880
    protected SQLTableModelSourceOnline instantiateTableSourceOnline(final ListSQLRequest req) {
1882
    protected SQLTableModelSourceOnline instantiateTableSourceOnline(final ListSQLRequest req) {
1881
        return new SQLTableModelSourceOnline(req, this);
1883
        return new SQLTableModelSourceOnline(req, this);
1882
    }
1884
    }
1883
 
1885
 
1884
    protected synchronized void _initTableSource(final SQLTableModelSource res) {
1886
    protected synchronized void _initTableSource(final SQLTableModelSource res) {
1885
        if (!this.additionalListCols.isEmpty())
1887
        if (!this.additionalListCols.isEmpty())
1886
            res.getColumns().addAll(this.additionalListCols);
1888
            res.getColumns().addAll(this.additionalListCols);
1887
    }
1889
    }
1888
 
1890
 
1889
    public final <S extends SQLTableModelSource> S initTableSource(final S res) {
1891
    public final <S extends SQLTableModelSource> S initTableSource(final S res) {
1890
        return this.initTableSource(res, false);
1892
        return this.initTableSource(res, false);
1891
    }
1893
    }
1892
 
1894
 
1893
    public final synchronized <S extends SQLTableModelSource> S initTableSource(final S res, final boolean minimal) {
1895
    public final synchronized <S extends SQLTableModelSource> S initTableSource(final S res, final boolean minimal) {
1894
        res.init();
1896
        res.init();
1895
        // do init first since it can modify the columns
1897
        // do init first since it can modify the columns
1896
        if (!minimal)
1898
        if (!minimal)
1897
            this._initTableSource(res);
1899
            this._initTableSource(res);
1898
        // setEditable(false) on read only fields
1900
        // setEditable(false) on read only fields
1899
        // MAYBE setReadOnlyFields() on SQLTableModelSource, so that SQLTableModelLinesSource can
1901
        // MAYBE setReadOnlyFields() on SQLTableModelSource, so that SQLTableModelLinesSource can
1900
        // check in commit()
1902
        // check in commit()
1901
        final Set<String> dontModif = CollectionUtils.union(this.getReadOnlyFields(), this.getInsertOnlyFields());
1903
        final Set<String> dontModif = CollectionUtils.union(this.getReadOnlyFields(), this.getInsertOnlyFields());
1902
        for (final String f : dontModif)
1904
        for (final String f : dontModif)
1903
            for (final SQLTableModelColumn col : res.getColumns(getTable().getField(f)))
1905
            for (final SQLTableModelColumn col : res.getColumns(getTable().getField(f)))
1904
                if (col instanceof SQLTableModelColumnPath)
1906
                if (col instanceof SQLTableModelColumnPath)
1905
                    ((SQLTableModelColumnPath) col).setEditable(false);
1907
                    ((SQLTableModelColumnPath) col).setEditable(false);
1906
        return res;
1908
        return res;
1907
    }
1909
    }
1908
 
1910
 
1909
    public final SQLTableModelSourceOffline createTableSourceOffline() {
1911
    public final SQLTableModelSourceOffline createTableSourceOffline() {
1910
        return createTableSourceOfflineWithWhere(null);
1912
        return createTableSourceOfflineWithWhere(null);
1911
    }
1913
    }
1912
 
1914
 
1913
    public final SQLTableModelSourceOffline createTableSourceOfflineWithWhere(final Where w) {
1915
    public final SQLTableModelSourceOffline createTableSourceOfflineWithWhere(final Where w) {
1914
        return createTableSourceOffline(createListRequest(null, w, null));
1916
        return createTableSourceOffline(createListRequest(null, w, null));
1915
    }
1917
    }
1916
 
1918
 
1917
    public final SQLTableModelSourceOffline createTableSourceOffline(final ListSQLRequest req) {
1919
    public final SQLTableModelSourceOffline createTableSourceOffline(final ListSQLRequest req) {
1918
        return initTableSource(instantiateTableSourceOffline(req));
1920
        return initTableSource(instantiateTableSourceOffline(req));
1919
    }
1921
    }
1920
 
1922
 
1921
    protected SQLTableModelSourceOffline instantiateTableSourceOffline(final ListSQLRequest req) {
1923
    protected SQLTableModelSourceOffline instantiateTableSourceOffline(final ListSQLRequest req) {
1922
        return new SQLTableModelSourceOffline(req, this);
1924
        return new SQLTableModelSourceOffline(req, this);
1923
    }
1925
    }
1924
 
1926
 
1925
    /**
1927
    /**
1926
     * Whether to cache our tableSource.
1928
     * Whether to cache our tableSource.
1927
     * 
1929
     * 
1928
     * @return <code>true</code> to call {@link #createTableSource()} only once, or
1930
     * @return <code>true</code> to call {@link #createTableSource()} only once, or
1929
     *         <code>false</code> to call it each time {@link #getTableSource()} is.
1931
     *         <code>false</code> to call it each time {@link #getTableSource()} is.
1930
     */
1932
     */
1931
    protected boolean cacheTableSource() {
1933
    protected boolean cacheTableSource() {
1932
        return true;
1934
        return true;
1933
    }
1935
    }
1934
 
1936
 
1935
    abstract protected List<String> getListFields();
1937
    abstract protected List<String> getListFields();
1936
 
1938
 
1937
    public final void addListFields(final List<String> fields) {
1939
    public final void addListFields(final List<String> fields) {
1938
        for (final String f : fields)
1940
        for (final String f : fields)
1939
            this.addListColumn(new SQLTableModelColumnPath(getTable().getField(f)));
1941
            this.addListColumn(new SQLTableModelColumnPath(getTable().getField(f)));
1940
    }
1942
    }
1941
 
1943
 
1942
    public final void addListColumn(SQLTableModelColumn col) {
1944
    public final void addListColumn(SQLTableModelColumn col) {
1943
        this.additionalListCols.add(col);
1945
        this.additionalListCols.add(col);
1944
    }
1946
    }
1945
 
1947
 
1946
    public final Collection<IListeAction> getRowActions() {
1948
    public final Collection<IListeAction> getRowActions() {
1947
        return this.rowActions;
1949
        return this.rowActions;
1948
    }
1950
    }
1949
 
1951
 
1950
    public final void addRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
1952
    public final void addRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
1951
        this.rowActions.getRecipe().addListener(listener);
1953
        this.rowActions.getRecipe().addListener(listener);
1952
    }
1954
    }
1953
 
1955
 
1954
    public final void removeRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
1956
    public final void removeRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
1955
        this.rowActions.getRecipe().rmListener(listener);
1957
        this.rowActions.getRecipe().rmListener(listener);
1956
    }
1958
    }
1957
 
1959
 
1958
    public String getDescription(SQLRow fromRow) {
1960
    public String getDescription(SQLRow fromRow) {
1959
        return fromRow.toString();
1961
        return fromRow.toString();
1960
    }
1962
    }
1961
 
1963
 
1962
    // *** iterators
1964
    // *** iterators
1963
 
1965
 
1964
    static interface ChildProcessor<R extends SQLRowAccessor> {
1966
    static interface ChildProcessor<R extends SQLRowAccessor> {
1965
        public void process(R parent, SQLField joint, R child) throws SQLException;
1967
        public void process(R parent, SQLField joint, R child) throws SQLException;
1966
    }
1968
    }
1967
 
1969
 
1968
    /**
1970
    /**
1969
     * Execute <code>c</code> for each children of <code>row</code>. NOTE: <code>c</code> will be
1971
     * Execute <code>c</code> for each children of <code>row</code>. NOTE: <code>c</code> will be
1970
     * called with <code>row</code> as its first parameter, and with its child of the same type
1972
     * called with <code>row</code> as its first parameter, and with its child of the same type
1971
     * (SQLRow or SQLRowValues) for the third parameter.
1973
     * (SQLRow or SQLRowValues) for the third parameter.
1972
     * 
1974
     * 
1973
     * @param <R> type of SQLRowAccessor to use.
1975
     * @param <R> type of SQLRowAccessor to use.
1974
     * @param row the parent row.
1976
     * @param row the parent row.
1975
     * @param c what to do for each children.
1977
     * @param c what to do for each children.
1976
     * @param deep <code>true</code> to ignore {@link #dontDeepCopy()}.
1978
     * @param deep <code>true</code> to ignore {@link #dontDeepCopy()}.
1977
     * @param archived <code>true</code> to iterate over archived children.
1979
     * @param archived <code>true</code> to iterate over archived children.
1978
     * @throws SQLException if <code>c</code> raises an exn.
1980
     * @throws SQLException if <code>c</code> raises an exn.
1979
     */
1981
     */
1980
    private <R extends SQLRowAccessor> void forChildrenDo(R row, ChildProcessor<? super R> c, boolean deep, boolean archived) throws SQLException {
1982
    private <R extends SQLRowAccessor> void forChildrenDo(R row, ChildProcessor<? super R> c, boolean deep, boolean archived) throws SQLException {
1981
        for (final SQLElementLink childLink : this.getChildrenLinks()) {
1983
        for (final SQLElementLink childLink : this.getChildrenLinks()) {
1982
            if (deep || !childLink.getChild().dontDeepCopy()) {
1984
            if (deep || !childLink.getChild().dontDeepCopy()) {
1983
                final SQLField childField = childLink.getSingleField();
1985
                final SQLField childField = childLink.getSingleField();
1984
                final List<SQLRow> children = row.asRow().getReferentRows(childField, archived ? SQLSelect.ARCHIVED : SQLSelect.UNARCHIVED);
1986
                final List<SQLRow> children = row.asRow().getReferentRows(childField, archived ? SQLSelect.ARCHIVED : SQLSelect.UNARCHIVED);
1985
                // eg BATIMENT[516]
1987
                // eg BATIMENT[516]
1986
                for (final SQLRow child : children) {
1988
                for (final SQLRow child : children) {
1987
                    c.process(row, childField, convert(child, row));
1989
                    c.process(row, childField, convert(child, row));
1988
                }
1990
                }
1989
            }
1991
            }
1990
        }
1992
        }
1991
    }
1993
    }
1992
 
1994
 
1993
    // convert toConv to same type as row
1995
    // convert toConv to same type as row
1994
    @SuppressWarnings("unchecked")
1996
    @SuppressWarnings("unchecked")
1995
    private <R extends SQLRowAccessor> R convert(final SQLRow toConv, R row) {
1997
    private <R extends SQLRowAccessor> R convert(final SQLRow toConv, R row) {
1996
        final R ch;
1998
        final R ch;
1997
        if (row instanceof SQLRow)
1999
        if (row instanceof SQLRow)
1998
            ch = (R) toConv;
2000
            ch = (R) toConv;
1999
        else if (row instanceof SQLRowValues)
2001
        else if (row instanceof SQLRowValues)
2000
            ch = (R) toConv.createUpdateRow();
2002
            ch = (R) toConv.createUpdateRow();
2001
        else
2003
        else
2002
            throw new IllegalStateException("SQLRowAccessor is neither SQLRow nor SQLRowValues: " + toConv);
2004
            throw new IllegalStateException("SQLRowAccessor is neither SQLRow nor SQLRowValues: " + toConv);
2003
        return ch;
2005
        return ch;
2004
    }
2006
    }
2005
 
2007
 
2006
    // first the leaves
2008
    // first the leaves
2007
    private void forDescendantsDo(final SQLRow row, final ChildProcessor<SQLRow> c, final boolean deep) throws SQLException {
2009
    private void forDescendantsDo(final SQLRow row, final ChildProcessor<SQLRow> c, final boolean deep) throws SQLException {
2008
        this.forDescendantsDo(row, c, deep, true, false);
2010
        this.forDescendantsDo(row, c, deep, true, false);
2009
    }
2011
    }
2010
 
2012
 
2011
    <R extends SQLRowAccessor> void forDescendantsDo(final R row, final ChildProcessor<R> c, final boolean deep, final boolean leavesFirst, final boolean archived) throws SQLException {
2013
    <R extends SQLRowAccessor> void forDescendantsDo(final R row, final ChildProcessor<R> c, final boolean deep, final boolean leavesFirst, final boolean archived) throws SQLException {
2012
        this.check(row);
2014
        this.check(row);
2013
        this.forChildrenDo(row, new ChildProcessor<R>() {
2015
        this.forChildrenDo(row, new ChildProcessor<R>() {
2014
            public void process(R parent, SQLField joint, R child) throws SQLException {
2016
            public void process(R parent, SQLField joint, R child) throws SQLException {
2015
                if (!leavesFirst)
2017
                if (!leavesFirst)
2016
                    c.process(parent, joint, child);
2018
                    c.process(parent, joint, child);
2017
                getElement(child.getTable()).forDescendantsDo(child, c, deep, leavesFirst, archived);
2019
                getElement(child.getTable()).forDescendantsDo(child, c, deep, leavesFirst, archived);
2018
                if (leavesFirst)
2020
                if (leavesFirst)
2019
                    c.process(parent, joint, child);
2021
                    c.process(parent, joint, child);
2020
            }
2022
            }
2021
        }, deep, archived);
2023
        }, deep, archived);
2022
    }
2024
    }
2023
 
2025
 
2024
    protected final void check(SQLRowAccessor row) {
2026
    protected final void check(SQLRowAccessor row) {
2025
        if (!row.getTable().equals(this.getTable()))
2027
        if (!row.getTable().equals(this.getTable()))
2026
            throw new IllegalArgumentException("row must of table " + this.getTable() + " : " + row);
2028
            throw new IllegalArgumentException("row must of table " + this.getTable() + " : " + row);
2027
    }
2029
    }
2028
 
2030
 
2029
    private void checkUndefined(SQLRow row) {
2031
    private void checkUndefined(SQLRow row) {
2030
        this.check(row);
2032
        this.check(row);
2031
        if (row.isUndefined())
2033
        if (row.isUndefined())
2032
            throw new IllegalArgumentException("row is undefined: " + row);
2034
            throw new IllegalArgumentException("row is undefined: " + row);
2033
    }
2035
    }
2034
 
2036
 
2035
    // *** copy
2037
    // *** copy
2036
 
2038
 
2037
    public final SQLRow copyRecursive(int id) throws SQLException {
2039
    public final SQLRow copyRecursive(int id) throws SQLException {
2038
        return this.copyRecursive(this.getTable().getRow(id));
2040
        return this.copyRecursive(this.getTable().getRow(id));
2039
    }
2041
    }
2040
 
2042
 
2041
    public final SQLRow copyRecursive(SQLRow row) throws SQLException {
2043
    public final SQLRow copyRecursive(SQLRow row) throws SQLException {
2042
        return this.copyRecursive(row, null);
2044
        return this.copyRecursive(row, null);
2043
    }
2045
    }
2044
 
2046
 
2045
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent) throws SQLException {
2047
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent) throws SQLException {
2046
        return this.copyRecursive(row, parent, null);
2048
        return this.copyRecursive(row, parent, null);
2047
    }
2049
    }
2048
 
2050
 
2049
    /**
2051
    /**
2050
     * Copy <code>row</code> and its children into <code>parent</code>.
2052
     * Copy <code>row</code> and its children into <code>parent</code>.
2051
     * 
2053
     * 
2052
     * @param row which row to clone.
2054
     * @param row which row to clone.
2053
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
2055
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
2054
     *        <code>row</code>.
2056
     *        <code>row</code>.
2055
     * @param c allow one to modify the copied rows before they are inserted, can be
2057
     * @param c allow one to modify the copied rows before they are inserted, can be
2056
     *        <code>null</code>.
2058
     *        <code>null</code>.
2057
     * @return the new copy.
2059
     * @return the new copy.
2058
     * @throws SQLException if an error occurs.
2060
     * @throws SQLException if an error occurs.
2059
     */
2061
     */
2060
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
2062
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
2061
        return copyRecursive(row, false, parent, c);
2063
        return copyRecursive(row, false, parent, c);
2062
    }
2064
    }
2063
 
2065
 
2064
    /**
2066
    /**
2065
     * Copy <code>row</code> and its children into <code>parent</code>.
2067
     * Copy <code>row</code> and its children into <code>parent</code>.
2066
     * 
2068
     * 
2067
     * @param row which row to clone.
2069
     * @param row which row to clone.
2068
     * @param full <code>true</code> if {@link #dontDeepCopy()} should be ignored, i.e. an exact
2070
     * @param full <code>true</code> if {@link #dontDeepCopy()} should be ignored, i.e. an exact
2069
     *        copy will be made.
2071
     *        copy will be made.
2070
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
2072
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
2071
     *        <code>row</code>.
2073
     *        <code>row</code>.
2072
     * @param c allow one to modify the copied rows before they are inserted, can be
2074
     * @param c allow one to modify the copied rows before they are inserted, can be
2073
     *        <code>null</code>.
2075
     *        <code>null</code>.
2074
     * @return the new copy.
2076
     * @return the new copy.
2075
     * @throws SQLException if an error occurs.
2077
     * @throws SQLException if an error occurs.
2076
     */
2078
     */
2077
    public SQLRow copyRecursive(final SQLRow row, final boolean full, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
2079
    public SQLRow copyRecursive(final SQLRow row, final boolean full, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
2078
        check(row);
2080
        check(row);
2079
        if (row.isUndefined())
2081
        if (row.isUndefined())
2080
            return row;
2082
            return row;
2081
 
2083
 
2082
        // current => new copy
2084
        // current => new copy
2083
        // contains private and join rows otherwise we can't fix ASSOCIATION
2085
        // contains private and join rows otherwise we can't fix ASSOCIATION
2084
        final Map<SQLRow, SQLRowValues> copies = new HashMap<SQLRow, SQLRowValues>();
2086
        final Map<SQLRow, SQLRowValues> copies = new HashMap<SQLRow, SQLRowValues>();
2085
 
2087
 
2086
        return SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<SQLRow>() {
2088
        return SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<SQLRow>() {
2087
            @Override
2089
            @Override
2088
            public SQLRow create() throws SQLException {
2090
            public SQLRow create() throws SQLException {
2089
 
2091
 
2090
                // eg SITE[128]
2092
                // eg SITE[128]
2091
                final SQLRowValues copy = createTransformedCopy(row, full, parent, copies, c);
2093
                final SQLRowValues copy = createTransformedCopy(row, full, parent, copies, c);
2092
 
2094
 
2093
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2095
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2094
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
2096
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
2095
                        final SQLRowValues parentCopy = copies.get(parent);
2097
                        final SQLRowValues parentCopy = copies.get(parent);
2096
                        if (parentCopy == null)
2098
                        if (parentCopy == null)
2097
                            throw new IllegalStateException("null copy of " + parent);
2099
                            throw new IllegalStateException("null copy of " + parent);
2098
                        final SQLRowValues descCopy = createTransformedCopy(desc, full, null, copies, c);
2100
                        final SQLRowValues descCopy = createTransformedCopy(desc, full, null, copies, c);
2099
                        descCopy.put(joint.getName(), parentCopy);
2101
                        descCopy.put(joint.getName(), parentCopy);
2100
                    }
2102
                    }
2101
                }, full, false, false);
2103
                }, full, false, false);
2102
                // ne pas descendre en deep
2104
                // ne pas descendre en deep
2103
 
2105
 
2104
                // private and parent relationships are already handled, now fix ASSOCIATION : the
2106
                // private and parent relationships are already handled, now fix ASSOCIATION : the
2105
                // associations in the source hierarchy either point outside or inside the
2107
                // associations in the source hierarchy either point outside or inside the
2106
                // hierarchy, for the former the copy is correct. But for the latter, the copy still
2108
                // hierarchy, for the former the copy is correct. But for the latter, the copy still
2107
                // point to the source hierarchy when it should point to copy hierarchy.
2109
                // point to the source hierarchy when it should point to copy hierarchy.
2108
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2110
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2109
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
2111
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
2110
                        for (final SQLElementLink link : getElement(desc.getTable()).getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2112
                        for (final SQLElementLink link : getElement(desc.getTable()).getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2111
                            final Path toRowWFK = link.getPath().minusLast();
2113
                            final Path toRowWFK = link.getPath().minusLast();
2112
                            final Step lastStep = link.getPath().getStep(-1);
2114
                            final Step lastStep = link.getPath().getStep(-1);
2113
                            for (final SQLRow rowWithFK : desc.getDistantRows(toRowWFK)) {
2115
                            for (final SQLRow rowWithFK : desc.getDistantRows(toRowWFK)) {
2114
                                final SQLRow ref = rowWithFK.getForeignRow(lastStep.getSingleLink(), SQLRowMode.NO_CHECK);
2116
                                final SQLRow ref = rowWithFK.getForeignRow(lastStep.getSingleLink(), SQLRowMode.NO_CHECK);
2115
                                // eg copy of SOURCE[12] is SOURCE[354]
2117
                                // eg copy of SOURCE[12] is SOURCE[354]
2116
                                final SQLRowValues refCopy = copies.get(ref);
2118
                                final SQLRowValues refCopy = copies.get(ref);
2117
                                if (refCopy != null) {
2119
                                if (refCopy != null) {
2118
                                    // CPI[1203]
2120
                                    // CPI[1203]
2119
                                    final SQLRowValues rowWithFKCopy = copies.get(rowWithFK);
2121
                                    final SQLRowValues rowWithFKCopy = copies.get(rowWithFK);
2120
                                    rowWithFKCopy.put(lastStep, refCopy);
2122
                                    rowWithFKCopy.put(lastStep, refCopy);
2121
                                }
2123
                                }
2122
                            }
2124
                            }
2123
                        }
2125
                        }
2124
                    }
2126
                    }
2125
                }, full);
2127
                }, full);
2126
 
2128
 
2127
                // we used to remove foreign links pointing outside the copy, but this was almost
2129
                // we used to remove foreign links pointing outside the copy, but this was almost
2128
                // never right, e.g. : copy a batiment, its locals loose ID_FAMILLE ; copy a local,
2130
                // never right, e.g. : copy a batiment, its locals loose ID_FAMILLE ; copy a local,
2129
                // if a source in it points to an item in another local, its copy won't.
2131
                // if a source in it points to an item in another local, its copy won't.
2130
 
2132
 
2131
                return copy.insert();
2133
                return copy.insert();
2132
            }
2134
            }
2133
        });
2135
        });
2134
    }
2136
    }
2135
 
2137
 
2136
    private final SQLRowValues createTransformedCopy(SQLRow desc, final boolean full, SQLRow parent, final Map<SQLRow, SQLRowValues> map, final IClosure<SQLRowValues> c) throws SQLException {
2138
    private final SQLRowValues createTransformedCopy(SQLRow desc, final boolean full, SQLRow parent, final Map<SQLRow, SQLRowValues> map, final IClosure<SQLRowValues> c) throws SQLException {
2137
        final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, full, parent, null, map);
2139
        final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, full, parent, null, map);
2138
        assert copiedVals != null : "failed to copy " + desc;
2140
        assert copiedVals != null : "failed to copy " + desc;
2139
        if (c != null)
2141
        if (c != null)
2140
            c.executeChecked(copiedVals);
2142
            c.executeChecked(copiedVals);
2141
        return copiedVals;
2143
        return copiedVals;
2142
    }
2144
    }
2143
 
2145
 
2144
    public final SQLRow copy(int id) throws SQLException {
2146
    public final SQLRow copy(int id) throws SQLException {
2145
        return this.copy(this.getTable().getRow(id));
2147
        return this.copy(this.getTable().getRow(id));
2146
    }
2148
    }
2147
 
2149
 
2148
    public final SQLRow copy(SQLRow row) throws SQLException {
2150
    public final SQLRow copy(SQLRow row) throws SQLException {
2149
        return this.copy(row, null);
2151
        return this.copy(row, null);
2150
    }
2152
    }
2151
 
2153
 
2152
    public final SQLRow copy(SQLRow row, SQLRow parent) throws SQLException {
2154
    public final SQLRow copy(SQLRow row, SQLRow parent) throws SQLException {
2153
        final SQLRowValues copy = this.createCopy(row, parent);
2155
        final SQLRowValues copy = this.createCopy(row, parent);
2154
        return copy == null ? row : copy.insert();
2156
        return copy == null ? row : copy.insert();
2155
    }
2157
    }
2156
 
2158
 
2157
    public final SQLRowValues createCopy(int id) {
2159
    public final SQLRowValues createCopy(int id) {
2158
        final SQLRow row = this.getTable().getRow(id);
2160
        final SQLRow row = this.getTable().getRow(id);
2159
        return this.createCopy(row, null);
2161
        return this.createCopy(row, null);
2160
    }
2162
    }
2161
 
2163
 
2162
    /**
2164
    /**
2163
     * Copies the passed row into an SQLRowValues. NOTE: this method will only access the DB if
2165
     * Copies the passed row into an SQLRowValues. NOTE: this method will only access the DB if
2164
     * necessary : when <code>row</code> is not an {@link SQLRowValues} and this element has
2166
     * necessary : when <code>row</code> is not an {@link SQLRowValues} and this element has
2165
     * {@link LinkType#COMPOSITION privates} or {@link SQLElementLink#isJoin() joins}. Otherwise the
2167
     * {@link LinkType#COMPOSITION privates} or {@link SQLElementLink#isJoin() joins}. Otherwise the
2166
     * copy won't be a copy of the current values in DB, but of the current values of the passed
2168
     * copy won't be a copy of the current values in DB, but of the current values of the passed
2167
     * instance.
2169
     * instance.
2168
     * 
2170
     * 
2169
     * @param row the row to copy, can be <code>null</code>.
2171
     * @param row the row to copy, can be <code>null</code>.
2170
     * @param parent the parent the copy will be in, <code>null</code> meaning the same as
2172
     * @param parent the parent the copy will be in, <code>null</code> meaning the same as
2171
     *        <code>row</code>. If it's an {@link SQLRowValues} it will be used directly, otherwise
2173
     *        <code>row</code>. If it's an {@link SQLRowValues} it will be used directly, otherwise
2172
     *        {@link SQLRowAccessor#getIDNumber()} will be used (i.e. if the copy isn't to be linked
2174
     *        {@link SQLRowAccessor#getIDNumber()} will be used (i.e. if the copy isn't to be linked
2173
     *        to its parent, pass a {@link SQLRowAccessor#asRow() row}).
2175
     *        to its parent, pass a {@link SQLRowAccessor#asRow() row}).
2174
     * @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
2176
     * @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
2175
     *         copied.
2177
     *         copied.
2176
     */
2178
     */
2177
    public SQLRowValues createCopy(SQLRowAccessor row, SQLRowAccessor parent) {
2179
    public SQLRowValues createCopy(SQLRowAccessor row, SQLRowAccessor parent) {
2178
        return createCopy(row, false, parent);
2180
        return createCopy(row, false, parent);
2179
    }
2181
    }
2180
 
2182
 
2181
    public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent) {
2183
    public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent) {
2182
        return this.createCopy(row, full, parent, null, null);
2184
        return this.createCopy(row, full, parent, null, null);
2183
    }
2185
    }
2184
 
2186
 
2185
    public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap) {
2187
    public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRowAccessor parent, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap) {
2186
        // do NOT copy the undefined
2188
        // do NOT copy the undefined
2187
        if (row == null || row.isUndefined())
2189
        if (row == null || row.isUndefined())
2188
            return null;
2190
            return null;
2189
        this.check(row);
2191
        this.check(row);
2190
 
2192
 
2191
        final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
2193
        final Set<SQLElementLink> privates = this.getOwnedLinks().getByType(LinkType.COMPOSITION);
2192
        final SQLRowValues privateGraph = this.createGraph(VirtualFields.ALL, !full ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, true);
2194
        final SQLRowValues privateGraph = this.createGraph(VirtualFields.ALL, !full ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, true);
2193
        // Don't make one request per private, just fetch the whole graph at once
2195
        // Don't make one request per private, just fetch the whole graph at once
2194
        // further with joined privates an SQLRow cannot contain privates nor carry the lack of them
2196
        // further with joined privates an SQLRow cannot contain privates nor carry the lack of them
2195
        // (without joins a row lacking privates was passed with just an SQLRow with undefined
2197
        // (without joins a row lacking privates was passed with just an SQLRow with undefined
2196
        // foreign keys).
2198
        // foreign keys).
2197
        final SQLRowValues rowVals;
2199
        final SQLRowValues rowVals;
2198
        if (row instanceof SQLRowValues) {
2200
        if (row instanceof SQLRowValues) {
2199
            rowVals = (SQLRowValues) row;
2201
            rowVals = (SQLRowValues) row;
2200
        } else if (privateGraph.getGraphSize() == 1) {
2202
        } else if (privateGraph.getGraphSize() == 1) {
2201
            rowVals = null;
2203
            rowVals = null;
2202
        } else {
2204
        } else {
2203
            final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph);
2205
            final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(privateGraph);
2204
            fetcher.setSelID(row.getIDNumber());
2206
            fetcher.setSelID(row.getIDNumber());
2205
            rowVals = CollectionUtils.getSole(fetcher.fetch());
2207
            rowVals = CollectionUtils.getSole(fetcher.fetch());
2206
            if (rowVals == null)
2208
            if (rowVals == null)
2207
                throw new IllegalStateException("Not exactly one row for " + row);
2209
                throw new IllegalStateException("Not exactly one row for " + row);
2208
        }
2210
        }
2209
        // Use just fetched values so that data is coherent.
2211
        // Use just fetched values so that data is coherent.
2210
        final SQLRowAccessor upToDateRow = rowVals != null ? rowVals : row;
2212
        final SQLRowAccessor upToDateRow = rowVals != null ? rowVals : row;
2211
 
2213
 
2212
        final SQLRowValues copy = new SQLRowValues(this.getTable());
2214
        final SQLRowValues copy = new SQLRowValues(this.getTable());
2213
        this.loadAllSafe(copy, upToDateRow);
2215
        this.loadAllSafe(copy, upToDateRow);
2214
        if (valsMap != null) {
2216
        if (valsMap != null) {
2215
            if (rowVals == null)
2217
            if (rowVals == null)
2216
                throw new IllegalArgumentException("Cannot fill map since no SQLRowValues were provided");
2218
                throw new IllegalArgumentException("Cannot fill map since no SQLRowValues were provided");
2217
            valsMap.put(rowVals, copy);
2219
            valsMap.put(rowVals, copy);
2218
        }
2220
        }
2219
        if (rowMap != null) {
2221
        if (rowMap != null) {
2220
            if (!upToDateRow.hasID())
2222
            if (!upToDateRow.hasID())
2221
                throw new IllegalArgumentException("Cannot fill map since no SQLRow were provided");
2223
                throw new IllegalArgumentException("Cannot fill map since no SQLRow were provided");
2222
            rowMap.put(upToDateRow.asRow(), copy);
2224
            rowMap.put(upToDateRow.asRow(), copy);
2223
        }
2225
        }
2224
 
2226
 
2225
        for (final SQLElementLink privateLink : privates) {
2227
        for (final SQLElementLink privateLink : privates) {
2226
            final SQLElement privateElement = privateLink.getOwned();
2228
            final SQLElement privateElement = privateLink.getOwned();
2227
            final boolean deepCopy = full || !privateElement.dontDeepCopy();
2229
            final boolean deepCopy = full || !privateElement.dontDeepCopy();
2228
            if (!privateLink.isJoin()) {
2230
            if (!privateLink.isJoin()) {
2229
                final String privateName = privateLink.getSingleField().getName();
2231
                final String privateName = privateLink.getSingleField().getName();
2230
                if (deepCopy && !rowVals.isForeignEmpty(privateName)) {
2232
                if (deepCopy && !rowVals.isForeignEmpty(privateName)) {
2231
                    final SQLRowValues foreign = checkPrivateLoaded(privateLink, rowVals.getForeign(privateName));
2233
                    final SQLRowValues foreign = checkPrivateLoaded(privateLink, rowVals.getForeign(privateName));
2232
                    final SQLRowValues child = privateElement.createCopy(foreign, full, null, valsMap, rowMap);
2234
                    final SQLRowValues child = privateElement.createCopy(foreign, full, null, valsMap, rowMap);
2233
                    copy.put(privateName, child);
2235
                    copy.put(privateName, child);
2234
                    // use upToDateRow instead of rowVals since the latter might be null if
2236
                    // use upToDateRow instead of rowVals since the latter might be null if
2235
                    // !full
2237
                    // !full
2236
                } else if (upToDateRow.getFields().contains(privateName)) {
2238
                } else if (upToDateRow.getFields().contains(privateName)) {
2237
                    copy.putEmptyLink(privateName);
2239
                    copy.putEmptyLink(privateName);
2238
                }
2240
                }
2239
            } else {
2241
            } else {
2240
                // join
2242
                // join
2241
                assert privateLink.getPath().getStep(0).getDirection() == Direction.REFERENT;
2243
                assert privateLink.getPath().getStep(0).getDirection() == Direction.REFERENT;
2242
                if (deepCopy) {
2244
                if (deepCopy) {
2243
                    copyJoin(rowVals, full, valsMap, rowMap, copy, privateLink);
2245
                    copyJoin(rowVals, full, valsMap, rowMap, copy, privateLink);
2244
                } // else nothing to do since there's no fields in copy
2246
                } // else nothing to do since there's no fields in copy
2245
            }
2247
            }
2246
        }
2248
        }
2247
 
2249
 
2248
        for (final SQLElementLink association : this.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2250
        for (final SQLElementLink association : this.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2249
            if (association.isJoin()) {
2251
            if (association.isJoin()) {
2250
                copyJoin(rowVals, full, valsMap, rowMap, copy, association);
2252
                copyJoin(rowVals, full, valsMap, rowMap, copy, association);
2251
            } // else fields already in copy
2253
            } // else fields already in copy
2252
        }
2254
        }
2253
 
2255
 
2254
        // si on a spécifié un parent, eg BATIMENT[23]
2256
        // si on a spécifié un parent, eg BATIMENT[23]
2255
        if (parent != null) {
2257
        if (parent != null) {
2256
            final SQLTable foreignTable = this.getParentForeignField().getForeignTable();
2258
            final SQLTable foreignTable = this.getParentForeignField().getForeignTable();
2257
            if (!parent.getTable().equals(foreignTable))
2259
            if (!parent.getTable().equals(foreignTable))
2258
                throw new IllegalArgumentException(parent + " is not a parent of " + row);
2260
                throw new IllegalArgumentException(parent + " is not a parent of " + row);
2259
            copy.put(this.getParentForeignFieldName(), parent instanceof SQLRowValues ? parent : parent.getIDNumber());
2261
            copy.put(this.getParentForeignFieldName(), parent instanceof SQLRowValues ? parent : parent.getIDNumber());
2260
        }
2262
        }
2261
 
2263
 
2262
        return copy;
2264
        return copy;
2263
    }
2265
    }
2264
 
2266
 
2265
    private SQLRowValues checkPrivateLoaded(final SQLElementLink privateLink, final SQLRowAccessor foreign) {
2267
    private SQLRowValues checkPrivateLoaded(final SQLElementLink privateLink, final SQLRowAccessor foreign) {
2266
        assert privateLink.getLinkType() == LinkType.COMPOSITION && privateLink.getOwned().getTable() == foreign.getTable();
2268
        assert privateLink.getLinkType() == LinkType.COMPOSITION && privateLink.getOwned().getTable() == foreign.getTable();
2267
        // otherwise the recursive call will fetch the missing data, which could be
2269
        // otherwise the recursive call will fetch the missing data, which could be
2268
        // incoherent with rowVals
2270
        // incoherent with rowVals
2269
        if (!(foreign instanceof SQLRowValues))
2271
        if (!(foreign instanceof SQLRowValues))
2270
            throw new IllegalStateException("Graph missing non-empty private for " + privateLink);
2272
            throw new IllegalStateException("Graph missing non-empty private for " + privateLink);
2271
        return (SQLRowValues) foreign;
2273
        return (SQLRowValues) foreign;
2272
    }
2274
    }
2273
 
2275
 
2274
    private final void copyJoin(final SQLRowValues rowVals, final boolean full, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap,
2276
    private final void copyJoin(final SQLRowValues rowVals, final boolean full, final IdentityHashMap<SQLRowValues, SQLRowValues> valsMap, final Map<SQLRow, SQLRowValues> rowMap,
2275
            final SQLRowValues copy, final SQLElementLink link) {
2277
            final SQLRowValues copy, final SQLElementLink link) {
2276
        assert link.isJoin();
2278
        assert link.isJoin();
2277
        final Step firstStep = link.getPath().getStep(0);
2279
        final Step firstStep = link.getPath().getStep(0);
2278
        final SQLElement joinElem = getElement(firstStep.getTo());
2280
        final SQLElement joinElem = getElement(firstStep.getTo());
2279
        final Step lastStep = link.getPath().getStep(-1);
2281
        final Step lastStep = link.getPath().getStep(-1);
2280
        for (final SQLRowValues joinToCopy : rowVals.followPath(link.getPath().minusLast(), CreateMode.CREATE_NONE, false)) {
2282
        for (final SQLRowValues joinToCopy : rowVals.followPath(link.getPath().minusLast(), CreateMode.CREATE_NONE, false)) {
2281
            final SQLRowValues joinCopy = new SQLRowValues(joinElem.getTable());
2283
            final SQLRowValues joinCopy = new SQLRowValues(joinElem.getTable());
2282
            joinElem.loadAllSafe(joinCopy, joinToCopy, link.getLinkType() == LinkType.COMPOSITION);
2284
            joinElem.loadAllSafe(joinCopy, joinToCopy, link.getLinkType() == LinkType.COMPOSITION);
2283
            copy.put(firstStep, joinCopy);
2285
            copy.put(firstStep, joinCopy);
2284
            if (valsMap != null)
2286
            if (valsMap != null)
2285
                valsMap.put(joinToCopy, joinCopy);
2287
                valsMap.put(joinToCopy, joinCopy);
2286
            if (rowMap != null)
2288
            if (rowMap != null)
2287
                rowMap.put(joinToCopy.asRow(), joinCopy);
2289
                rowMap.put(joinToCopy.asRow(), joinCopy);
2288
            // copy private
2290
            // copy private
2289
            if (link.getLinkType() == LinkType.COMPOSITION) {
2291
            if (link.getLinkType() == LinkType.COMPOSITION) {
2290
                final SQLElement privateElement = link.getOwned();
2292
                final SQLElement privateElement = link.getOwned();
2291
                final SQLRowAccessor privateRow = joinToCopy.getForeign(lastStep.getSingleLink());
2293
                final SQLRowAccessor privateRow = joinToCopy.getForeign(lastStep.getSingleLink());
2292
                if (privateRow.isUndefined())
2294
                if (privateRow.isUndefined())
2293
                    throw new IllegalStateException("Joined to undefined " + link);
2295
                    throw new IllegalStateException("Joined to undefined " + link);
2294
                checkPrivateLoaded(link, privateRow);
2296
                checkPrivateLoaded(link, privateRow);
2295
                final SQLRowValues privateCopy = privateElement.createCopy(privateRow, full, null, valsMap, rowMap);
2297
                final SQLRowValues privateCopy = privateElement.createCopy(privateRow, full, null, valsMap, rowMap);
2296
                joinCopy.put(lastStep, privateCopy);
2298
                joinCopy.put(lastStep, privateCopy);
2297
            }
2299
            }
2298
            assert !joinCopy.hasID() && joinCopy.getFields().containsAll(lastStep.getSingleLink().getCols());
2300
            assert !joinCopy.hasID() && joinCopy.getFields().containsAll(lastStep.getSingleLink().getCols());
2299
        }
2301
        }
2300
    }
2302
    }
2301
 
2303
 
2302
    static private final VirtualFields JOIN_SAFE_FIELDS = VirtualFields.ALL.difference(VirtualFields.PRIMARY_KEY, VirtualFields.ORDER);
2304
    static private final VirtualFields JOIN_SAFE_FIELDS = VirtualFields.ALL.difference(VirtualFields.PRIMARY_KEY, VirtualFields.ORDER);
2303
    static private final VirtualFields SAFE_FIELDS = JOIN_SAFE_FIELDS.difference(VirtualFields.FOREIGN_KEYS);
2305
    static private final VirtualFields SAFE_FIELDS = JOIN_SAFE_FIELDS.difference(VirtualFields.FOREIGN_KEYS);
2304
 
2306
 
2305
    /**
2307
    /**
2306
     * Load all values that can be safely copied (shared by multiple rows). This means all values
2308
     * Load all values that can be safely copied (shared by multiple rows). This means all values
2307
     * except private, primary, and order.
2309
     * except private, primary, and order.
2308
     * 
2310
     * 
2309
     * @param vals the row to modify.
2311
     * @param vals the row to modify.
2310
     * @param row the row to be loaded.
2312
     * @param row the row to be loaded.
2311
     */
2313
     */
2312
    public final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row) {
2314
    public final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row) {
2313
        this.loadAllSafe(vals, row, null);
2315
        this.loadAllSafe(vals, row, null);
2314
    }
2316
    }
2315
 
2317
 
2316
    private final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row, final Boolean isPrivateJoinElement) {
2318
    private final void loadAllSafe(final SQLRowValues vals, final SQLRowAccessor row, final Boolean isPrivateJoinElement) {
2317
        check(vals);
2319
        check(vals);
2318
        check(row);
2320
        check(row);
2319
        // JoinSQLElement has no links but we still want to copy metadata
2321
        // JoinSQLElement has no links but we still want to copy metadata
2320
        if (this instanceof JoinSQLElement) {
2322
        if (this instanceof JoinSQLElement) {
2321
            if (isPrivateJoinElement == null)
2323
            if (isPrivateJoinElement == null)
2322
                throw new IllegalStateException("joins are not public");
2324
                throw new IllegalStateException("joins are not public");
2323
            assert this.getOwnedLinks().getByPath().size() == 0;
2325
            assert this.getOwnedLinks().getByPath().size() == 0;
2324
            vals.setAll(row.getValues(JOIN_SAFE_FIELDS));
2326
            vals.setAll(row.getValues(JOIN_SAFE_FIELDS));
2325
            // remove links to owned if private join
2327
            // remove links to owned if private join
2326
            final Path pathFromOwner = ((JoinSQLElement) this).getPathFromOwner();
2328
            final Path pathFromOwner = ((JoinSQLElement) this).getPathFromOwner();
2327
            assert pathFromOwner.length() == 2;
2329
            assert pathFromOwner.length() == 2;
2328
            if (isPrivateJoinElement)
2330
            if (isPrivateJoinElement)
2329
                vals.remove(pathFromOwner.getStep(1));
2331
                vals.remove(pathFromOwner.getStep(1));
2330
        } else {
2332
        } else {
2331
            if (isPrivateJoinElement != null)
2333
            if (isPrivateJoinElement != null)
2332
                throw new IllegalStateException("should a join : " + this);
2334
                throw new IllegalStateException("should a join : " + this);
2333
            // Don't copy foreign keys then remove privates (i.e. JOIN_SAFE_FIELDS), as this will
2335
            // Don't copy foreign keys then remove privates (i.e. JOIN_SAFE_FIELDS), as this will
2334
            // copy ignored paths (see SQLElementLinkSetup.ignore()) and they might be privates
2336
            // copy ignored paths (see SQLElementLinkSetup.ignore()) and they might be privates
2335
            vals.setAll(row.getValues(SAFE_FIELDS));
2337
            vals.setAll(row.getValues(SAFE_FIELDS));
2336
            for (final SQLElementLink l : this.getOwnedLinks().getByPath().values()) {
2338
            for (final SQLElementLink l : this.getOwnedLinks().getByPath().values()) {
2337
                if (l.getLinkType() != LinkType.COMPOSITION && !l.isJoin()) {
2339
                if (l.getLinkType() != LinkType.COMPOSITION && !l.isJoin()) {
2338
                    vals.putAll(row.getValues(l.getSingleLink().getCols()));
2340
                    vals.putAll(row.getValues(l.getSingleLink().getCols()));
2339
                }
2341
                }
2340
            }
2342
            }
2341
        }
2343
        }
2342
    }
2344
    }
2343
 
2345
 
2344
    // *** getRows
2346
    // *** getRows
2345
 
2347
 
2346
    /**
2348
    /**
2347
     * Returns the descendant rows : the children of this element, recursively. ATTN does not carry
2349
     * Returns the descendant rows : the children of this element, recursively. ATTN does not carry
2348
     * the hierarchy.
2350
     * the hierarchy.
2349
     * 
2351
     * 
2350
     * @param row a SQLRow.
2352
     * @param row a SQLRow.
2351
     * @return the descendant rows by SQLTable.
2353
     * @return the descendant rows by SQLTable.
2352
     */
2354
     */
2353
    public final ListMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
2355
    public final ListMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
2354
        check(row);
2356
        check(row);
2355
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
2357
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
2356
        try {
2358
        try {
2357
            this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2359
            this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
2358
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
2360
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
2359
                    mm.add(joint.getTable(), child);
2361
                    mm.add(joint.getTable(), child);
2360
                }
2362
                }
2361
            }, true);
2363
            }, true);
2362
        } catch (SQLException e) {
2364
        } catch (SQLException e) {
2363
            // never happen
2365
            // never happen
2364
            e.printStackTrace();
2366
            e.printStackTrace();
2365
        }
2367
        }
2366
        return mm;
2368
        return mm;
2367
    }
2369
    }
2368
 
2370
 
2369
    /**
2371
    /**
2370
     * Returns the tree beneath the passed row.
2372
     * Returns the tree beneath the passed row.
2371
     * 
2373
     * 
2372
     * @param row the root of the desired tree.
2374
     * @param row the root of the desired tree.
2373
     * @param archived <code>true</code> if the returned rows should be archived.
2375
     * @param archived <code>true</code> if the returned rows should be archived.
2374
     * @return the asked tree.
2376
     * @return the asked tree.
2375
     */
2377
     */
2376
    private SQLRowValues getTree(SQLRow row, boolean archived) {
2378
    private SQLRowValues getTree(SQLRow row, boolean archived) {
2377
        check(row);
2379
        check(row);
2378
        final SQLRowValues res = row.asRowValues();
2380
        final SQLRowValues res = row.asRowValues();
2379
        try {
2381
        try {
2380
            this.forDescendantsDo(res, new ChildProcessor<SQLRowValues>() {
2382
            this.forDescendantsDo(res, new ChildProcessor<SQLRowValues>() {
2381
                public void process(SQLRowValues parent, SQLField joint, SQLRowValues desc) throws SQLException {
2383
                public void process(SQLRowValues parent, SQLField joint, SQLRowValues desc) throws SQLException {
2382
                    desc.put(joint.getName(), parent);
2384
                    desc.put(joint.getName(), parent);
2383
                }
2385
                }
2384
            }, true, false, archived);
2386
            }, true, false, archived);
2385
        } catch (SQLException e) {
2387
        } catch (SQLException e) {
2386
            // never happen cause process don't throw it
2388
            // never happen cause process don't throw it
2387
            e.printStackTrace();
2389
            e.printStackTrace();
2388
        }
2390
        }
2389
        return res;
2391
        return res;
2390
    }
2392
    }
2391
 
2393
 
2392
    /**
2394
    /**
2393
     * Returns the children of the passed row.
2395
     * Returns the children of the passed row.
2394
     * 
2396
     * 
2395
     * @param row a SQLRow.
2397
     * @param row a SQLRow.
2396
     * @return the children rows by SQLTable.
2398
     * @return the children rows by SQLTable.
2397
     */
2399
     */
2398
    public ListMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
2400
    public ListMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
2399
        check(row);
2401
        check(row);
2400
        // List to retain order
2402
        // List to retain order
2401
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
2403
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
2402
        try {
2404
        try {
2403
            this.forChildrenDo(row, new ChildProcessor<SQLRow>() {
2405
            this.forChildrenDo(row, new ChildProcessor<SQLRow>() {
2404
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
2406
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
2405
                    mm.add(child.getTable(), child);
2407
                    mm.add(child.getTable(), child);
2406
                }
2408
                }
2407
            }, true, false);
2409
            }, true, false);
2408
        } catch (SQLException e) {
2410
        } catch (SQLException e) {
2409
            // never happen
2411
            // never happen
2410
            e.printStackTrace();
2412
            e.printStackTrace();
2411
        }
2413
        }
2412
        // TODO return Map of SQLElement instead of SQLTable (this avoids the caller a call to
2414
        // TODO return Map of SQLElement instead of SQLTable (this avoids the caller a call to
2413
        // getDirectory())
2415
        // getDirectory())
2414
        return mm;
2416
        return mm;
2415
    }
2417
    }
2416
 
2418
 
2417
    public SQLRowValues getContainer(final SQLRowValues row) {
2419
    public SQLRowValues getContainer(final SQLRowValues row) {
2418
        return this.getContainer(row, true, true);
2420
        return this.getContainer(row, true, true);
2419
    }
2421
    }
2420
 
2422
 
2421
    public final SQLRowValues getContainer(final SQLRowValues row, final boolean privateParent, final boolean parent) {
2423
    public final SQLRowValues getContainer(final SQLRowValues row, final boolean privateParent, final boolean parent) {
2422
        check(row);
2424
        check(row);
2423
        if (row.isUndefined() || !privateParent && !parent)
2425
        if (row.isUndefined() || !privateParent && !parent)
2424
            return null;
2426
            return null;
2425
 
2427
 
2426
        final List<SQLRowValues> parents = new ArrayList<SQLRowValues>();
2428
        final List<SQLRowValues> parents = new ArrayList<SQLRowValues>();
2427
        for (final SQLElementLink l : this.getContainerLinks(privateParent, parent).getByPath().values()) {
2429
        for (final SQLElementLink l : this.getContainerLinks(privateParent, parent).getByPath().values()) {
2428
            parents.addAll(row.followPath(l.getPathToParent(), CreateMode.CREATE_NONE, true));
2430
            parents.addAll(row.followPath(l.getPathToParent(), CreateMode.CREATE_NONE, true));
2429
        }
2431
        }
2430
        if (parents.size() > 1)
2432
        if (parents.size() > 1)
2431
            throw new IllegalStateException("More than one parent for " + row + " : " + parents);
2433
            throw new IllegalStateException("More than one parent for " + row + " : " + parents);
2432
        return parents.size() == 0 ? null : parents.get(0);
2434
        return parents.size() == 0 ? null : parents.get(0);
2433
    }
2435
    }
2434
 
2436
 
2435
    @Deprecated
2437
    @Deprecated
2436
    public SQLRow getForeignParent(SQLRow row) {
2438
    public SQLRow getForeignParent(SQLRow row) {
2437
        return this.getForeignParent(row, SQLRowMode.VALID);
2439
        return this.getForeignParent(row, SQLRowMode.VALID);
2438
    }
2440
    }
2439
 
2441
 
2440
    // ATTN cannot replace with getParent(SQLRowAccessor) since some callers assume the result to be
2442
    // ATTN cannot replace with getParent(SQLRowAccessor) since some callers assume the result to be
2441
    // a foreign row (which isn't the case for private)
2443
    // a foreign row (which isn't the case for private)
2442
    @Deprecated
2444
    @Deprecated
2443
    private SQLRow getForeignParent(SQLRow row, final SQLRowMode mode) {
2445
    private SQLRow getForeignParent(SQLRow row, final SQLRowMode mode) {
2444
        check(row);
2446
        check(row);
2445
        return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode);
2447
        return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode);
2446
    }
2448
    }
2447
 
2449
 
2448
    public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter) {
2450
    public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter) {
2449
        return this.fetchPrivateParent(row, modifyParameter, ArchiveMode.UNARCHIVED);
2451
        return this.fetchPrivateParent(row, modifyParameter, ArchiveMode.UNARCHIVED);
2450
    }
2452
    }
2451
 
2453
 
2452
    /**
2454
    /**
2453
     * Return the parent if any of the passed row. This method will access the DB.
2455
     * Return the parent if any of the passed row. This method will access the DB.
2454
     * 
2456
     * 
2455
     * @param row the row.
2457
     * @param row the row.
2456
     * @param modifyParameter <code>true</code> if <code>row</code> can be linked to the result,
2458
     * @param modifyParameter <code>true</code> if <code>row</code> can be linked to the result,
2457
     *        <code>false</code> to link a new {@link SQLRowValues}.
2459
     *        <code>false</code> to link a new {@link SQLRowValues}.
2458
     * @param archiveMode the parent must match this mode.
2460
     * @param archiveMode the parent must match this mode.
2459
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
2461
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
2460
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
2462
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
2461
     * @throws IllegalStateException if <code>row</code> has more than one parent matching.
2463
     * @throws IllegalStateException if <code>row</code> has more than one parent matching.
2462
     */
2464
     */
2463
    public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode) {
2465
    public final SQLRowValues fetchPrivateParent(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode) {
2464
        return this.fetchContainer(row, modifyParameter, archiveMode, true, false);
2466
        return this.fetchContainer(row, modifyParameter, archiveMode, true, false);
2465
    }
2467
    }
2466
 
2468
 
2467
    public final SQLRowValues fetchContainer(final SQLRowAccessor row) {
2469
    public final SQLRowValues fetchContainer(final SQLRowAccessor row) {
2468
        return fetchContainer(row, ArchiveMode.UNARCHIVED);
2470
        return fetchContainer(row, ArchiveMode.UNARCHIVED);
2469
    }
2471
    }
2470
 
2472
 
2471
    public final SQLRowValues fetchContainer(final SQLRowAccessor row, final ArchiveMode archiveMode) {
2473
    public final SQLRowValues fetchContainer(final SQLRowAccessor row, final ArchiveMode archiveMode) {
2472
        return this.fetchContainer(row, false, archiveMode, true, true);
2474
        return this.fetchContainer(row, false, archiveMode, true, true);
2473
    }
2475
    }
2474
 
2476
 
2475
    static private SQLField getToID(final Step s) {
2477
    static private SQLField getToID(final Step s) {
2476
        return s.isForeign() ? s.getSingleField() : s.getTo().getKey();
2478
        return s.isForeign() ? s.getSingleField() : s.getTo().getKey();
2477
    }
2479
    }
2478
 
2480
 
2479
    public final SQLRowValues fetchContainer(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode, final boolean privateParent, final boolean parent) {
2481
    public final SQLRowValues fetchContainer(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode, final boolean privateParent, final boolean parent) {
2480
        check(row);
2482
        check(row);
2481
        if (row.isUndefined() || !privateParent && !parent)
2483
        if (row.isUndefined() || !privateParent && !parent)
2482
            return null;
2484
            return null;
2483
        final SQLSyntax syntax = SQLSyntax.get(getTable());
2485
        final SQLSyntax syntax = SQLSyntax.get(getTable());
2484
        final List<SQLElementLink> parentLinks = new ArrayList<SQLElementLink>(this.getContainerLinks(privateParent, parent).getByPath().values());
2486
        final List<SQLElementLink> parentLinks = new ArrayList<SQLElementLink>(this.getContainerLinks(privateParent, parent).getByPath().values());
2485
        if (parentLinks.isEmpty())
2487
        if (parentLinks.isEmpty())
2486
            return null;
2488
            return null;
2487
        final ListIterator<SQLElementLink> listIter = parentLinks.listIterator();
2489
        final ListIterator<SQLElementLink> listIter = parentLinks.listIterator();
2488
        final List<String> selects = new ArrayList<String>(parentLinks.size());
2490
        final List<String> selects = new ArrayList<String>(parentLinks.size());
2489
        while (listIter.hasNext()) {
2491
        while (listIter.hasNext()) {
2490
            final SQLElementLink parentLink = listIter.next();
2492
            final SQLElementLink parentLink = listIter.next();
2491
 
2493
 
2492
            final SQLSelect sel = new SQLSelect(true);
2494
            final SQLSelect sel = new SQLSelect(true);
2493
            sel.addSelect(getToID(parentLink.getStepToParent()), null, "parentID");
2495
            sel.addSelect(getToID(parentLink.getStepToParent()), null, "parentID");
2494
            final SQLField joinPK = parentLink.getPath().getTable(1).getKey();
2496
            final SQLField joinPK = parentLink.getPath().getTable(1).getKey();
2495
            if (parentLink.isJoin()) {
2497
            if (parentLink.isJoin()) {
2496
                sel.addSelect(joinPK, null, "joinID");
2498
                sel.addSelect(joinPK, null, "joinID");
2497
            } else {
2499
            } else {
2498
                sel.addRawSelect(syntax.cast("NULL", joinPK.getType()), "joinID");
2500
                sel.addRawSelect(syntax.cast("NULL", joinPK.getType()), "joinID");
2499
            }
2501
            }
2500
            sel.addRawSelect(String.valueOf(listIter.previousIndex()), "fieldIndex");
2502
            sel.addRawSelect(String.valueOf(listIter.previousIndex()), "fieldIndex");
2501
            sel.setArchivedPolicy(archiveMode);
2503
            sel.setArchivedPolicy(archiveMode);
2502
            sel.setWhere(new Where(getToID(parentLink.getStepToChild()), "=", row.getIDNumber()));
2504
            sel.setWhere(new Where(getToID(parentLink.getStepToChild()), "=", row.getIDNumber()));
2503
 
2505
 
2504
            assert sel.getTableRefs().size() == 1 : "Non optimal query";
2506
            assert sel.getTableRefs().size() == 1 : "Non optimal query";
2505
            selects.add(sel.asString());
2507
            selects.add(sel.asString());
2506
        }
2508
        }
2507
        final List<?> parentIDs = getTable().getDBSystemRoot().getDataSource().executeA(CollectionUtils.join(selects, "\nUNION ALL "));
2509
        final List<?> parentIDs = getTable().getDBSystemRoot().getDataSource().executeA(CollectionUtils.join(selects, "\nUNION ALL "));
2508
        if (parentIDs.size() > 1)
2510
        if (parentIDs.size() > 1)
2509
            throw new IllegalStateException("More than one parent for " + row + " : " + parentIDs);
2511
            throw new IllegalStateException("More than one parent for " + row + " : " + parentIDs);
2510
        else if (parentIDs.size() == 0)
2512
        else if (parentIDs.size() == 0)
2511
            // e.g. no UNARCHIVED parent of an ARCHIVED private
2513
            // e.g. no UNARCHIVED parent of an ARCHIVED private
2512
            return null;
2514
            return null;
2513
 
2515
 
2514
        final Object[] idAndIndex = (Object[]) parentIDs.get(0);
2516
        final Object[] idAndIndex = (Object[]) parentIDs.get(0);
2515
        final Number mainID = (Number) idAndIndex[0];
2517
        final Number mainID = (Number) idAndIndex[0];
2516
        final Number joinID = (Number) idAndIndex[1];
2518
        final Number joinID = (Number) idAndIndex[1];
2517
        final SQLElementLink parentLink = parentLinks.get(((Number) idAndIndex[2]).intValue());
2519
        final SQLElementLink parentLink = parentLinks.get(((Number) idAndIndex[2]).intValue());
2518
        final Path toChildPath = parentLink.getPathToChild();
2520
        final Path toChildPath = parentLink.getPathToChild();
2519
        final SQLRowValues res = new SQLRowValues(toChildPath.getTable(0)).setID(mainID);
2521
        final SQLRowValues res = new SQLRowValues(toChildPath.getTable(0)).setID(mainID);
2520
        final SQLRowValues rowWithFK;
2522
        final SQLRowValues rowWithFK;
2521
        if (parentLink.isJoin()) {
2523
        if (parentLink.isJoin()) {
2522
            if (joinID == null)
2524
            if (joinID == null)
2523
                throw new IllegalStateException("Missing join ID for " + parentLink);
2525
                throw new IllegalStateException("Missing join ID for " + parentLink);
2524
            final Step parentToJoin = toChildPath.getStep(0);
2526
            final Step parentToJoin = toChildPath.getStep(0);
2525
            rowWithFK = res.putRowValues(parentToJoin).setID(joinID);
2527
            rowWithFK = res.putRowValues(parentToJoin).setID(joinID);
2526
        } else {
2528
        } else {
2527
            rowWithFK = res;
2529
            rowWithFK = res;
2528
        }
2530
        }
2529
        assert rowWithFK.hasID();
2531
        assert rowWithFK.hasID();
2530
        // first convert to SQLRow to avoid modifying the (graph of our) method parameter
2532
        // first convert to SQLRow to avoid modifying the (graph of our) method parameter
2531
        rowWithFK.put(toChildPath.getStep(-1), (modifyParameter ? row : row.asRow()).asRowValues());
2533
        rowWithFK.put(toChildPath.getStep(-1), (modifyParameter ? row : row.asRow()).asRowValues());
2532
        return res;
2534
        return res;
2533
    }
2535
    }
2534
 
2536
 
2535
    /**
2537
    /**
2536
     * Return the main row if any of the passed row. This method will access the DB.
2538
     * Return the main row if any of the passed row. This method will access the DB.
2537
     * 
2539
     * 
2538
     * @param row the row, if it's a {@link SQLRowValues} it will be linked to the result.
2540
     * @param row the row, if it's a {@link SQLRowValues} it will be linked to the result.
2539
     * @param archiveMode the parent must match this mode.
2541
     * @param archiveMode the parent must match this mode.
2540
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
2542
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
2541
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
2543
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
2542
     * @see #fetchPrivateParent(SQLRowAccessor, boolean, ArchiveMode)
2544
     * @see #fetchPrivateParent(SQLRowAccessor, boolean, ArchiveMode)
2543
     */
2545
     */
2544
    public final SQLRowValues fetchPrivateRoot(SQLRowAccessor row, final ArchiveMode archiveMode) {
2546
    public final SQLRowValues fetchPrivateRoot(SQLRowAccessor row, final ArchiveMode archiveMode) {
2545
        SQLRowValues prev = null;
2547
        SQLRowValues prev = null;
2546
        SQLRowValues res = fetchPrivateParent(row, true, archiveMode);
2548
        SQLRowValues res = fetchPrivateParent(row, true, archiveMode);
2547
        while (res != null) {
2549
        while (res != null) {
2548
            prev = res;
2550
            prev = res;
2549
            res = getElement(res.getTable()).fetchPrivateParent(res, true, archiveMode);
2551
            res = getElement(res.getTable()).fetchPrivateParent(res, true, archiveMode);
2550
        }
2552
        }
2551
        return prev;
2553
        return prev;
2552
    }
2554
    }
2553
 
2555
 
2554
    Map<SQLField, List<SQLRow>> getNonChildrenReferents(SQLRow row) {
2556
    Map<SQLField, List<SQLRow>> getNonChildrenReferents(SQLRow row) {
2555
        check(row);
2557
        check(row);
2556
        final Map<SQLField, List<SQLRow>> mm = new HashMap<SQLField, List<SQLRow>>();
2558
        final Map<SQLField, List<SQLRow>> mm = new HashMap<SQLField, List<SQLRow>>();
2557
        final Set<SQLField> nonChildren = new HashSet<SQLField>(row.getTable().getDBSystemRoot().getGraph().getReferentKeys(row.getTable()));
2559
        final Set<SQLField> nonChildren = new HashSet<SQLField>(row.getTable().getDBSystemRoot().getGraph().getReferentKeys(row.getTable()));
2558
        nonChildren.removeAll(this.getChildrenReferentFields());
2560
        nonChildren.removeAll(this.getChildrenReferentFields());
2559
        for (final SQLField refField : nonChildren) {
2561
        for (final SQLField refField : nonChildren) {
2560
            // eg CONTACT.ID_SITE => [CONTACT[12], CONTACT[13]]
2562
            // eg CONTACT.ID_SITE => [CONTACT[12], CONTACT[13]]
2561
            mm.put(refField, row.getReferentRows(refField));
2563
            mm.put(refField, row.getReferentRows(refField));
2562
        }
2564
        }
2563
        return mm;
2565
        return mm;
2564
    }
2566
    }
2565
 
2567
 
2566
    /**
2568
    /**
2567
     * Fetch the whole {@link #createGraph() graph} for the passed ID and wrap it in the model
2569
     * Fetch the whole {@link #createGraph() graph} for the passed ID and wrap it in the model
2568
     * object.
2570
     * object.
2569
     * 
2571
     * 
2570
     * @param id the ID to fetch.
2572
     * @param id the ID to fetch.
2571
     * @return a model object.
2573
     * @return a model object.
2572
     */
2574
     */
2573
    public final Object fetchModelObject(Number id) {
2575
    public final Object fetchModelObject(Number id) {
2574
        final SQLRowValues r = createModelFetcher().fetchOne(id, true);
2576
        final SQLRowValues r = createModelFetcher().fetchOne(id, true);
2575
        if (r == null)
2577
        if (r == null)
2576
            throw new IllegalStateException("Missing " + id + " for " + this);
2578
            throw new IllegalStateException("Missing " + id + " for " + this);
2577
        return this.getModelObject(r);
2579
        return this.getModelObject(r);
2578
    }
2580
    }
2579
 
2581
 
2580
    protected final SQLRowValuesListFetcher createModelFetcher() {
2582
    protected final SQLRowValuesListFetcher createModelFetcher() {
2581
        return SQLRowValuesListFetcher.create(this.createGraph());
2583
        return SQLRowValuesListFetcher.create(this.createGraph());
2582
    }
2584
    }
2583
 
2585
 
2584
    public final Map<Number, Object> fetchModelObjects(Collection<? extends Number> ids) {
2586
    public final Map<Number, Object> fetchModelObjects(Collection<? extends Number> ids) {
2585
        return this.fetchModelObjects(ids, Object.class);
2587
        return this.fetchModelObjects(ids, Object.class);
2586
    }
2588
    }
2587
 
2589
 
2588
    public final <T> Map<Number, T> fetchModelObjects(Collection<? extends Number> ids, final Class<T> clazz) {
2590
    public final <T> Map<Number, T> fetchModelObjects(Collection<? extends Number> ids, final Class<T> clazz) {
2589
        final List<SQLRowValues> rows = createModelFetcher().fetch(new Where(getTable().getKey(), ids), true);
2591
        final List<SQLRowValues> rows = createModelFetcher().fetch(new Where(getTable().getKey(), ids), true);
2590
        final Map<Number, T> res = new LinkedHashMap<Number, T>();
2592
        final Map<Number, T> res = new LinkedHashMap<Number, T>();
2591
        for (final SQLRowValues r : rows) {
2593
        for (final SQLRowValues r : rows) {
2592
            res.put(r.getIDNumber(), clazz.cast(this.getModelObject(r)));
2594
            res.put(r.getIDNumber(), clazz.cast(this.getModelObject(r)));
2593
        }
2595
        }
2594
        return res;
2596
        return res;
2595
    }
2597
    }
2596
 
2598
 
2597
    /**
2599
    /**
2598
     * Returns a java object modeling the passed row. No access to the DB will be performed.
2600
     * Returns a java object modeling the passed row. No access to the DB will be performed.
2599
     * 
2601
     * 
2600
     * @param row the row to model.
2602
     * @param row the row to model.
2601
     * @return an instance modeling the passed row or <code>null</code> if there's no class to model
2603
     * @return an instance modeling the passed row or <code>null</code> if there's no class to model
2602
     *         this table.
2604
     *         this table.
2603
     * @see #canCreateModelObject()
2605
     * @see #canCreateModelObject()
2604
     */
2606
     */
2605
    public final Object getModelObject(SQLRowAccessor row) {
2607
    public final Object getModelObject(SQLRowAccessor row) {
2606
        check(row);
2608
        check(row);
2607
        if (!canCreateModelObject())
2609
        if (!canCreateModelObject())
2608
            return null;
2610
            return null;
2609
 
2611
 
2610
        final Object res;
2612
        final Object res;
2611
        // only SQLRow are cached (otherwise need another cache to not return model objects with
2613
        // only SQLRow are cached (otherwise need another cache to not return model objects with
2612
        // SQLRowValues if passed SQLRow and vice-versa)
2614
        // SQLRowValues if passed SQLRow and vice-versa)
2613
        if (row instanceof SQLRow) {
2615
        if (row instanceof SQLRow) {
2614
            final CacheResult<Object> cached = this.getModelCache().check(row.asRow(), Collections.singleton(row));
2616
            final CacheResult<Object> cached = this.getModelCache().check(row.asRow(), Collections.singleton(row));
2615
            if (cached.getState() == CacheResult.State.INTERRUPTED)
2617
            if (cached.getState() == CacheResult.State.INTERRUPTED)
2616
                throw new RTInterruptedException("interrupted while waiting for the cache");
2618
                throw new RTInterruptedException("interrupted while waiting for the cache");
2617
            else if (cached.getState() == CacheResult.State.VALID)
2619
            else if (cached.getState() == CacheResult.State.VALID)
2618
                return cached.getRes();
2620
                return cached.getRes();
2619
 
2621
 
2620
            try {
2622
            try {
2621
                res = this.createModelObject(row);
2623
                res = this.createModelObject(row);
2622
                this.getModelCache().put(cached, res);
2624
                this.getModelCache().put(cached, res);
2623
            } catch (RuntimeException exn) {
2625
            } catch (RuntimeException exn) {
2624
                this.getModelCache().removeRunning(cached);
2626
                this.getModelCache().removeRunning(cached);
2625
                throw exn;
2627
                throw exn;
2626
            }
2628
            }
2627
        } else
2629
        } else
2628
            res = this.createModelObject(row);
2630
            res = this.createModelObject(row);
2629
 
2631
 
2630
        return res;
2632
        return res;
2631
    }
2633
    }
2632
 
2634
 
2633
    protected final Object createModelObject(SQLRowAccessor row) {
2635
    protected final Object createModelObject(SQLRowAccessor row) {
2634
        return this.getModelClass().cast(this._createModelObject(row));
2636
        return this.getModelClass().cast(this._createModelObject(row));
2635
    }
2637
    }
2636
 
2638
 
2637
    protected Object _createModelObject(SQLRowAccessor row) {
2639
    protected Object _createModelObject(SQLRowAccessor row) {
2638
        Constructor<?> ctor = ReflectUtils.getMatchingConstructor(this.getModelClass(), row.getClass(), this.getClass());
2640
        Constructor<?> ctor = ReflectUtils.getMatchingConstructor(this.getModelClass(), row.getClass(), this.getClass());
2639
        if (ctor == null) {
2641
        if (ctor == null) {
2640
            // deprecated constructor
2642
            // deprecated constructor
2641
            try {
2643
            try {
2642
                ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class });
2644
                ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class });
2643
            } catch (NoSuchMethodException e) {
2645
            } catch (NoSuchMethodException e) {
2644
                throw new IllegalStateException(this + " found no public suitable constructor in " + this.getModelClass());
2646
                throw new IllegalStateException(this + " found no public suitable constructor in " + this.getModelClass());
2645
            }
2647
            }
2646
        }
2648
        }
2647
        try {
2649
        try {
2648
            return ctor.getParameterTypes().length == 2 ? ctor.newInstance(new Object[] { row, this }) : ctor.newInstance(new Object[] { row });
2650
            return ctor.getParameterTypes().length == 2 ? ctor.newInstance(new Object[] { row, this }) : ctor.newInstance(new Object[] { row });
2649
        } catch (Exception e) {
2651
        } catch (Exception e) {
2650
            throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e);
2652
            throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e);
2651
        }
2653
        }
2652
    }
2654
    }
2653
 
2655
 
2654
    public boolean canCreateModelObject() {
2656
    public boolean canCreateModelObject() {
2655
        return this.getModelClass() != null;
2657
        return this.getModelClass() != null;
2656
    }
2658
    }
2657
 
2659
 
2658
    protected Class<?> getModelClass() {
2660
    protected Class<?> getModelClass() {
2659
        return null;
2661
        return null;
2660
    }
2662
    }
2661
 
2663
 
2662
    // *** equals
2664
    // *** equals
2663
 
2665
 
2664
    public static final class EqualOptionBuilder {
2666
    public static final class EqualOptionBuilder {
2665
 
2667
 
2666
        private boolean ignoreNotDeepCopied, testNonShared, testParent, testMetadata;
2668
        private boolean ignoreNotDeepCopied, testNonShared, testParent, testMetadata;
2667
 
2669
 
2668
        public EqualOptionBuilder() {
2670
        public EqualOptionBuilder() {
2669
            this.ignoreNotDeepCopied = false;
2671
            this.ignoreNotDeepCopied = false;
2670
            this.testNonShared = false;
2672
            this.testNonShared = false;
2671
            this.testParent = false;
2673
            this.testParent = false;
2672
            this.testMetadata = false;
2674
            this.testMetadata = false;
2673
        }
2675
        }
2674
 
2676
 
2675
        public boolean isIgnoreNotDeepCopied() {
2677
        public boolean isIgnoreNotDeepCopied() {
2676
            return this.ignoreNotDeepCopied;
2678
            return this.ignoreNotDeepCopied;
2677
        }
2679
        }
2678
 
2680
 
2679
        public EqualOptionBuilder setIgnoreNotDeepCopied(boolean ignoreNotDeepCopied) {
2681
        public EqualOptionBuilder setIgnoreNotDeepCopied(boolean ignoreNotDeepCopied) {
2680
            this.ignoreNotDeepCopied = ignoreNotDeepCopied;
2682
            this.ignoreNotDeepCopied = ignoreNotDeepCopied;
2681
            return this;
2683
            return this;
2682
        }
2684
        }
2683
 
2685
 
2684
        public boolean isNonSharedTested() {
2686
        public boolean isNonSharedTested() {
2685
            return this.testNonShared;
2687
            return this.testNonShared;
2686
        }
2688
        }
2687
 
2689
 
2688
        public EqualOptionBuilder setNonSharedTested(boolean testNonShared) {
2690
        public EqualOptionBuilder setNonSharedTested(boolean testNonShared) {
2689
            this.testNonShared = testNonShared;
2691
            this.testNonShared = testNonShared;
2690
            return this;
2692
            return this;
2691
        }
2693
        }
2692
 
2694
 
2693
        public boolean isParentTested() {
2695
        public boolean isParentTested() {
2694
            return this.testParent;
2696
            return this.testParent;
2695
        }
2697
        }
2696
 
2698
 
2697
        public EqualOptionBuilder setParentTested(boolean testParent) {
2699
        public EqualOptionBuilder setParentTested(boolean testParent) {
2698
            this.testParent = testParent;
2700
            this.testParent = testParent;
2699
            return this;
2701
            return this;
2700
        }
2702
        }
2701
 
2703
 
2702
        public boolean isMetadataTested() {
2704
        public boolean isMetadataTested() {
2703
            return this.testMetadata;
2705
            return this.testMetadata;
2704
        }
2706
        }
2705
 
2707
 
2706
        public EqualOptionBuilder setMetadataTested(boolean testMetadata) {
2708
        public EqualOptionBuilder setMetadataTested(boolean testMetadata) {
2707
            this.testMetadata = testMetadata;
2709
            this.testMetadata = testMetadata;
2708
            return this;
2710
            return this;
2709
        }
2711
        }
2710
 
2712
 
2711
        public EqualOption build() {
2713
        public EqualOption build() {
2712
            return new EqualOption(this.ignoreNotDeepCopied, this.testNonShared, this.testParent, this.testMetadata);
2714
            return new EqualOption(this.ignoreNotDeepCopied, this.testNonShared, this.testParent, this.testMetadata);
2713
        }
2715
        }
2714
    }
2716
    }
2715
 
2717
 
2716
    @Immutable
2718
    @Immutable
2717
    public static final class EqualOption {
2719
    public static final class EqualOption {
2718
 
2720
 
2719
        static private final VirtualFields EQUALS_FIELDS = VirtualFields.CONTENT.union(VirtualFields.ARCHIVE);
2721
        static private final VirtualFields EQUALS_FIELDS = VirtualFields.CONTENT.union(VirtualFields.ARCHIVE);
2720
        static private final VirtualFields EQUALS_WITH_MD_FIELDS = EQUALS_FIELDS.union(VirtualFields.METADATA);
2722
        static private final VirtualFields EQUALS_WITH_MD_FIELDS = EQUALS_FIELDS.union(VirtualFields.METADATA);
2721
 
2723
 
2722
        public static final EqualOption ALL = new EqualOption(false, true, true, true);
2724
        public static final EqualOption ALL = new EqualOption(false, true, true, true);
2723
        public static final EqualOption ALL_BUT_IGNORE_NOT_DEEP_COPIED = ALL.createBuilder().setIgnoreNotDeepCopied(true).build();
2725
        public static final EqualOption ALL_BUT_IGNORE_NOT_DEEP_COPIED = ALL.createBuilder().setIgnoreNotDeepCopied(true).build();
2724
 
2726
 
2725
        public static final EqualOption IGNORE_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(true).build();
2727
        public static final EqualOption IGNORE_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(true).build();
2726
        public static final EqualOption TEST_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(false).build();
2728
        public static final EqualOption TEST_NOT_DEEP_COPIED = new EqualOptionBuilder().setIgnoreNotDeepCopied(false).build();
2727
 
2729
 
2728
        static final EqualOption fromIgnoreNotDeepCopied(final boolean ignoreNotDeepCopied) {
2730
        static final EqualOption fromIgnoreNotDeepCopied(final boolean ignoreNotDeepCopied) {
2729
            return ignoreNotDeepCopied ? IGNORE_NOT_DEEP_COPIED : TEST_NOT_DEEP_COPIED;
2731
            return ignoreNotDeepCopied ? IGNORE_NOT_DEEP_COPIED : TEST_NOT_DEEP_COPIED;
2730
        }
2732
        }
2731
 
2733
 
2732
        private final boolean ignoreNotDeepCopied, testNonShared, testParent;
2734
        private final boolean ignoreNotDeepCopied, testNonShared, testParent;
2733
        private final VirtualFields fields;
2735
        private final VirtualFields fields;
2734
 
2736
 
2735
        protected EqualOption(final boolean ignoreNotDeepCopied, final boolean testNonShared, final boolean testParent, final boolean testMetadata) {
2737
        protected EqualOption(final boolean ignoreNotDeepCopied, final boolean testNonShared, final boolean testParent, final boolean testMetadata) {
2736
            this.ignoreNotDeepCopied = ignoreNotDeepCopied;
2738
            this.ignoreNotDeepCopied = ignoreNotDeepCopied;
2737
            this.testNonShared = testNonShared;
2739
            this.testNonShared = testNonShared;
2738
            this.testParent = testParent;
2740
            this.testParent = testParent;
2739
            this.fields = testMetadata ? EQUALS_WITH_MD_FIELDS : EQUALS_FIELDS;
2741
            this.fields = testMetadata ? EQUALS_WITH_MD_FIELDS : EQUALS_FIELDS;
2740
        }
2742
        }
2741
 
2743
 
2742
        public boolean isIgnoreNotDeepCopied() {
2744
        public boolean isIgnoreNotDeepCopied() {
2743
            return this.ignoreNotDeepCopied;
2745
            return this.ignoreNotDeepCopied;
2744
        }
2746
        }
2745
 
2747
 
2746
        public boolean isNonSharedTested() {
2748
        public boolean isNonSharedTested() {
2747
            return this.testNonShared;
2749
            return this.testNonShared;
2748
        }
2750
        }
2749
 
2751
 
2750
        public boolean isParentTested() {
2752
        public boolean isParentTested() {
2751
            return this.testParent;
2753
            return this.testParent;
2752
        }
2754
        }
2753
 
2755
 
2754
        public EqualOptionBuilder createBuilder() {
2756
        public EqualOptionBuilder createBuilder() {
2755
            return new EqualOptionBuilder().setIgnoreNotDeepCopied(isIgnoreNotDeepCopied()).setNonSharedTested(isNonSharedTested()).setParentTested(this.isParentTested())
2757
            return new EqualOptionBuilder().setIgnoreNotDeepCopied(isIgnoreNotDeepCopied()).setNonSharedTested(isNonSharedTested()).setParentTested(this.isParentTested())
2756
                    .setMetadataTested(this.fields == EQUALS_WITH_MD_FIELDS);
2758
                    .setMetadataTested(this.fields == EQUALS_WITH_MD_FIELDS);
2757
        }
2759
        }
2758
    }
2760
    }
2759
 
2761
 
2760
    public boolean equals(SQLRow row, SQLRow row2) {
2762
    public boolean equals(SQLRow row, SQLRow row2) {
2761
        return this.equals(row, row2, false);
2763
        return this.equals(row, row2, false);
2762
    }
2764
    }
2763
 
2765
 
2764
    /**
2766
    /**
2765
     * Compare local values (excluding order and obviously primary key). This method doesn't cross
2767
     * Compare local values (excluding order and obviously primary key). This method doesn't cross
2766
     * links except for privates but it does compare the value of shared normal links. This method
2768
     * links except for privates but it does compare the value of shared normal links. This method
2767
     * always uses the DB.
2769
     * always uses the DB.
2768
     * 
2770
     * 
2769
     * @param row the first row.
2771
     * @param row the first row.
2770
     * @param row2 the second row.
2772
     * @param row2 the second row.
2771
     * @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
2773
     * @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
2772
     *        {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
2774
     *        {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
2773
     *        {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
2775
     *        {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
2774
     * @return <code>true</code> if the two rows are equal.
2776
     * @return <code>true</code> if the two rows are equal.
2775
     * @see #equals(SQLRowValues, SQLRowValues, boolean)
2777
     * @see #equals(SQLRowValues, SQLRowValues, boolean)
2776
     */
2778
     */
2777
    public boolean equals(SQLRow row, SQLRow row2, boolean ignoreNotDeepCopied) {
2779
    public boolean equals(SQLRow row, SQLRow row2, boolean ignoreNotDeepCopied) {
2778
        return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
2780
        return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
2779
    }
2781
    }
2780
 
2782
 
2781
    public boolean equals(SQLRow row, SQLRow row2, final EqualOption option) {
2783
    public boolean equals(SQLRow row, SQLRow row2, final EqualOption option) {
2782
        return this.diff(row, row2, option).get0();
2784
        return this.diff(row, row2, option).get0();
2783
    }
2785
    }
2784
 
2786
 
2785
    private static final Tuple2<Boolean, DiffResult> TRUE_NULL = new Tuple2<Boolean, DiffResult>(true, null);
2787
    private static final Tuple2<Boolean, DiffResult> TRUE_NULL = new Tuple2<Boolean, DiffResult>(true, null);
2786
    private static final Tuple2<Boolean, DiffResult> FALSE_NULL = new Tuple2<Boolean, DiffResult>(false, null);
2788
    private static final Tuple2<Boolean, DiffResult> FALSE_NULL = new Tuple2<Boolean, DiffResult>(false, null);
2787
 
2789
 
2788
    // Boolean is never null, DiffResult is null if difference is trivial
2790
    // Boolean is never null, DiffResult is null if difference is trivial
2789
    Tuple2<Boolean, DiffResult> diff(SQLRow row, SQLRow row2, final EqualOption option) {
2791
    Tuple2<Boolean, DiffResult> diff(SQLRow row, SQLRow row2, final EqualOption option) {
2790
        check(row);
2792
        check(row);
2791
        if (!row2.getTable().equals(this.getTable()))
2793
        if (!row2.getTable().equals(this.getTable()))
2792
            return FALSE_NULL;
2794
            return FALSE_NULL;
2793
        if (row.equals(row2))
2795
        if (row.equals(row2))
2794
            return TRUE_NULL;
2796
            return TRUE_NULL;
2795
        // the same table but not the same id
2797
        // the same table but not the same id
2796
 
2798
 
2797
        final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(getPrivateGraphForEquals(option));
2799
        final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(getPrivateGraphForEquals(option));
2798
        final List<SQLRowValues> fetched = fetcher.fetch(new Where(this.getTable().getKey(), Arrays.asList(row.getIDNumber(), row2.getIDNumber())));
2800
        final List<SQLRowValues> fetched = fetcher.fetch(new Where(this.getTable().getKey(), Arrays.asList(row.getIDNumber(), row2.getIDNumber())));
2799
        if (fetched.size() > 2)
2801
        if (fetched.size() > 2)
2800
            throw new IllegalStateException("More than 2 rows for " + row + " and " + row2);
2802
            throw new IllegalStateException("More than 2 rows for " + row + " and " + row2);
2801
        else if (fetched.size() < 2)
2803
        else if (fetched.size() < 2)
2802
            // at least one is inexistent or archived
2804
            // at least one is inexistent or archived
2803
            return FALSE_NULL;
2805
            return FALSE_NULL;
2804
 
2806
 
2805
        final DiffResult res = equalsPruned(fetched.get(0), fetched.get(1));
2807
        final DiffResult res = equalsPruned(fetched.get(0), fetched.get(1));
2806
        return Tuple2.create(res.isEqual(), res);
2808
        return Tuple2.create(res.isEqual(), res);
2807
    }
2809
    }
2808
 
2810
 
2809
    /**
2811
    /**
2810
     * Compare local values (excluding order and obviously primary key). This method doesn't cross
2812
     * Compare local values (excluding order and obviously primary key). This method doesn't cross
2811
     * links except for privates but it does compare the value of shared normal links. This method
2813
     * links except for privates but it does compare the value of shared normal links. This method
2812
     * never uses the DB but does {@link SQLRowValuesCluster#prune(SQLRowValues, SQLRowValues)
2814
     * never uses the DB but does {@link SQLRowValuesCluster#prune(SQLRowValues, SQLRowValues)
2813
     * prune} the parameters before comparing them.
2815
     * prune} the parameters before comparing them.
2814
     * 
2816
     * 
2815
     * @param row the first row.
2817
     * @param row the first row.
2816
     * @param row2 the second row.
2818
     * @param row2 the second row.
2817
     * @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
2819
     * @param ignoreNotDeepCopied if <code>true</code> ignores the rows that are
2818
     *        {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
2820
     *        {@link #dontDeepCopy() not to be copied}. See also the <code>full</code> parameter of
2819
     *        {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
2821
     *        {@link #createCopy(SQLRowAccessor, boolean, SQLRowAccessor)}.
2820
     * @return <code>true</code> if the two rows are equal.
2822
     * @return <code>true</code> if the two rows are equal.
2821
     * @see #equals(SQLRow, SQLRow, boolean)
2823
     * @see #equals(SQLRow, SQLRow, boolean)
2822
     */
2824
     */
2823
    public boolean equals(SQLRowValues row, SQLRowValues row2, boolean ignoreNotDeepCopied) {
2825
    public boolean equals(SQLRowValues row, SQLRowValues row2, boolean ignoreNotDeepCopied) {
2824
        return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
2826
        return this.equals(row, row2, EqualOption.fromIgnoreNotDeepCopied(ignoreNotDeepCopied));
2825
    }
2827
    }
2826
 
2828
 
2827
    public boolean equals(SQLRowValues row, SQLRowValues row2, final EqualOption option) {
2829
    public boolean equals(SQLRowValues row, SQLRowValues row2, final EqualOption option) {
2828
        check(row);
2830
        check(row);
2829
        if (row == row2)
2831
        if (row == row2)
2830
            return true;
2832
            return true;
2831
        if (!row2.getTable().equals(this.getTable()))
2833
        if (!row2.getTable().equals(this.getTable()))
2832
            return false;
2834
            return false;
2833
 
2835
 
2834
        final SQLRowValues privateGraphForEquals = getPrivateGraphForEquals(option);
2836
        final SQLRowValues privateGraphForEquals = getPrivateGraphForEquals(option);
2835
        return equalsPruned(row.prune(privateGraphForEquals), row2.prune(privateGraphForEquals)).isEqual();
2837
        return equalsPruned(row.prune(privateGraphForEquals), row2.prune(privateGraphForEquals)).isEqual();
2836
    }
2838
    }
2837
 
2839
 
2838
    private final SQLRowValues getPrivateGraphForEquals(final EqualOption option) {
2840
    private final SQLRowValues getPrivateGraphForEquals(final EqualOption option) {
2839
        // don't include joins as we only add those required by "option"
2841
        // don't include joins as we only add those required by "option"
2840
        final SQLRowValues res = this.createGraph(option.fields, option.isIgnoreNotDeepCopied() ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, false);
2842
        final SQLRowValues res = this.createGraph(option.fields, option.isIgnoreNotDeepCopied() ? PrivateMode.DEEP_COPIED_PRIVATES : PrivateMode.ALL_PRIVATES, false);
2841
        for (final SQLRowValues item : new HashSet<SQLRowValues>(res.getGraph().getItems())) {
2843
        for (final SQLRowValues item : new HashSet<SQLRowValues>(res.getGraph().getItems())) {
2842
            final SQLElement elem = getElement(item.getTable());
2844
            final SQLElement elem = getElement(item.getTable());
2843
            // remove parent
2845
            // remove parent
2844
            final SQLElementLink parentLink = elem.getParentLink();
2846
            final SQLElementLink parentLink = elem.getParentLink();
2845
            setLink(item, parentLink, option.isParentTested());
2847
            setLink(item, parentLink, option.isParentTested());
2846
            // remove non shared normal links
2848
            // remove non shared normal links
2847
            // add shared normal links (if join)
2849
            // add shared normal links (if join)
2848
            for (final SQLElementLink normalLink : elem.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2850
            for (final SQLElementLink normalLink : elem.getOwnedLinks().getByType(LinkType.ASSOCIATION)) {
2849
                setLink(item, normalLink, option.isNonSharedTested() || normalLink.getOwned().isShared());
2851
                setLink(item, normalLink, option.isNonSharedTested() || normalLink.getOwned().isShared());
2850
            }
2852
            }
2851
        }
2853
        }
2852
        return res;
2854
        return res;
2853
    }
2855
    }
2854
 
2856
 
2855
    private final void setLink(final SQLRowValues item, final SQLElementLink link, final boolean shouldBeTested) {
2857
    private final void setLink(final SQLRowValues item, final SQLElementLink link, final boolean shouldBeTested) {
2856
        if (link == null)
2858
        if (link == null)
2857
            return;
2859
            return;
2858
        if (shouldBeTested) {
2860
        if (shouldBeTested) {
2859
            if (link.isJoin()) {
2861
            if (link.isJoin()) {
2860
                assert link.getPath().getStep(0).getDirection() == Direction.REFERENT;
2862
                assert link.getPath().getStep(0).getDirection() == Direction.REFERENT;
2861
                item.assurePath(link.getPath().minusLast()).fillWith(null, false);
2863
                item.assurePath(link.getPath().minusLast()).fillWith(null, false);
2862
            }
2864
            }
2863
        } else {
2865
        } else {
2864
            if (!link.isJoin()) {
2866
            if (!link.isJoin()) {
2865
                item.removeForeignKey(link.getSingleLink());
2867
                item.removeForeignKey(link.getSingleLink());
2866
            }
2868
            }
2867
        }
2869
        }
2868
    }
2870
    }
2869
 
2871
 
2870
    static private DiffResult equalsPruned(SQLRowValues row, SQLRowValues row2) {
2872
    static private DiffResult equalsPruned(SQLRowValues row, SQLRowValues row2) {
2871
        // neither use order nor PK (don't just remove PK since we need them for
2873
        // neither use order nor PK (don't just remove PK since we need them for
2872
        // DiffResult.fillRowMap())
2874
        // DiffResult.fillRowMap())
2873
        return row.getGraph().getFirstDifference(row, row2, false, false, false);
2875
        return row.getGraph().getFirstDifference(row, row2, false, false, false);
2874
    }
2876
    }
2875
 
2877
 
2876
    public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException {
2878
    public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException {
2877
        return this.equalsRecursive(row, row2, EqualOption.ALL);
2879
        return this.equalsRecursive(row, row2, EqualOption.ALL);
2878
    }
2880
    }
2879
 
2881
 
2880
    /**
2882
    /**
2881
     * Test those rows and all their descendants.
2883
     * Test those rows and all their descendants.
2882
     * 
2884
     * 
2883
     * @param row first row.
2885
     * @param row first row.
2884
     * @param row2 second row.
2886
     * @param row2 second row.
2885
     * @param option how to compare each descendant, note that #{@link EqualOption#isParentTested()}
2887
     * @param option how to compare each descendant, note that #{@link EqualOption#isParentTested()}
2886
     *        is only meaningful for the passed (root) rows, since descendants are found through
2888
     *        is only meaningful for the passed (root) rows, since descendants are found through
2887
     *        their parents (i.e. they always have equal parents).
2889
     *        their parents (i.e. they always have equal parents).
2888
     * @return true if both trees are equal according to <code>option</code>.
2890
     * @return true if both trees are equal according to <code>option</code>.
2889
     * @throws SQLException if an error occurs.
2891
     * @throws SQLException if an error occurs.
2890
     */
2892
     */
2891
    public boolean equalsRecursive(SQLRow row, SQLRow row2, EqualOption option) throws SQLException {
2893
    public boolean equalsRecursive(SQLRow row, SQLRow row2, EqualOption option) throws SQLException {
2892
        // if (!equals(row, row2))
2894
        // if (!equals(row, row2))
2893
        // return false;
2895
        // return false;
2894
        return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2), option);
2896
        return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2), option);
2895
    }
2897
    }
2896
 
2898
 
2897
    // no need for equals()/hashCode() since there's only one SQLElement per table and directory
2899
    // no need for equals()/hashCode() since there's only one SQLElement per table and directory
2898
 
2900
 
2899
    @Override
2901
    @Override
2900
    public String toString() {
2902
    public String toString() {
2901
        return this.getClass().getName() + " " + this.getTable().getSQLName();
2903
        return this.getClass().getName() + " " + this.getTable().getSQLName();
2902
    }
2904
    }
2903
 
2905
 
2904
    // *** gui
2906
    // *** gui
2905
 
2907
 
2906
    public final void addComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
2908
    public final void addComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
2907
        if (t == null)
2909
        if (t == null)
2908
            throw new NullPointerException();
2910
            throw new NullPointerException();
2909
        this.components.add(id, t);
2911
        this.components.add(id, t);
2910
    }
2912
    }
2911
 
2913
 
2912
    public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
2914
    public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
2913
        if (t == null)
2915
        if (t == null)
2914
            throw new NullPointerException();
2916
            throw new NullPointerException();
2915
        this.components.removeOne(id, t);
2917
        this.components.removeOne(id, t);
2916
    }
2918
    }
2917
 
2919
 
2918
    private final SQLComponent createComponentFromFactory(final String id, final boolean defaultItem) {
2920
    private final SQLComponent createComponentFromFactory(final String id, final boolean defaultItem) {
2919
        final String actualID = defaultItem ? DEFAULT_COMP_ID : id;
2921
        final String actualID = defaultItem ? DEFAULT_COMP_ID : id;
2920
        final Tuple2<SQLElement, String> t = Tuple2.create(this, id);
2922
        final Tuple2<SQLElement, String> t = Tuple2.create(this, id);
2921
        // start from the most recently added factory
2923
        // start from the most recently added factory
2922
        final Iterator<ITransformer<Tuple2<SQLElement, String>, SQLComponent>> iter = this.components.getNonNull(actualID).descendingIterator();
2924
        final Iterator<ITransformer<Tuple2<SQLElement, String>, SQLComponent>> iter = this.components.getNonNull(actualID).descendingIterator();
2923
        while (iter.hasNext()) {
2925
        while (iter.hasNext()) {
2924
            final SQLComponent res = iter.next().transformChecked(t);
2926
            final SQLComponent res = iter.next().transformChecked(t);
2925
            if (res != null)
2927
            if (res != null)
2926
                return res;
2928
                return res;
2927
        }
2929
        }
2928
        return null;
2930
        return null;
2929
    }
2931
    }
2930
 
2932
 
2931
    public final SQLComponent createDefaultComponent() {
2933
    public final SQLComponent createDefaultComponent() {
2932
        return this.createComponent(DEFAULT_COMP_ID);
2934
        return this.createComponent(DEFAULT_COMP_ID);
2933
    }
2935
    }
2934
 
2936
 
2935
    /**
2937
    /**
2936
     * Create the component for the passed ID. First factories for the passed ID are executed, after
2938
     * Create the component for the passed ID. First factories for the passed ID are executed, after
2937
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
2939
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
2938
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
2940
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
2939
     * 
2941
     * 
2940
     * @param id the requested ID.
2942
     * @param id the requested ID.
2941
     * @return the component, never <code>null</code>.
2943
     * @return the component, never <code>null</code>.
2942
     * @throws IllegalStateException if no component is found.
2944
     * @throws IllegalStateException if no component is found.
2943
     */
2945
     */
2944
    public final SQLComponent createComponent(final String id) throws IllegalStateException {
2946
    public final SQLComponent createComponent(final String id) throws IllegalStateException {
2945
        return this.createComponent(id, true);
2947
        return this.createComponent(id, true);
2946
    }
2948
    }
2947
 
2949
 
2948
    /**
2950
    /**
2949
     * Create the component for the passed ID. First factories for the passed ID are executed, after
2951
     * Create the component for the passed ID. First factories for the passed ID are executed, after
2950
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
2952
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
2951
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
2953
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
2952
     * 
2954
     * 
2953
     * @param id the requested ID.
2955
     * @param id the requested ID.
2954
     * @param required <code>true</code> if the result cannot be <code>null</code>.
2956
     * @param required <code>true</code> if the result cannot be <code>null</code>.
2955
     * @return the component or <code>null</code> if all factories return <code>null</code> and
2957
     * @return the component or <code>null</code> if all factories return <code>null</code> and
2956
     *         <code>required</code> is <code>false</code>.
2958
     *         <code>required</code> is <code>false</code>.
2957
     * @throws IllegalStateException if <code>required</code> and no component is found.
2959
     * @throws IllegalStateException if <code>required</code> and no component is found.
2958
     */
2960
     */
2959
    public final SQLComponent createComponent(final String id, final boolean required) throws IllegalStateException {
2961
    public final SQLComponent createComponent(final String id, final boolean required) throws IllegalStateException {
2960
        SQLComponent res = this.createComponentFromFactory(id, false);
2962
        SQLComponent res = this.createComponentFromFactory(id, false);
2961
        if (res == null) {
2963
        if (res == null) {
2962
            if (CompareUtils.equals(id, DEFAULT_COMP_ID)) {
2964
            if (CompareUtils.equals(id, DEFAULT_COMP_ID)) {
2963
                // since we don't pass id to this method, only call it for DEFAULT_ID
2965
                // since we don't pass id to this method, only call it for DEFAULT_ID
2964
                res = this.createComponent();
2966
                res = this.createComponent();
2965
            } else {
2967
            } else {
2966
                res = this.createComponentFromFactory(id, true);
2968
                res = this.createComponentFromFactory(id, true);
2967
            }
2969
            }
2968
        }
2970
        }
2969
        if (res != null)
2971
        if (res != null)
2970
            res.setCode(id);
2972
            res.setCode(id);
2971
        else if (required)
2973
        else if (required)
2972
            throw new IllegalStateException("No component for " + id);
2974
            throw new IllegalStateException("No component for " + id);
2973
        return res;
2975
        return res;
2974
    }
2976
    }
2975
 
2977
 
2976
    /**
2978
    /**
2977
     * Retourne l'interface graphique de saisie.
2979
     * Retourne l'interface graphique de saisie.
2978
     * 
2980
     * 
2979
     * @return l'interface graphique de saisie.
2981
     * @return l'interface graphique de saisie.
2980
     */
2982
     */
2981
    protected abstract SQLComponent createComponent();
2983
    protected abstract SQLComponent createComponent();
2982
 
2984
 
2983
    public final void addToMDPath(final String mdVariant) {
2985
    public final void addToMDPath(final String mdVariant) {
2984
        if (mdVariant == null)
2986
        if (mdVariant == null)
2985
            throw new NullPointerException();
2987
            throw new NullPointerException();
2986
        synchronized (this) {
2988
        synchronized (this) {
2987
            final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
2989
            final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
2988
            newL.addFirst(mdVariant);
2990
            newL.addFirst(mdVariant);
2989
            this.mdPath = Collections.unmodifiableList(newL);
2991
            this.mdPath = Collections.unmodifiableList(newL);
2990
        }
2992
        }
2991
    }
2993
    }
2992
 
2994
 
2993
    public synchronized final void removeFromMDPath(final String mdVariant) {
2995
    public synchronized final void removeFromMDPath(final String mdVariant) {
2994
        final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
2996
        final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
2995
        if (newL.remove(mdVariant))
2997
        if (newL.remove(mdVariant))
2996
            this.mdPath = Collections.unmodifiableList(newL);
2998
            this.mdPath = Collections.unmodifiableList(newL);
2997
    }
2999
    }
2998
 
3000
 
2999
    /**
3001
    /**
3000
     * The variants searched to find item metadata by
3002
     * The variants searched to find item metadata by
3001
     * {@link SQLFieldTranslator#getDescFor(SQLTable, String, String)}. This allow to configure this
3003
     * {@link SQLFieldTranslator#getDescFor(SQLTable, String, String)}. This allow to configure this
3002
     * element to choose between the simultaneously loaded metadata.
3004
     * element to choose between the simultaneously loaded metadata.
3003
     * 
3005
     * 
3004
     * @return the variants path.
3006
     * @return the variants path.
3005
     */
3007
     */
3006
    public synchronized final List<String> getMDPath() {
3008
    public synchronized final List<String> getMDPath() {
3007
        return this.mdPath;
3009
        return this.mdPath;
3008
    }
3010
    }
3009
 
3011
 
3010
    /**
3012
    /**
3011
     * Allows a module to add a view for a field to this element.
3013
     * Allows a module to add a view for a field to this element.
3012
     * 
3014
     * 
3013
     * @param field the field of the component.
3015
     * @param field the field of the component.
3014
     * @return <code>true</code> if no view existed.
3016
     * @return <code>true</code> if no view existed.
3015
     */
3017
     */
3016
    public final boolean putAdditionalField(final String field) {
3018
    public final boolean putAdditionalField(final String field) {
3017
        return this.putAdditionalField(field, (JComponent) null);
3019
        return this.putAdditionalField(field, null);
3018
    }
3020
    }
3019
 
3021
 
3020
    public final boolean putAdditionalField(final String field, final JTextComponent comp) {
3022
    public final boolean putAdditionalTextField(final String field, final Supplier<? extends JTextComponent> comp) {
3021
        return this.putAdditionalField(field, (JComponent) comp);
3023
        return this.putAdditionalField(field, comp);
3022
    }
3024
    }
3023
 
3025
 
3024
    public final boolean putAdditionalField(final String field, final SQLTextCombo comp) {
3026
    public final boolean putAdditionalTextCombo(final String field, final Supplier<? extends SQLTextCombo> comp) {
-
 
3027
        return this.putAdditionalField(field, comp);
-
 
3028
    }
-
 
3029
 
-
 
3030
    public final boolean putAdditionalCombo(final String field, final Supplier<? extends SQLRequestComboBox> comp) {
3025
        return this.putAdditionalField(field, (JComponent) comp);
3031
        return this.putAdditionalField(field, comp);
3026
    }
3032
    }
3027
 
3033
 
3028
    // private as only a few JComponent are OK
3034
    // private as only a few JComponent are OK
3029
    private final boolean putAdditionalField(final String field, final JComponent comp) {
3035
    private final boolean putAdditionalField(final String field, final Supplier<? extends JComponent> comp) {
3030
        if (this.additionalFields.containsKey(field)) {
3036
        if (this.additionalFields.containsKey(field)) {
3031
            return false;
3037
            return false;
3032
        } else {
3038
        } else {
3033
            this.additionalFields.put(field, comp);
3039
            this.additionalFields.put(field, comp);
3034
            return true;
3040
            return true;
3035
        }
3041
        }
3036
    }
3042
    }
3037
 
3043
 
3038
    public final Map<String, JComponent> getAdditionalFields() {
3044
    public final Map<String, Supplier<? extends JComponent>> getAdditionalFields() {
3039
        return Collections.unmodifiableMap(this.additionalFields);
3045
        return Collections.unmodifiableMap(this.additionalFields);
3040
    }
3046
    }
3041
 
3047
 
3042
    public final void removeAdditionalField(final String field) {
3048
    public final void removeAdditionalField(final String field) {
3043
        this.additionalFields.remove(field);
3049
        this.additionalFields.remove(field);
3044
    }
3050
    }
3045
 
3051
 
3046
    public final boolean askArchive(final Component comp, final Number ids) {
3052
    public final boolean askArchive(final Component comp, final Number ids) {
3047
        return Value.hasValue(this.askArchive(comp, Collections.singleton(ids)));
3053
        return Value.hasValue(this.askArchive(comp, Collections.singleton(ids)));
3048
    }
3054
    }
3049
 
3055
 
3050
    /**
3056
    /**
3051
     * Ask to the user before archiving.
3057
     * Ask to the user before archiving.
3052
     * 
3058
     * 
3053
     * @param comp the parent component.
3059
     * @param comp the parent component.
3054
     * @param ids which rows to archive.
3060
     * @param ids which rows to archive.
3055
     * @return <code>null</code> if there was an error (already presented to the user),
3061
     * @return <code>null</code> if there was an error (already presented to the user),
3056
     *         {@link Value#hasValue() a value} if the user agreed, none if the user refused.
3062
     *         {@link Value#hasValue() a value} if the user agreed, none if the user refused.
3057
     * @deprecated this methods mixes DB and UI access.
3063
     * @deprecated this methods mixes DB and UI access.
3058
     */
3064
     */
3059
    public final Value<TreesOfSQLRows> askArchive(final Component comp, final Collection<? extends Number> ids) {
3065
    public final Value<TreesOfSQLRows> askArchive(final Component comp, final Collection<? extends Number> ids) {
3060
        final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
3066
        final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
3061
        try {
3067
        try {
3062
            trees.fetch(LockStrength.NONE);
3068
            trees.fetch(LockStrength.NONE);
3063
            final Boolean agreed = this.ask(comp, trees);
3069
            final Boolean agreed = this.ask(comp, trees);
3064
            if (agreed == null) {
3070
            if (agreed == null) {
3065
                return null;
3071
                return null;
3066
            } else if (agreed) {
3072
            } else if (agreed) {
3067
                this.archive(trees, true);
3073
                this.archive(trees, true);
3068
                return Value.getSome(trees);
3074
                return Value.getSome(trees);
3069
            } else {
3075
            } else {
3070
                return Value.getNone();
3076
                return Value.getNone();
3071
            }
3077
            }
3072
        } catch (SQLException e) {
3078
        } catch (SQLException e) {
3073
            ExceptionHandler.handle(comp, TM.tr("sqlElement.archiveError", this, ids), e);
3079
            ExceptionHandler.handle(comp, TM.tr("sqlElement.archiveError", this, ids), e);
3074
            return null;
3080
            return null;
3075
        }
3081
        }
3076
    }
3082
    }
3077
 
3083
 
3078
    /**
3084
    /**
3079
     * Ask the user about rows to archive.
3085
     * Ask the user about rows to archive.
3080
     * 
3086
     * 
3081
     * @param comp the parent component.
3087
     * @param comp the parent component.
3082
     * @param trees which rows to archive.
3088
     * @param trees which rows to archive.
3083
     * @return <code>null</code> if there was an error (already presented to the user),
3089
     * @return <code>null</code> if there was an error (already presented to the user),
3084
     *         <code>true</code> if the user agreed, <code>false</code> if the user refused.
3090
     *         <code>true</code> if the user agreed, <code>false</code> if the user refused.
3085
     */
3091
     */
3086
    public Boolean ask(final Component comp, final TreesOfSQLRows trees) {
3092
    public Boolean ask(final Component comp, final TreesOfSQLRows trees) {
3087
        boolean shouldArchive = false;
3093
        boolean shouldArchive = false;
3088
        if (!trees.isFetched())
3094
        if (!trees.isFetched())
3089
            throw new IllegalStateException("Trees not yet fetched");
3095
            throw new IllegalStateException("Trees not yet fetched");
3090
        try {
3096
        try {
3091
            final int rowCount = trees.getTrees().size();
3097
            final int rowCount = trees.getTrees().size();
3092
            if (rowCount == 0)
3098
            if (rowCount == 0)
3093
                return true;
3099
                return true;
3094
            // only check rights if there's actually some rows to delete
3100
            // only check rights if there's actually some rows to delete
3095
            if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
3101
            if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
3096
                throw new SQLException("forbidden");
3102
                throw new SQLException("forbidden");
3097
            // only display main rows since the user might not be aware of the private ones (the UI
3103
            // only display main rows since the user might not be aware of the private ones (the UI
3098
            // might hide the fact that one panel is in fact multiple rows)
3104
            // might hide the fact that one panel is in fact multiple rows)
3099
            final Map<SQLTable, List<SQLRowAccessor>> descs = trees.getDescendantsByTable();
3105
            final Map<SQLTable, List<SQLRowAccessor>> descs = trees.getDescendantsByTable();
3100
            final SortedMap<LinkToCut, Integer> externRefs = trees.getExternReferences().countByLink();
3106
            final SortedMap<LinkToCut, Integer> externRefs = trees.getExternReferences().countByLink();
3101
            final String confirmDelete = getTM().trA("sqlElement.confirmDelete");
3107
            final String confirmDelete = getTM().trA("sqlElement.confirmDelete");
3102
            final Map<String, Object> map = new HashMap<String, Object>();
3108
            final Map<String, Object> map = new HashMap<String, Object>();
3103
            map.put("rowCount", rowCount);
3109
            map.put("rowCount", rowCount);
3104
            final int descsSize = descs.size();
3110
            final int descsSize = descs.size();
3105
            final int externsSize = externRefs.size();
3111
            final int externsSize = externRefs.size();
3106
            if (descsSize + externsSize > 0) {
3112
            if (descsSize + externsSize > 0) {
3107
                final String descsS = descsSize > 0 ? toString(descs) : null;
3113
                final String descsS = descsSize > 0 ? toString(descs) : null;
3108
                final String externsS = externsSize > 0 ? toStringExtern(externRefs) : null;
3114
                final String externsS = externsSize > 0 ? toStringExtern(externRefs) : null;
3109
                map.put("descsSize", descsSize);
3115
                map.put("descsSize", descsSize);
3110
                map.put("descs", descsS);
3116
                map.put("descs", descsS);
3111
                map.put("externsSize", externsSize);
3117
                map.put("externsSize", externsSize);
3112
                map.put("externs", externsS);
3118
                map.put("externs", externsS);
3113
                map.put("times", "once");
3119
                map.put("times", "once");
3114
                int i = askSerious(comp, getTM().trM("sqlElement.deleteRef.details", map) + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
3120
                int i = askSerious(comp, getTM().trM("sqlElement.deleteRef.details", map) + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
3115
                if (i == JOptionPane.YES_OPTION) {
3121
                if (i == JOptionPane.YES_OPTION) {
3116
                    map.put("times", "twice");
3122
                    map.put("times", "twice");
3117
                    final String msg = externsSize > 0 ? getTM().trM("sqlElement.deleteRef.details2", map) : "";
3123
                    final String msg = externsSize > 0 ? getTM().trM("sqlElement.deleteRef.details2", map) : "";
3118
                    i = askSerious(comp, msg + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
3124
                    i = askSerious(comp, msg + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
3119
                    if (i == JOptionPane.YES_OPTION) {
3125
                    if (i == JOptionPane.YES_OPTION) {
3120
                        shouldArchive = true;
3126
                        shouldArchive = true;
3121
                    } else {
3127
                    } else {
3122
                        JOptionPane.showMessageDialog(comp, getTM().trA("sqlElement.noLinesDeleted"), getTM().trA("sqlElement.noLinesDeletedTitle"), JOptionPane.INFORMATION_MESSAGE);
3128
                        JOptionPane.showMessageDialog(comp, getTM().trA("sqlElement.noLinesDeleted"), getTM().trA("sqlElement.noLinesDeletedTitle"), JOptionPane.INFORMATION_MESSAGE);
3123
                    }
3129
                    }
3124
                }
3130
                }
3125
            } else {
3131
            } else {
3126
                int i = askSerious(comp, getTM().trM("sqlElement.deleteNoRef", map), confirmDelete);
3132
                int i = askSerious(comp, getTM().trM("sqlElement.deleteNoRef", map), confirmDelete);
3127
                if (i == JOptionPane.YES_OPTION) {
3133
                if (i == JOptionPane.YES_OPTION) {
3128
                    shouldArchive = true;
3134
                    shouldArchive = true;
3129
                }
3135
                }
3130
            }
3136
            }
3131
            return shouldArchive;
3137
            return shouldArchive;
3132
        } catch (Exception e) {
3138
        } catch (Exception e) {
3133
            ExceptionHandler.handle(comp, TM.tr("sqlElement.rowsToArchiveError", this), e);
3139
            ExceptionHandler.handle(comp, TM.tr("sqlElement.rowsToArchiveError", this), e);
3134
            return null;
3140
            return null;
3135
        }
3141
        }
3136
    }
3142
    }
3137
 
3143
 
3138
    private final String toString(Map<SQLTable, List<SQLRowAccessor>> descs) {
3144
    private final String toString(Map<SQLTable, List<SQLRowAccessor>> descs) {
3139
        final List<String> l = new ArrayList<String>(descs.size());
3145
        final List<String> l = new ArrayList<String>(descs.size());
3140
        for (final Entry<SQLTable, List<SQLRowAccessor>> e : descs.entrySet()) {
3146
        for (final Entry<SQLTable, List<SQLRowAccessor>> e : descs.entrySet()) {
3141
            final SQLTable t = e.getKey();
3147
            final SQLTable t = e.getKey();
3142
            final SQLElement elem = getElement(t);
3148
            final SQLElement elem = getElement(t);
3143
            l.add(elemToString(e.getValue().size(), elem));
3149
            l.add(elemToString(e.getValue().size(), elem));
3144
        }
3150
        }
3145
        return CollectionUtils.join(l, "\n");
3151
        return CollectionUtils.join(l, "\n");
3146
    }
3152
    }
3147
 
3153
 
3148
    private static final String elemToString(int count, SQLElement elem) {
3154
    private static final String elemToString(int count, SQLElement elem) {
3149
        return "- " + elem.getName().getNumeralVariant(count, Grammar.INDEFINITE_NUMERAL);
3155
        return "- " + elem.getName().getNumeralVariant(count, Grammar.INDEFINITE_NUMERAL);
3150
    }
3156
    }
3151
 
3157
 
3152
    // traduire TRANSFO.ID_ELEMENT_TABLEAU_PRI -> {TRANSFO[5], TRANSFO[12]}
3158
    // traduire TRANSFO.ID_ELEMENT_TABLEAU_PRI -> {TRANSFO[5], TRANSFO[12]}
3153
    // en 2 transformateurs vont perdre leurs champs 'Circuit primaire'
3159
    // en 2 transformateurs vont perdre leurs champs 'Circuit primaire'
3154
    private final String toStringExtern(SortedMap<LinkToCut, Integer> externRefs) {
3160
    private final String toStringExtern(SortedMap<LinkToCut, Integer> externRefs) {
3155
        final List<String> l = new ArrayList<String>();
3161
        final List<String> l = new ArrayList<String>();
3156
        final Map<String, Object> map = new HashMap<String, Object>(4);
3162
        final Map<String, Object> map = new HashMap<String, Object>(4);
3157
        for (final Entry<LinkToCut, Integer> entry : externRefs.entrySet()) {
3163
        for (final Entry<LinkToCut, Integer> entry : externRefs.entrySet()) {
3158
            final LinkToCut foreignKey = entry.getKey();
3164
            final LinkToCut foreignKey = entry.getKey();
3159
            final int count = entry.getValue();
3165
            final int count = entry.getValue();
3160
            final String label = foreignKey.getLabel();
3166
            final String label = foreignKey.getLabel();
3161
            final SQLElement elem = getElement(foreignKey.getTable());
3167
            final SQLElement elem = getElement(foreignKey.getTable());
3162
            map.put("elementName", elem.getName());
3168
            map.put("elementName", elem.getName());
3163
            map.put("count", count);
3169
            map.put("count", count);
3164
            map.put("linkName", label);
3170
            map.put("linkName", label);
3165
            l.add(getTM().trM("sqlElement.linksWillBeCut", map));
3171
            l.add(getTM().trM("sqlElement.linksWillBeCut", map));
3166
        }
3172
        }
3167
        return CollectionUtils.join(l, "\n");
3173
        return CollectionUtils.join(l, "\n");
3168
    }
3174
    }
3169
 
3175
 
3170
    private final int askSerious(Component comp, String msg, String title) {
3176
    private final int askSerious(Component comp, String msg, String title) {
3171
        return JOptionPane.showConfirmDialog(comp, msg, title + " (" + this.getPluralName() + ")", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
3177
        return JOptionPane.showConfirmDialog(comp, msg, title + " (" + this.getPluralName() + ")", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
3172
    }
3178
    }
3173
 
3179
 
3174
}
3180
}