OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Details | Compare with Previous | Last modification | View Log | RSS feed

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