OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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