OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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