OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.element;
15
 
73 ilm 16
import static org.openconcerto.sql.TM.getTM;
17 ilm 17
import org.openconcerto.sql.Configuration;
18
import org.openconcerto.sql.Log;
73 ilm 19
import org.openconcerto.sql.TM;
17 ilm 20
import org.openconcerto.sql.model.DBStructureItemNotFound;
21
import org.openconcerto.sql.model.SQLField;
22
import org.openconcerto.sql.model.SQLFieldsSet;
23
import org.openconcerto.sql.model.SQLRow;
24
import org.openconcerto.sql.model.SQLRowAccessor;
25
import org.openconcerto.sql.model.SQLRowMode;
26
import org.openconcerto.sql.model.SQLRowValues;
83 ilm 27
import org.openconcerto.sql.model.SQLRowValues.ForeignCopyMode;
28
import org.openconcerto.sql.model.SQLRowValuesCluster;
29
import org.openconcerto.sql.model.SQLRowValuesCluster.State;
30
import org.openconcerto.sql.model.SQLRowValuesCluster.StopRecurseException;
31
import org.openconcerto.sql.model.SQLRowValuesCluster.StoreMode;
32
import org.openconcerto.sql.model.SQLRowValuesCluster.WalkOptions;
17 ilm 33
import org.openconcerto.sql.model.SQLSelect;
34
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
35
import org.openconcerto.sql.model.SQLTable;
83 ilm 36
import org.openconcerto.sql.model.SQLTable.VirtualFields;
19 ilm 37
import org.openconcerto.sql.model.Where;
17 ilm 38
import org.openconcerto.sql.model.graph.DatabaseGraph;
39
import org.openconcerto.sql.model.graph.Link;
83 ilm 40
import org.openconcerto.sql.model.graph.Link.Direction;
17 ilm 41
import org.openconcerto.sql.request.ComboSQLRequest;
42
import org.openconcerto.sql.request.ListSQLRequest;
43
import org.openconcerto.sql.request.SQLCache;
80 ilm 44
import org.openconcerto.sql.request.SQLFieldTranslator;
19 ilm 45
import org.openconcerto.sql.sqlobject.SQLTextCombo;
17 ilm 46
import org.openconcerto.sql.users.rights.UserRightsManager;
47
import org.openconcerto.sql.utils.SQLUtils;
48
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
21 ilm 49
import org.openconcerto.sql.view.list.IListeAction;
17 ilm 50
import org.openconcerto.sql.view.list.SQLTableModelColumn;
51
import org.openconcerto.sql.view.list.SQLTableModelColumnPath;
52
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
83 ilm 53
import org.openconcerto.ui.group.Group;
17 ilm 54
import org.openconcerto.utils.CollectionMap;
55
import org.openconcerto.utils.CollectionUtils;
56
import org.openconcerto.utils.CompareUtils;
57
import org.openconcerto.utils.ExceptionHandler;
58
import org.openconcerto.utils.ExceptionUtils;
83 ilm 59
import org.openconcerto.utils.LinkedListMap;
60
import org.openconcerto.utils.ListMap;
61
import org.openconcerto.utils.NumberUtils;
62
import org.openconcerto.utils.RecursionType;
63
import org.openconcerto.utils.SetMap;
27 ilm 64
import org.openconcerto.utils.Tuple2;
17 ilm 65
import org.openconcerto.utils.cache.CacheResult;
66
import org.openconcerto.utils.cc.IClosure;
27 ilm 67
import org.openconcerto.utils.cc.ITransformer;
19 ilm 68
import org.openconcerto.utils.change.ListChangeIndex;
69
import org.openconcerto.utils.change.ListChangeRecorder;
73 ilm 70
import org.openconcerto.utils.i18n.Grammar;
71
import org.openconcerto.utils.i18n.Grammar_fr;
72
import org.openconcerto.utils.i18n.NounClass;
73
import org.openconcerto.utils.i18n.Phrase;
17 ilm 74
 
75
import java.awt.Component;
76
import java.lang.reflect.Constructor;
77
import java.sql.SQLException;
78
import java.util.ArrayList;
79
import java.util.Collection;
80
import java.util.Collections;
81
import java.util.HashMap;
82
import java.util.HashSet;
83 ilm 83
import java.util.IdentityHashMap;
17 ilm 84
import java.util.Iterator;
28 ilm 85
import java.util.LinkedHashMap;
27 ilm 86
import java.util.LinkedList;
17 ilm 87
import java.util.List;
83 ilm 88
import java.util.ListIterator;
17 ilm 89
import java.util.Map;
90
import java.util.Map.Entry;
91
import java.util.Set;
92
import java.util.SortedMap;
83 ilm 93
import java.util.concurrent.atomic.AtomicReference;
17 ilm 94
import java.util.logging.Level;
95
 
19 ilm 96
import javax.swing.JComponent;
17 ilm 97
import javax.swing.JOptionPane;
19 ilm 98
import javax.swing.text.JTextComponent;
17 ilm 99
 
73 ilm 100
import net.jcip.annotations.GuardedBy;
101
 
17 ilm 102
/**
103
 * Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
104
 * privateForeignField).
105
 *
106
 * @author ilm
107
 */
108
public abstract class SQLElement {
109
 
110
    static final private Set<String> computingFF = Collections.unmodifiableSet(new HashSet<String>());
111
    static final private Set<SQLField> computingRF = Collections.unmodifiableSet(new HashSet<SQLField>());
112
 
73 ilm 113
    private static Phrase createPhrase(String singular, String plural) {
114
        final NounClass nounClass;
115
        final String base;
116
        if (singular.startsWith("une ")) {
117
            nounClass = NounClass.FEMININE;
118
            base = singular.substring(4);
119
        } else if (singular.startsWith("un ")) {
120
            nounClass = NounClass.MASCULINE;
121
            base = singular.substring(3);
122
        } else {
123
            nounClass = null;
124
            base = singular;
125
        }
126
        final Phrase res = new Phrase(Grammar_fr.getInstance(), base, nounClass);
127
        if (nounClass != null)
128
            res.putVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR, singular);
129
        res.putVariant(Grammar.PLURAL, plural);
130
        return res;
131
    }
132
 
17 ilm 133
    // from the most loss of information to the least.
134
    public static enum ReferenceAction {
135
        /** If a referenced row is archived, empty the foreign field */
136
        SET_EMPTY,
137
        /** If a referenced row is archived, archive this row too */
138
        CASCADE,
139
        /** If a referenced row is to be archived, abort the operation */
140
        RESTRICT
141
    }
142
 
41 ilm 143
    static final public String DEFAULT_COMP_ID = "default component code";
73 ilm 144
    /**
145
     * If this value is passed to the constructor, {@link #createCode()} will only be called the
146
     * first time {@link #getCode()} is. This allow the method to use objects passed to the
147
     * constructor of a subclass.
148
     */
149
    static final public String DEFERRED_CODE = new String("deferred code");
27 ilm 150
 
80 ilm 151
    @GuardedBy("this")
73 ilm 152
    private SQLElementDirectory directory;
153
    private String l18nPkgName;
80 ilm 154
    private Class<?> l18nClass;
73 ilm 155
    private Phrase name;
17 ilm 156
    private final SQLTable primaryTable;
27 ilm 157
    // used as a key in SQLElementDirectory so it should be immutable
73 ilm 158
    private String code;
17 ilm 159
    private ComboSQLRequest combo;
160
    private ListSQLRequest list;
161
    private SQLTableModelSourceOnline tableSrc;
21 ilm 162
    private final ListChangeRecorder<IListeAction> rowActions;
83 ilm 163
    private final LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>> components;
17 ilm 164
    // foreign fields
165
    private Set<String> normalFF;
166
    private String parentFF;
167
    private Set<String> sharedFF;
168
    private Map<String, SQLElement> privateFF;
169
    private final Map<String, ReferenceAction> actions;
170
    // referent fields
171
    private Set<SQLField> childRF;
172
    private Set<SQLField> privateParentRF;
173
    private Set<SQLField> otherRF;
174
    // lazy creation
175
    private SQLCache<SQLRowAccessor, Object> modelCache;
176
 
19 ilm 177
    private final Map<String, JComponent> additionalFields;
25 ilm 178
    private final List<SQLTableModelColumn> additionalListCols;
73 ilm 179
    @GuardedBy("this")
57 ilm 180
    private List<String> mdPath;
83 ilm 181
    private Group defaultGroup;
19 ilm 182
 
73 ilm 183
    @Deprecated
17 ilm 184
    public SQLElement(String singular, String plural, SQLTable primaryTable) {
73 ilm 185
        this(primaryTable, createPhrase(singular, plural));
27 ilm 186
    }
187
 
73 ilm 188
    public SQLElement(SQLTable primaryTable) {
189
        this(primaryTable, null);
190
    }
191
 
192
    public SQLElement(final SQLTable primaryTable, final Phrase name) {
193
        this(primaryTable, name, null);
194
    }
195
 
196
    public SQLElement(final SQLTable primaryTable, final Phrase name, final String code) {
17 ilm 197
        super();
198
        if (primaryTable == null) {
73 ilm 199
            throw new DBStructureItemNotFound("table is null for " + this.getClass());
17 ilm 200
        }
201
        this.primaryTable = primaryTable;
80 ilm 202
        this.setL18nPackageName(null);
73 ilm 203
        this.setDefaultName(name);
27 ilm 204
        this.code = code == null ? createCode() : code;
17 ilm 205
        this.combo = null;
206
        this.list = null;
21 ilm 207
        this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>());
19 ilm 208
        this.actions = new HashMap<String, ReferenceAction>();
209
        this.resetRelationships();
17 ilm 210
 
83 ilm 211
        this.components = new LinkedListMap<String, ITransformer<Tuple2<SQLElement, String>, SQLComponent>>();
27 ilm 212
 
19 ilm 213
        this.modelCache = null;
214
 
28 ilm 215
        // the components should always be in the same order
216
        this.additionalFields = new LinkedHashMap<String, JComponent>();
25 ilm 217
        this.additionalListCols = new ArrayList<SQLTableModelColumn>();
57 ilm 218
        this.mdPath = Collections.emptyList();
19 ilm 219
    }
220
 
221
    /**
27 ilm 222
     * Should return the code for this element. This method is only called if the <code>code</code>
223
     * parameter of the constructor is <code>null</code>.
224
     *
225
     * @return the default code for this element.
226
     */
227
    protected String createCode() {
228
        return getClass().getName() + "-" + getTable().getName();
229
    }
230
 
83 ilm 231
    public Group getDefaultGroup() {
232
        return defaultGroup;
233
    }
234
 
235
    public void setDefaultGroup(Group defaultGroup) {
236
        this.defaultGroup = defaultGroup;
237
    }
238
 
27 ilm 239
    /**
19 ilm 240
     * Must be called if foreign/referent keys are added or removed.
241
     */
242
    public synchronized void resetRelationships() {
17 ilm 243
        this.privateFF = null;
244
        this.parentFF = null;
245
        this.normalFF = null;
246
        this.sharedFF = null;
19 ilm 247
        this.actions.clear();
17 ilm 248
 
249
        this.childRF = null;
250
        this.privateParentRF = null;
251
        this.otherRF = null;
252
    }
253
 
73 ilm 254
    protected synchronized final boolean areRelationshipsInited() {
255
        return this.sharedFF != null;
256
    }
257
 
17 ilm 258
    private void checkSelfCall(boolean check, final String methodName) {
259
        assert check : this + " " + methodName + "() is calling itself, and thus the caller will only see a partial state";
260
    }
261
 
262
    private synchronized void initFF() {
263
        checkSelfCall(this.sharedFF != computingFF, "initFF");
73 ilm 264
        if (areRelationshipsInited())
17 ilm 265
            return;
266
        this.sharedFF = computingFF;
267
 
268
        final Set<String> privates = new HashSet<String>(this.getPrivateFields());
269
        this.privateFF = new HashMap<String, SQLElement>(privates.size());
270
        final Set<String> parents = new HashSet<String>();
271
        this.normalFF = new HashSet<String>();
272
        final Set<String> tmpSharedFF = new HashSet<String>();
273
        for (final SQLField ff : this.getTable().getForeignKeys()) {
274
            final String fieldName = ff.getName();
275
            final SQLElement foreignElement = this.getForeignElement(fieldName);
276
            if (privates.contains(fieldName)) {
277
                privates.remove(fieldName);
278
                this.privateFF.put(fieldName, foreignElement);
279
            } else if (foreignElement.isShared()) {
280
                tmpSharedFF.add(fieldName);
281
            } else if (foreignElement.getChildrenReferentFields().contains(ff)) {
282
                parents.add(fieldName);
283
            } else {
284
                this.normalFF.add(fieldName);
285
            }
286
        }
287
        if (parents.size() > 1)
288
            throw new IllegalStateException("for " + this + " more than one parent :" + parents);
289
        this.parentFF = parents.size() == 0 ? null : (String) parents.iterator().next();
290
        if (privates.size() > 0)
291
            throw new IllegalStateException("for " + this + " these private foreign fields are not valid :" + privates);
83 ilm 292
        assert assertPrivateDefaultValues();
293
 
17 ilm 294
        this.sharedFF = tmpSharedFF;
295
 
296
        // MAYBE move required fields to SQLElement and use RESTRICT
297
        this.actions.put(this.parentFF, ReferenceAction.CASCADE);
298
        for (final String s : this.privateFF.keySet()) {
299
            this.actions.put(s, ReferenceAction.SET_EMPTY);
300
        }
301
        for (final String s : this.normalFF) {
302
            this.actions.put(s, ReferenceAction.SET_EMPTY);
303
        }
304
        for (final String s : this.sharedFF) {
305
            this.actions.put(s, ReferenceAction.RESTRICT);
306
        }
307
        this.ffInited();
308
    }
309
 
83 ilm 310
    // since by definition private cannot be shared, the default value must be empty
311
    private final boolean assertPrivateDefaultValues() {
312
        for (final Entry<String, SQLElement> e : this.privateFF.entrySet()) {
313
            final String fieldName = e.getKey();
314
            final Number privateDefault = (Number) getTable().getField(fieldName).getParsedDefaultValue().getValue();
315
            final Number foreignUndef = e.getValue().getTable().getUndefinedIDNumber();
316
            assert NumberUtils.areNumericallyEqual(privateDefault, foreignUndef) : fieldName + " not empty : " + privateDefault;
317
        }
318
        return true;
319
    }
320
 
17 ilm 321
    protected void ffInited() {
322
        // MAYBE use DELETE_RULE of Link
323
    }
324
 
325
    // convert the list of String of getChildren() to a Set of SQLField pointing to this table
326
    private synchronized Set<SQLField> computeChildrenRF() {
327
        final Set<SQLField> res = new HashSet<SQLField>();
328
        // eg "BATIMENT" or "BATIMENT.ID_SITE"
329
        for (final String child : this.getChildren()) {
330
            // a field from our child to us, eg |BATIMENT.ID_SITE|
331
            final SQLField childField;
332
 
333
            final int comma = child.indexOf(',');
334
            final String tableName = comma < 0 ? child : child.substring(0, comma);
335
            final SQLTable childTable = this.getTable().getTable(tableName);
336
 
337
            if (comma < 0) {
338
                final Set<SQLField> keys = childTable.getForeignKeys(this.getTable());
339
                if (keys.size() != 1)
340
                    throw new IllegalArgumentException("cannot find a foreign from " + child + " to " + this.getTable());
341
                childField = keys.iterator().next();
342
            } else {
343
                childField = childTable.getField(child.substring(comma + 1));
344
                final SQLTable foreignTable = childField.getDBSystemRoot().getGraph().getForeignTable(childField);
345
                if (!foreignTable.equals(this.getTable())) {
346
                    throw new IllegalArgumentException(childField + " doesn't point to " + this.getTable());
347
                }
348
            }
349
            res.add(childField);
350
        }
351
        return res;
352
    }
353
 
354
    private synchronized void initRF() {
355
        checkSelfCall(this.otherRF != computingRF, "initRF");
356
        if (this.otherRF != null)
357
            return;
358
        this.otherRF = computingRF;
359
 
360
        this.privateParentRF = new HashSet<SQLField>();
361
        final Set<SQLField> tmpOtherRF = new HashSet<SQLField>();
362
        for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) {
363
            // don't force every table to have an SQLElement (eg ELEMENT_MISSION)
364
            final SQLElement refElem = this.getElementLenient(refField.getTable());
365
            if (refElem != null && refElem.getPrivateForeignFields().contains(refField.getName())) {
366
                this.privateParentRF.add(refField);
367
            } else if (!this.getChildrenReferentFields().contains(refField)) {
368
                tmpOtherRF.add(refField);
369
            }
370
        }
371
        this.otherRF = tmpOtherRF;
372
    }
373
 
374
    // childRF is done outside initRF() to avoid :
375
    // MISSION.initRF() -> ELEMENT_MISSION.getPrivateForeignFields() ->
376
    // ELEMENT_MISSION.initFF() -> MISSION.getChildrenReferentFields() -> MISSION.initRF()
377
    private synchronized void initChildRF() {
378
        checkSelfCall(this.childRF != computingRF, "initFF");
379
        if (this.childRF != null)
380
            return;
381
        this.childRF = computingRF;
382
 
19 ilm 383
        final Set<SQLField> children = this.computeChildrenRF();
17 ilm 384
 
385
        final Set<SQLField> tmpChildRF = new HashSet<SQLField>();
386
        for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) {
387
            // don't force every table to have an SQLElement (eg ELEMENT_MISSION)
388
            final SQLElement refElem = this.getElementLenient(refField.getTable());
389
            // if no element found, treat as elements with no parent
390
            final SQLField refParentFF = refElem == null ? null : refElem.getParentFF();
391
            // check coherence, either overload getParentFFName() or use getChildren(), but not both
392
            if (refParentFF != null && children.contains(refField))
393
                throw new IllegalStateException(refElem + " specifies this as its parent: " + refParentFF + " and is also mentioned as our (" + this + ") child: " + refField);
394
            if (children.contains(refField) || refParentFF == refField) {
395
                tmpChildRF.add(refField);
396
            }
397
        }
398
        // pas besoin de faire comme dans initFF pour vérifier children :
399
        // computeChildrenRF le fait déjà
400
        this.childRF = tmpChildRF;
401
    }
402
 
73 ilm 403
    final void setDirectory(final SQLElementDirectory directory) {
404
        // since this method should only be called at the end of SQLElementDirectory.addSQLElement()
80 ilm 405
        assert directory == null || directory.getElement(this.getTable()) == this;
73 ilm 406
        synchronized (this) {
407
            if (this.directory != directory) {
408
                if (this.areRelationshipsInited())
409
                    this.resetRelationships();
410
                this.directory = directory;
411
            }
412
        }
413
    }
414
 
80 ilm 415
    public synchronized final SQLElementDirectory getDirectory() {
73 ilm 416
        return this.directory;
417
    }
418
 
17 ilm 419
    final SQLElement getElement(SQLTable table) {
420
        final SQLElement res = getElementLenient(table);
421
        if (res == null)
422
            throw new IllegalStateException("no element for " + table.getSQLName());
423
        return res;
424
    }
425
 
426
    final SQLElement getElementLenient(SQLTable table) {
73 ilm 427
        synchronized (this) {
428
            return this.getDirectory().getElement(table);
429
        }
17 ilm 430
    }
431
 
432
    public final SQLElement getForeignElement(String foreignField) {
433
        try {
434
            return this.getElement(this.getForeignTable(foreignField));
435
        } catch (RuntimeException e) {
436
            throw new IllegalStateException("no element for " + foreignField + " in " + this, e);
437
        }
438
    }
439
 
440
    private final SQLTable getForeignTable(String foreignField) {
441
        return this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(foreignField));
442
    }
443
 
73 ilm 444
    public final synchronized String getL18nPackageName() {
445
        return this.l18nPkgName;
446
    }
447
 
80 ilm 448
    public final synchronized Class<?> getL18nClass() {
449
        return this.l18nClass;
73 ilm 450
    }
451
 
80 ilm 452
    public final void setL18nLocation(Class<?> clazz) {
453
        this.setL18nLocation(clazz.getPackage().getName(), clazz);
454
    }
455
 
456
    public final void setL18nPackageName(String name) {
457
        this.setL18nLocation(name, null);
458
    }
459
 
460
    /**
461
     * Set the location for the localized name.
462
     *
463
     * @param name a package name, can be <code>null</code> :
464
     *        {@link SQLElementDirectory#getL18nPackageName()} will be used.
465
     * @param ctxt the class loader to load the resource, <code>null</code> meaning this class.
466
     * @see SQLElementDirectory#getName(SQLElement)
467
     */
468
    public final synchronized void setL18nLocation(final String name, final Class<?> ctxt) {
73 ilm 469
        this.l18nPkgName = name;
80 ilm 470
        this.l18nClass = ctxt == null ? this.getClass() : ctxt;
73 ilm 471
    }
472
 
473
    /**
474
     * Set the default name, used if no translations could be found.
475
     *
476
     * @param name the default name, if <code>null</code> the {@link #getTable() table} name will be
477
     *        used.
478
     */
479
    public final synchronized void setDefaultName(Phrase name) {
480
        this.name = name != null ? name : Phrase.getInvariant(getTable().getName());
481
    }
482
 
483
    /**
484
     * The default name.
485
     *
486
     * @return the default name, never <code>null</code>.
487
     */
488
    public final synchronized Phrase getDefaultName() {
489
        return this.name;
490
    }
491
 
492
    /**
493
     * The name of this element in the current locale.
494
     *
495
     * @return the name of this, {@link #getDefaultName()} if there's no {@link #getDirectory()
496
     *         directory} or if it hasn't a name for this.
497
     * @see SQLElementDirectory#getName(SQLElement)
498
     */
499
    public final Phrase getName() {
500
        final SQLElementDirectory dir = this.getDirectory();
501
        final Phrase res = dir == null ? null : dir.getName(this);
502
        return res == null ? this.getDefaultName() : res;
503
    }
504
 
17 ilm 505
    public String getPluralName() {
73 ilm 506
        return this.getName().getVariant(Grammar.PLURAL);
17 ilm 507
    }
508
 
509
    public String getSingularName() {
73 ilm 510
        return this.getName().getVariant(Grammar.INDEFINITE_ARTICLE_SINGULAR);
17 ilm 511
    }
512
 
513
    public CollectionMap<String, String> getShowAs() {
514
        // nothing by default
515
        return null;
516
    }
517
 
518
    /**
519
     * Fields that can neither be inserted nor updated.
520
     *
521
     * @return fields that cannot be modified.
522
     */
523
    public Set<String> getReadOnlyFields() {
524
        return Collections.emptySet();
525
    }
526
 
527
    /**
528
     * Fields that can only be set on insertion.
529
     *
530
     * @return fields that cannot be modified.
531
     */
532
    public Set<String> getInsertOnlyFields() {
533
        return Collections.emptySet();
534
    }
535
 
536
    private final SQLCache<SQLRowAccessor, Object> getModelCache() {
537
        if (this.modelCache == null)
73 ilm 538
            this.modelCache = new SQLCache<SQLRowAccessor, Object>(60, -1, "modelObjects of " + this.getCode());
17 ilm 539
        return this.modelCache;
540
    }
541
 
542
    // *** update
543
 
544
    /**
545
     * Compute the necessary steps to transform <code>from</code> into <code>to</code>.
546
     *
547
     * @param from the row currently in the db.
548
     * @param to the new values.
549
     * @return the script transforming <code>from</code> into <code>to</code>.
550
     */
551
    public final UpdateScript update(SQLRowValues from, SQLRowValues to) {
552
        check(from);
553
        check(to);
554
 
555
        if (!from.hasID())
556
            throw new IllegalArgumentException("missing id in " + from);
557
        if (from.getID() != to.getID())
558
            throw new IllegalArgumentException("not the same row: " + from + " != " + to);
559
 
560
        final Set<SQLField> fks = this.getTable().getForeignKeys();
561
        final UpdateScript res = new UpdateScript(this.getTable());
562
        for (final String field : to.getFields()) {
563
            if (!fks.contains(this.getTable().getField(field))) {
564
                res.getUpdateRow().put(field, to.getObject(field));
565
            } else {
566
                final Object fromPrivate = from.getObject(field);
567
                final Object toPrivate = to.getObject(field);
83 ilm 568
                final SQLElement privateElem = this.getPrivateElement(field);
569
                if (privateElem != null) {
570
                    assert !from.isDefault(field) : "A row in the DB cannot have DEFAULT";
571
                    final boolean fromIsEmpty = from.isForeignEmpty(field);
572
                    // as checked in initFF() the default for a private is empty
573
                    final boolean toIsEmpty = to.isDefault(field) || to.isForeignEmpty(field);
574
                    if (fromIsEmpty && toIsEmpty) {
575
                        // nothing to do, don't add to v
576
                    } else if (fromIsEmpty) {
577
                        final SQLRowValues toPR = (SQLRowValues) toPrivate;
578
                        // insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouillé"}
579
                        // clear referents otherwise we will merge the updateRow with the to
580
                        // graph (toPR being a private is pointed to by its owner, which itself
581
                        // points to others, but we just want the private)
582
                        res.getUpdateRow().put(field, toPR.deepCopy().clearReferents());
583
                    } else if (toIsEmpty) {
584
                        // archive
585
                        res.addToArchive(privateElem, from.getForeign(field));
586
                    } else {
587
                        // neither is empty
588
                        if (!CompareUtils.equals(from.getForeignID(field), to.getForeignID(field)))
589
                            throw new IllegalArgumentException("private have changed for " + field + " : " + fromPrivate + " != " + toPrivate);
590
                        if (toPrivate instanceof SQLRowValues) {
591
                            final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
592
                            final SQLRowValues toPR = (SQLRowValues) toPrivate;
593
                            // must have same ID
594
                            res.put(field, privateElem.update(fromPR, toPR));
17 ilm 595
                        }
596
                    }
83 ilm 597
                } else if (to.isDefault(field)) {
598
                    res.getUpdateRow().putDefault(field);
17 ilm 599
                } else {
83 ilm 600
                    res.getUpdateRow().put(field, to.getForeignIDNumber(field));
17 ilm 601
                }
602
            }
603
        }
604
 
605
        return res;
606
    }
607
 
83 ilm 608
    public void unarchiveNonRec(int id) throws SQLException {
609
        this.unarchive(this.getTable().getRow(id), false);
610
    }
611
 
17 ilm 612
    public final void unarchive(int id) throws SQLException {
613
        this.unarchive(this.getTable().getRow(id));
614
    }
615
 
616
    public void unarchive(final SQLRow row) throws SQLException {
83 ilm 617
        this.unarchive(row, true);
618
    }
619
 
620
    public void unarchive(final SQLRow row, final boolean desc) throws SQLException {
17 ilm 621
        checkUndefined(row);
622
        // don't test row.isArchived() (it is done by getTree())
623
        // to allow an unarchived parent to unarchive all its descendants.
624
 
625
        // nos descendants
83 ilm 626
        final SQLRowValues descsAndMe = desc ? this.getTree(row, true) : row.asRowValues();
627
        final SQLRowValues connectedRows = new ArchivedGraph(this.getDirectory(), descsAndMe).expand();
17 ilm 628
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
629
            @Override
630
            public Object create() throws SQLException {
83 ilm 631
                setArchive(Collections.singletonList(connectedRows.getGraph()), false);
17 ilm 632
                return null;
633
            }
634
        });
635
    }
636
 
637
    public final void archive(int id) throws SQLException {
638
        this.archive(this.getTable().getRow(id));
639
    }
640
 
83 ilm 641
    public final void archive(final Collection<? extends SQLRowAccessor> rows) throws SQLException {
642
        // rows checked by TreesOfSQLRows
643
        this.archive(new TreesOfSQLRows(this, rows), true);
644
    }
645
 
17 ilm 646
    public final void archive(SQLRow row) throws SQLException {
647
        this.archive(row, true);
648
    }
649
 
650
    /**
651
     * Archive la ligne demandée et tous ses descendants mais ne cherche pas à couper les références
652
     * pointant sur ceux-ci. ATTN peut donc laisser la base dans un état inconsistent, à n'utiliser
653
     * que si aucun lien ne pointe sur ceux ci. En revanche, accélère grandement (par exemple pour
654
     * OBSERVATION) car pas besoin de chercher toutes les références.
655
     *
656
     * @param id la ligne voulue.
657
     * @throws SQLException if pb while archiving.
658
     */
659
    public final void archiveNoCut(int id) throws SQLException {
660
        this.archive(this.getTable().getRow(id), false);
661
    }
662
 
663
    protected void archive(final SQLRow row, final boolean cutLinks) throws SQLException {
664
        this.archive(new TreesOfSQLRows(this, row), cutLinks);
665
    }
666
 
667
    protected void archive(final TreesOfSQLRows trees, final boolean cutLinks) throws SQLException {
668
        if (trees.getElem() != this)
669
            throw new IllegalArgumentException(this + " != " + trees.getElem());
670
        for (final SQLRow row : trees.getRows())
671
            checkUndefined(row);
672
 
673
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
674
            @Override
675
            public Object create() throws SQLException {
676
                // reference
677
                // d'abord couper les liens qui pointent sur les futurs archivés
678
                if (cutLinks) {
679
                    // TODO prend bcp de temps
680
                    // FIXME update tableau pour chaque observation, ecrase les changements
681
                    // faire : 'La base à changée voulez vous recharger ou garder vos modifs ?'
83 ilm 682
                    final Map<SQLField, Set<SQLRow>> externReferences = trees.getExternReferences();
17 ilm 683
                    // avoid toString() which might make requests to display rows (eg archived)
684
                    if (Log.get().isLoggable(Level.FINEST))
685
                        Log.get().finest("will cut : " + externReferences);
83 ilm 686
                    for (final Entry<SQLField, Set<SQLRow>> e : externReferences.entrySet()) {
687
                        final SQLField refKey = e.getKey();
688
                        for (final SQLRow ref : e.getValue()) {
17 ilm 689
                            ref.createEmptyUpdateRow().putEmptyLink(refKey.getName()).update();
690
                        }
691
                    }
692
                    Log.get().finest("done cutting links");
693
                }
694
 
695
                // on archive tous nos descendants
83 ilm 696
                setArchive(trees.getClusters(), true);
17 ilm 697
 
698
                return null;
699
            }
700
        });
701
    }
702
 
83 ilm 703
    static private final SQLRowValues setArchive(SQLRowValues r, final boolean archive) throws SQLException {
704
        final SQLField archiveField = r.getTable().getArchiveField();
705
        final Object newVal;
706
        if (Boolean.class.equals(archiveField.getType().getJavaType()))
707
            newVal = archive;
708
        else
709
            newVal = archive ? 1 : 0;
710
        r.put(archiveField.getName(), newVal);
711
        return r;
17 ilm 712
    }
713
 
83 ilm 714
    // all rows will be either archived or unarchived (handling cycles)
715
    static private void setArchive(final Collection<SQLRowValuesCluster> clustersToArchive, final boolean archive) throws SQLException {
716
        final Set<SQLRowValues> toArchive = Collections.newSetFromMap(new IdentityHashMap<SQLRowValues, Boolean>());
717
        for (final SQLRowValuesCluster c : clustersToArchive)
718
            toArchive.addAll(c.getItems());
17 ilm 719
 
83 ilm 720
        final Map<SQLRow, SQLRowValues> linksCut = new HashMap<SQLRow, SQLRowValues>();
721
        while (!toArchive.isEmpty()) {
722
            // archive the maximum without referents
723
            // or unarchive the maximum without foreigns
724
            int archivedCount = -1;
725
            while (archivedCount != 0) {
726
                archivedCount = 0;
727
                final Iterator<SQLRowValues> iter = toArchive.iterator();
728
                while (iter.hasNext()) {
729
                    final SQLRowValues desc = iter.next();
730
                    if (archive && !desc.hasReferents() || !archive && !desc.hasForeigns()) {
731
                        SQLRowValues updateVals = linksCut.remove(desc.asRow());
732
                        if (updateVals == null)
733
                            updateVals = new SQLRowValues(desc.getTable());
734
                        // ne pas faire les fire après sinon qd on efface plusieurs éléments
735
                        // de la même table :
736
                        // on fire pour le 1er => updateSearchList => IListe.select(userID)
737
                        // hors si userID a aussi été archivé (mais il n'y a pas eu son fire
738
                        // correspondant), le component va lancer un RowNotFound
739
                        setArchive(updateVals, archive).setID(desc.getIDNumber());
740
                        // don't check validity since table events might have not already be
741
                        // fired
742
                        assert updateVals.getGraph().size() == 1 : "Archiving a graph : " + updateVals.printGraph();
743
                        updateVals.getGraph().store(StoreMode.COMMIT, false);
744
                        // remove from graph
745
                        desc.clear();
746
                        desc.clearReferents();
747
                        assert desc.getGraph().size() == 1 : "Next loop won't progress : " + desc.printGraph();
748
                        archivedCount++;
749
                        iter.remove();
750
                    }
751
                }
752
            }
753
 
754
            // if not empty there's at least one cycle
755
            if (!toArchive.isEmpty()) {
756
                // Identify one cycle, ATTN first might not be itself part of the cycle, like the
757
                // BATIMENT and the LOCALs :
758
                /**
759
                 * <pre>
760
                 * BATIMENT
761
                 * |      \
762
                 * LOCAL1  LOCAL2
763
                 * |        \
764
                 * CPI ---> SOURCE
765
                 *     <--/
766
                 * </pre>
767
                 */
768
                final SQLRowValues first = toArchive.iterator().next();
769
                // Among the rows in the cycle, archive one by cutting links (choose
770
                // one with the least of them)
771
                final AtomicReference<SQLRowValues> cutLinksRef = new AtomicReference<SQLRowValues>(null);
772
                first.getGraph().walk(first, null, new ITransformer<State<Object>, Object>() {
773
                    @Override
774
                    public Object transformChecked(State<Object> input) {
775
                        final SQLRowValues last = input.getCurrent();
776
                        boolean cycleFound = false;
777
                        int minLinksCount = -1;
778
                        SQLRowValues leastLinks = null;
779
                        final Iterator<SQLRowValues> iter = input.getValsPath().iterator();
780
                        while (iter.hasNext()) {
781
                            final SQLRowValues v = iter.next();
782
                            if (!cycleFound) {
783
                                // start of cycle found
784
                                cycleFound = iter.hasNext() && v == last;
785
                            }
786
                            if (cycleFound) {
787
                                // don't use getReferentRows() as it's not the row count but
788
                                // the link count that's important
789
                                final int linksCount = archive ? v.getReferentsMap().allValues().size() : v.getForeigns().size();
790
                                // otherwise should have been removed above
791
                                assert linksCount > 0;
792
                                if (leastLinks == null || linksCount < minLinksCount) {
793
                                    leastLinks = v;
794
                                    minLinksCount = linksCount;
795
                                }
796
                            }
797
                        }
798
                        if (cycleFound) {
799
                            cutLinksRef.set(leastLinks);
800
                            throw new StopRecurseException();
801
                        }
802
 
803
                        return null;
804
                    }
805
                }, new WalkOptions(Direction.REFERENT).setRecursionType(RecursionType.BREADTH_FIRST).setStartIncluded(false).setCycleAllowed(true));
806
                final SQLRowValues cutLinks = cutLinksRef.get();
807
 
808
                // if there were no cycles rows would have been removed above
809
                assert cutLinks != null;
810
 
811
                // cut links, and store them to be restored
812
                if (archive) {
813
                    for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(cutLinks.getReferentsMap()).entrySet()) {
814
                        final String fieldName = e.getKey().getName();
815
                        for (final SQLRowValues v : e.getValue()) {
816
                            // store before cutting
817
                            SQLRowValues cutVals = linksCut.get(v.asRow());
818
                            if (cutVals == null) {
819
                                cutVals = new SQLRowValues(v.getTable());
820
                                linksCut.put(v.asRow(), cutVals);
821
                            }
822
                            assert !cutVals.getFields().contains(fieldName) : fieldName + " already cut for " + v;
823
                            assert !v.isForeignEmpty(fieldName) : "Nothing to cut";
824
                            cutVals.put(fieldName, v.getForeignIDNumber(fieldName));
825
                            // cut graph
826
                            v.putEmptyLink(fieldName);
827
                            // cut DB
828
                            new SQLRowValues(v.getTable()).putEmptyLink(fieldName).update(v.getID());
829
                        }
830
                    }
831
                } else {
832
                    // store before cutting
833
                    final Set<String> foreigns = new HashSet<String>(cutLinks.getForeigns().keySet());
834
                    final SQLRowValues oldVal = linksCut.put(cutLinks.asRow(), new SQLRowValues(cutLinks, ForeignCopyMode.COPY_ID_OR_RM));
835
                    // can't pass twice, as the first time we clear all foreigns, so the next loop
836
                    // must unarchive it.
837
                    assert oldVal == null : "Already cut";
838
                    // cut graph
839
                    cutLinks.removeAll(foreigns);
840
                    // cut DB
841
                    final SQLRowValues updateVals = new SQLRowValues(cutLinks.getTable());
842
                    for (final String fieldName : foreigns) {
843
                        updateVals.putEmptyLink(fieldName);
844
                    }
845
                    updateVals.update(cutLinks.getID());
846
                }
847
                // ready to begin another loop
848
                assert archive && !cutLinks.hasReferents() || !archive && !cutLinks.hasForeigns();
849
            }
850
        }
851
        // for unarchive we need to update again the already treated (unarchived) row
852
        assert !archive || linksCut.isEmpty() : "Some links weren't restored : " + linksCut;
853
        if (!archive) {
854
            for (final Entry<SQLRow, SQLRowValues> e : linksCut.entrySet()) {
855
                e.getValue().update(e.getKey().getID());
856
            }
857
        }
17 ilm 858
    }
859
 
860
    public void delete(SQLRowAccessor r) throws SQLException {
861
        this.check(r);
862
        if (true)
863
            throw new UnsupportedOperationException("not yet implemented.");
864
    }
865
 
866
    public final SQLTable getTable() {
867
        return this.primaryTable;
868
    }
869
 
80 ilm 870
    /**
871
     * A code identifying a specific meaning for the table and fields. I.e. it is used by
872
     * {@link #getName() names} and {@link SQLFieldTranslator item metadata}. E.g. if two
873
     * applications use the same table for different purposes (at different times, of course), their
874
     * elements should not share a code. On the contrary, if one application merely adds a field to
875
     * an existing table, the new element should keep the same code so that existing name and
876
     * documentation remain.
877
     *
878
     * @return a code for the table and its meaning.
879
     */
73 ilm 880
    public synchronized final String getCode() {
881
        if (this.code == DEFERRED_CODE) {
882
            final String createCode = this.createCode();
883
            if (createCode == DEFERRED_CODE)
884
                throw new IllegalStateException("createCode() returned DEFERRED_CODE");
885
            this.code = createCode;
886
        }
27 ilm 887
        return this.code;
888
    }
889
 
17 ilm 890
    /**
891
     * Is the rows of this element shared, ie rows are unique and must not be copied.
892
     *
893
     * @return <code>true</code> if this element is shared.
894
     */
895
    public boolean isShared() {
896
        return false;
897
    }
898
 
899
    /**
900
     * Must the rows of this element be copied when traversing a hierarchy.
901
     *
902
     * @return <code>true</code> if the element must not be copied.
903
     */
904
    public boolean dontDeepCopy() {
905
        return false;
906
    }
907
 
908
    // *** rf
909
 
910
    public final synchronized Set<SQLField> getOtherReferentFields() {
911
        this.initRF();
912
        return this.otherRF;
913
    }
914
 
915
    public final synchronized Set<SQLField> getChildrenReferentFields() {
916
        this.initChildRF();
917
        return this.childRF;
918
    }
919
 
920
    /**
921
     * The private foreign fields pointing to this table. Eg if this is OBSERVATION,
922
     * {SOURCE.ID_OBS1, SOURCE.ID_OBS2, CPI.ID_OBS, LOCAL.ID_OBS} ; if this is LOCAL, {}.
923
     *
924
     * @return the private foreign fields pointing to this table.
925
     */
926
    public final synchronized Set<SQLField> getPrivateParentReferentFields() {
927
        this.initRF();
928
        return this.privateParentRF;
929
    }
930
 
931
    /**
932
     * Specify the tables whose rows are contained in rows of this element. They can be specified
933
     * with table names, in which case there must be exactly one foreign field from the specified
934
     * table to this element (eg "BATIMENT" for element SITE). Otherwise it must the fullname of
935
     * foreign field which points to the table of this element (eg "RECEPTEUR.ID_LOCAL").
936
     *
937
     * @return a Set of String.
938
     * @see #getParentFFName()
939
     */
940
    protected Set<String> getChildren() {
941
        return Collections.emptySet();
942
    }
943
 
944
    // *** ff
945
 
946
    public final synchronized Set<String> getNormalForeignFields() {
947
        this.initFF();
948
        return this.normalFF;
949
    }
950
 
951
    public final synchronized Set<String> getSharedForeignFields() {
952
        this.initFF();
953
        return this.sharedFF;
954
    }
955
 
81 ilm 956
    public final SQLField getParentForeignField() {
957
        return getOptionalField(this.getParentForeignFieldName());
958
    }
959
 
960
    public final synchronized String getParentForeignFieldName() {
17 ilm 961
        this.initFF();
962
        return this.parentFF;
963
    }
964
 
965
    private final SQLField getParentFF() {
81 ilm 966
        return getOptionalField(getParentFFName());
967
    }
968
 
969
    // optional but if specified it must exist
970
    private final SQLField getOptionalField(final String name) {
17 ilm 971
        return name == null ? null : this.getTable().getField(name);
972
    }
973
 
974
    /**
975
     * Should be overloaded to specify our parent. NOTE the relationship must be specified only once
976
     * either with this method or with {@link #getChildren()}. This method is preferred since it
977
     * avoids writing IFs to account for customer differences and there's no ambiguity (you return a
978
     * field of this table instead of a table name that must be searched in roots and then a foreign
979
     * key must be found).
980
     *
981
     * @return <code>null</code> for this implementation.
982
     * @see #getChildren()
983
     */
984
    protected String getParentFFName() {
985
        return null;
986
    }
987
 
988
    public final SQLElement getParentElement() {
81 ilm 989
        if (this.getParentForeignFieldName() == null)
17 ilm 990
            return null;
991
        else
81 ilm 992
            return this.getForeignElement(this.getParentForeignFieldName());
17 ilm 993
    }
994
 
995
    private final synchronized Map<String, SQLElement> getPrivateFF() {
996
        this.initFF();
997
        return this.privateFF;
998
    }
999
 
1000
    /**
1001
     * The fields that private to this table, ie rows pointed by these fields are referenced only by
1002
     * one row of this table.
1003
     *
1004
     * @return private fields of this element.
1005
     */
1006
    public final Set<String> getPrivateForeignFields() {
1007
        return Collections.unmodifiableSet(this.getPrivateFF().keySet());
1008
    }
1009
 
1010
    public final SQLElement getPrivateElement(String foreignField) {
1011
        return this.getPrivateFF().get(foreignField);
1012
    }
1013
 
1014
    /**
83 ilm 1015
     * The graph of this table and its privates.
17 ilm 1016
     *
83 ilm 1017
     * @return an SQLRowValues of this element's table filled with
1018
     *         {@link SQLRowValues#setAllToNull() <code>null</code>s} except for private foreign
1019
     *         fields containing SQLRowValues.
17 ilm 1020
     */
1021
    public final SQLRowValues getPrivateGraph() {
83 ilm 1022
        return this.getPrivateGraph(null);
1023
    }
1024
 
1025
    /**
1026
     * The graph of this table and its privates.
1027
     *
1028
     * @param fields which fields should be included in the graph, <code>null</code> meaning all.
1029
     * @return an SQLRowValues of this element's table filled with <code>null</code>s according to
1030
     *         the <code>fields</code> parameter except for private foreign fields containing
1031
     *         SQLRowValues.
1032
     */
1033
    public final SQLRowValues getPrivateGraph(final Set<VirtualFields> fields) {
17 ilm 1034
        final SQLRowValues res = new SQLRowValues(this.getTable());
83 ilm 1035
        if (fields == null) {
1036
            res.setAllToNull();
1037
        } else {
1038
            for (final VirtualFields vf : fields) {
1039
                for (final SQLField f : this.getTable().getFields(vf))
1040
                    res.put(f.getName(), null);
1041
            }
1042
        }
17 ilm 1043
        for (final Entry<String, SQLElement> e : this.getPrivateFF().entrySet()) {
83 ilm 1044
            res.put(e.getKey(), e.getValue().getPrivateGraph(fields));
17 ilm 1045
        }
1046
        return res;
1047
    }
1048
 
1049
    /**
1050
     * Renvoie les champs qui sont 'privé' càd que les ligne pointées par ce champ ne sont
1051
     * référencées que par une et une seule ligne de cette table. Cette implementation renvoie une
1052
     * liste vide. This method is intented for subclasses, call {@link #getPrivateForeignFields()}
1053
     * which does some checks.
1054
     *
1055
     * @return la List des noms des champs privés, eg ["ID_OBSERVATION_2"].
1056
     */
1057
    protected List<String> getPrivateFields() {
1058
        return Collections.emptyList();
1059
    }
1060
 
1061
    public final void clearPrivateFields(SQLRowValues rowVals) {
1062
        for (String s : getPrivateFF().keySet()) {
1063
            rowVals.remove(s);
1064
        }
1065
    }
1066
 
1067
    final Map<String, ReferenceAction> getActions() {
1068
        this.initFF();
1069
        return this.actions;
1070
    }
1071
 
1072
    /**
19 ilm 1073
     * Specify an action for a normal foreign field.
17 ilm 1074
     *
1075
     * @param ff the foreign field name.
1076
     * @param action what to do if a referenced row must be archived.
1077
     * @throws IllegalArgumentException if <code>ff</code> is not a normal foreign field.
1078
     */
1079
    public final void setAction(final String ff, ReferenceAction action) throws IllegalArgumentException {
1080
        // shared must be RESTRICT, parent at least CASCADE (to avoid child without a parent),
1081
        // normal is free
1082
        if (action.compareTo(ReferenceAction.RESTRICT) < 0 && !this.getNormalForeignFields().contains(ff))
25 ilm 1083
            // getField() checks if the field exists
1084
            throw new IllegalArgumentException(getTable().getField(ff).getSQLName() + " is not a normal foreign field : " + this.getNormalForeignFields());
17 ilm 1085
        this.getActions().put(ff, action);
1086
    }
1087
 
1088
    // *** rf and ff
1089
 
1090
    /**
81 ilm 1091
     * The links towards the parents (either {@link #getParentForeignFieldName()} or
17 ilm 1092
     * {@link #getPrivateParentReferentFields()}) of this element.
1093
     *
1094
     * @return the graph links towards the parents of this element.
1095
     */
1096
    public final Set<Link> getParentsLinks() {
1097
        final Set<SQLField> refFields = this.getPrivateParentReferentFields();
1098
        final Set<Link> res = new HashSet<Link>(refFields.size());
1099
        final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph();
1100
        for (final SQLField refField : refFields)
1101
            res.add(graph.getForeignLink(refField));
81 ilm 1102
        final SQLField parentFF = this.getParentForeignField();
1103
        if (parentFF != null)
1104
            res.add(graph.getForeignLink(parentFF));
17 ilm 1105
        return res;
1106
    }
1107
 
1108
    /**
1109
     * The elements beneath this, ie both children and privates.
1110
     *
1111
     * @return our children elements.
1112
     */
1113
    public final Set<SQLElement> getChildrenElements() {
1114
        final Set<SQLElement> res = new HashSet<SQLElement>();
1115
        res.addAll(this.getPrivateFF().values());
1116
        for (final SQLTable child : new SQLFieldsSet(this.getChildrenReferentFields()).getTables())
1117
            res.add(getElement(child));
1118
        return res;
1119
    }
1120
 
1121
    public final SQLElement getChildElement(final String tableName) {
1122
        final SQLField field = CollectionUtils.getSole(new SQLFieldsSet(this.getChildrenReferentFields()).getFields(tableName));
1123
        if (field == null)
1124
            throw new IllegalStateException("no child table named " + tableName);
1125
        else
1126
            return this.getElement(field.getTable());
1127
    }
1128
 
1129
    /**
1130
     * The tables beneath this.
1131
     *
1132
     * @return our descendants, including this.
1133
     * @see #getChildrenElements()
1134
     */
1135
    public final Set<SQLTable> getDescendantTables() {
1136
        final Set<SQLTable> res = new HashSet<SQLTable>();
1137
        this.getDescendantTables(res);
1138
        return res;
1139
    }
1140
 
1141
    private final void getDescendantTables(Set<SQLTable> res) {
1142
        res.add(this.getTable());
1143
        for (final SQLElement elem : this.getChildrenElements()) {
1144
            res.addAll(elem.getDescendantTables());
1145
        }
1146
    }
1147
 
1148
    // *** request
1149
 
61 ilm 1150
    public final ComboSQLRequest getComboRequest() {
1151
        return getComboRequest(false);
1152
    }
1153
 
1154
    /**
1155
     * Return a combo request for this element.
1156
     *
1157
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1158
     *        return a shared instance.
1159
     * @return a combo request for this.
1160
     */
1161
    public final ComboSQLRequest getComboRequest(final boolean create) {
1162
        if (!create) {
1163
            if (this.combo == null) {
1164
                this.combo = this.createComboRequest();
1165
            }
1166
            return this.combo;
1167
        } else {
1168
            return this.createComboRequest();
17 ilm 1169
        }
1170
    }
1171
 
61 ilm 1172
    protected ComboSQLRequest createComboRequest() {
1173
        return new ComboSQLRequest(this.getTable(), this.getComboFields());
1174
    }
1175
 
73 ilm 1176
    // not all elements need to be displayed in combos so don't make this method abstract
1177
    protected List<String> getComboFields() {
1178
        return this.getListFields();
1179
    }
17 ilm 1180
 
19 ilm 1181
    public final synchronized ListSQLRequest getListRequest() {
17 ilm 1182
        if (this.list == null) {
19 ilm 1183
            this.list = createListRequest();
17 ilm 1184
        }
1185
        return this.list;
1186
    }
1187
 
19 ilm 1188
    protected ListSQLRequest createListRequest() {
1189
        return new ListSQLRequest(this.getTable(), this.getListFields());
1190
    }
1191
 
17 ilm 1192
    public final SQLTableModelSourceOnline getTableSource() {
1193
        return this.getTableSource(!cacheTableSource());
1194
    }
1195
 
1196
    /**
1197
     * Return a table source for this element.
1198
     *
1199
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
1200
     *        return a shared instance.
1201
     * @return a table source for this.
1202
     */
1203
    public final synchronized SQLTableModelSourceOnline getTableSource(final boolean create) {
1204
        if (!create) {
1205
            if (this.tableSrc == null) {
1206
                this.tableSrc = createAndInitTableSource();
1207
            }
1208
            return this.tableSrc;
1209
        } else
1210
            return this.createAndInitTableSource();
1211
    }
1212
 
19 ilm 1213
    public final SQLTableModelSourceOnline createTableSource(final List<String> fields) {
1214
        return initTableSource(new SQLTableModelSourceOnline(new ListSQLRequest(this.getTable(), fields)));
1215
    }
1216
 
1217
    public final SQLTableModelSourceOnline createTableSource(final Where w) {
1218
        final SQLTableModelSourceOnline res = this.getTableSource(true);
1219
        res.getReq().setWhere(w);
1220
        return res;
1221
    }
1222
 
17 ilm 1223
    private final SQLTableModelSourceOnline createAndInitTableSource() {
1224
        final SQLTableModelSourceOnline res = this.createTableSource();
25 ilm 1225
        res.getColumns().addAll(this.additionalListCols);
19 ilm 1226
        return initTableSource(res);
1227
    }
1228
 
1229
    protected synchronized void _initTableSource(final SQLTableModelSourceOnline res) {
1230
    }
1231
 
1232
    public final synchronized SQLTableModelSourceOnline initTableSource(final SQLTableModelSourceOnline res) {
1233
        // do init first since it can modify the columns
1234
        this._initTableSource(res);
17 ilm 1235
        // setEditable(false) on read only fields
1236
        // MAYBE setReadOnlyFields() on SQLTableModelSource, so that SQLTableModelLinesSource can
1237
        // check in commit()
1238
        final Set<String> dontModif = CollectionUtils.union(this.getReadOnlyFields(), this.getInsertOnlyFields());
1239
        for (final String f : dontModif)
1240
            for (final SQLTableModelColumn col : res.getColumns(getTable().getField(f)))
1241
                if (col instanceof SQLTableModelColumnPath)
1242
                    ((SQLTableModelColumnPath) col).setEditable(false);
1243
        return res;
1244
    }
1245
 
1246
    protected SQLTableModelSourceOnline createTableSource() {
19 ilm 1247
        // also create a new ListSQLRequest, otherwise it's a backdoor to change the behaviour of
1248
        // the new TableModelSource
1249
        return new SQLTableModelSourceOnline(this.createListRequest());
17 ilm 1250
    }
1251
 
1252
    /**
1253
     * Whether to cache our tableSource.
1254
     *
1255
     * @return <code>true</code> to call {@link #createTableSource()} only once, or
1256
     *         <code>false</code> to call it each time {@link #getTableSource()} is.
1257
     */
1258
    protected boolean cacheTableSource() {
1259
        return true;
1260
    }
1261
 
1262
    abstract protected List<String> getListFields();
1263
 
25 ilm 1264
    public final void addListFields(final List<String> fields) {
1265
        for (final String f : fields)
1266
            this.addListColumn(new SQLTableModelColumnPath(getTable().getField(f)));
1267
    }
1268
 
1269
    public final void addListColumn(SQLTableModelColumn col) {
1270
        this.additionalListCols.add(col);
1271
    }
1272
 
21 ilm 1273
    public final Collection<IListeAction> getRowActions() {
19 ilm 1274
        return this.rowActions;
1275
    }
1276
 
21 ilm 1277
    public final void addRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
19 ilm 1278
        this.rowActions.getRecipe().addListener(listener);
1279
    }
1280
 
21 ilm 1281
    public final void removeRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
19 ilm 1282
        this.rowActions.getRecipe().rmListener(listener);
1283
    }
1284
 
17 ilm 1285
    public String getDescription(SQLRow fromRow) {
1286
        return fromRow.toString();
1287
    }
1288
 
1289
    // *** iterators
1290
 
1291
    static interface ChildProcessor<R extends SQLRowAccessor> {
1292
        public void process(R parent, SQLField joint, R child) throws SQLException;
1293
    }
1294
 
1295
    /**
1296
     * Execute <code>c</code> for each children of <code>row</code>. NOTE: <code>c</code> will be
1297
     * called with <code>row</code> as its first parameter, and with its child of the same type
1298
     * (SQLRow or SQLRowValues) for the third parameter.
1299
     *
1300
     * @param <R> type of SQLRowAccessor to use.
1301
     * @param row the parent row.
1302
     * @param c what to do for each children.
1303
     * @param deep <code>true</code> to ignore {@link #dontDeepCopy()}.
1304
     * @param archived <code>true</code> to iterate over archived children.
1305
     * @throws SQLException if <code>c</code> raises an exn.
1306
     */
1307
    private <R extends SQLRowAccessor> void forChildrenDo(R row, ChildProcessor<? super R> c, boolean deep, boolean archived) throws SQLException {
1308
        for (final SQLField childField : this.getChildrenReferentFields()) {
1309
            if (deep || !this.getElement(childField.getTable()).dontDeepCopy()) {
1310
                final List<SQLRow> children = row.asRow().getReferentRows(childField, archived ? SQLSelect.ARCHIVED : SQLSelect.UNARCHIVED);
1311
                // eg BATIMENT[516]
1312
                for (final SQLRow child : children) {
1313
                    c.process(row, childField, convert(child, row));
1314
                }
1315
            }
1316
        }
1317
    }
1318
 
1319
    // convert toConv to same type as row
1320
    @SuppressWarnings("unchecked")
1321
    private <R extends SQLRowAccessor> R convert(final SQLRow toConv, R row) {
1322
        final R ch;
1323
        if (row instanceof SQLRow)
1324
            ch = (R) toConv;
1325
        else if (row instanceof SQLRowValues)
1326
            ch = (R) toConv.createUpdateRow();
1327
        else
1328
            throw new IllegalStateException("SQLRowAccessor is neither SQLRow nor SQLRowValues: " + toConv);
1329
        return ch;
1330
    }
1331
 
1332
    // first the leaves
1333
    private void forDescendantsDo(final SQLRow row, final ChildProcessor<SQLRow> c, final boolean deep) throws SQLException {
1334
        this.forDescendantsDo(row, c, deep, true, false);
1335
    }
1336
 
1337
    <R extends SQLRowAccessor> void forDescendantsDo(final R row, final ChildProcessor<R> c, final boolean deep, final boolean leavesFirst, final boolean archived) throws SQLException {
1338
        this.check(row);
1339
        this.forChildrenDo(row, new ChildProcessor<R>() {
1340
            public void process(R parent, SQLField joint, R child) throws SQLException {
1341
                if (!leavesFirst)
1342
                    c.process(parent, joint, child);
1343
                getElement(child.getTable()).forDescendantsDo(child, c, deep, leavesFirst, archived);
1344
                if (leavesFirst)
1345
                    c.process(parent, joint, child);
1346
            }
1347
        }, deep, archived);
1348
    }
1349
 
1350
    void check(SQLRowAccessor row) {
1351
        if (!row.getTable().equals(this.getTable()))
1352
            throw new IllegalArgumentException("row must of table " + this.getTable() + " : " + row);
1353
    }
1354
 
1355
    private void checkUndefined(SQLRow row) {
1356
        this.check(row);
1357
        if (row.isUndefined())
1358
            throw new IllegalArgumentException("row is undefined: " + row);
1359
    }
1360
 
1361
    // *** copy
1362
 
1363
    public final SQLRow copyRecursive(int id) throws SQLException {
1364
        return this.copyRecursive(this.getTable().getRow(id));
1365
    }
1366
 
1367
    public final SQLRow copyRecursive(SQLRow row) throws SQLException {
1368
        return this.copyRecursive(row, null);
1369
    }
1370
 
1371
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent) throws SQLException {
1372
        return this.copyRecursive(row, parent, null);
1373
    }
1374
 
1375
    /**
1376
     * Copy <code>row</code> and its children into <code>parent</code>.
1377
     *
1378
     * @param row which row to clone.
1379
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
1380
     *        <code>row</code>.
1381
     * @param c allow one to modify the copied rows before they are inserted, can be
1382
     *        <code>null</code>.
1383
     * @return the new copy.
1384
     * @throws SQLException if an error occurs.
1385
     */
1386
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
83 ilm 1387
        return copyRecursive(row, false, parent, c);
1388
    }
1389
 
1390
    /**
1391
     * Copy <code>row</code> and its children into <code>parent</code>.
1392
     *
1393
     * @param row which row to clone.
1394
     * @param full <code>true</code> if {@link #dontDeepCopy()} should be ignored, i.e. an exact
1395
     *        copy will be made.
1396
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
1397
     *        <code>row</code>.
1398
     * @param c allow one to modify the copied rows before they are inserted, can be
1399
     *        <code>null</code>.
1400
     * @return the new copy.
1401
     * @throws SQLException if an error occurs.
1402
     */
1403
    public SQLRow copyRecursive(final SQLRow row, final boolean full, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
17 ilm 1404
        check(row);
1405
        if (row.isUndefined())
1406
            return row;
1407
 
1408
        // current => new copy
1409
        final Map<SQLRow, SQLRowValues> copies = new HashMap<SQLRow, SQLRowValues>();
1410
 
1411
        return SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<SQLRow>() {
1412
            @Override
1413
            public SQLRow create() throws SQLException {
1414
 
1415
                // eg SITE[128]
83 ilm 1416
                final SQLRowValues copy = createTransformedCopy(row, full, parent, c);
17 ilm 1417
                copies.put(row, copy);
1418
 
1419
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1420
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
1421
                        final SQLRowValues parentCopy = copies.get(parent);
1422
                        if (parentCopy == null)
1423
                            throw new IllegalStateException("null copy of " + parent);
83 ilm 1424
                        final SQLRowValues descCopy = createTransformedCopy(desc, full, null, c);
17 ilm 1425
                        descCopy.put(joint.getName(), parentCopy);
1426
                        copies.put(desc, descCopy);
1427
                    }
83 ilm 1428
                }, full, false, false);
17 ilm 1429
                // ne pas descendre en deep
1430
 
1431
                // reference
1432
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1433
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
83 ilm 1434
                        final Map<SQLField, List<SQLRow>> normalReferents = getElement(desc.getTable()).getNonChildrenReferents(desc);
1435
                        for (final Entry<SQLField, List<SQLRow>> e : normalReferents.entrySet()) {
17 ilm 1436
                            // eg SOURCE.ID_CPI
1437
                            final SQLField refField = e.getKey();
1438
                            for (final SQLRow ref : e.getValue()) {
1439
                                // eg copy of SOURCE[12] is SOURCE[354]
1440
                                final SQLRowValues refCopy = copies.get(ref);
1441
                                if (refCopy != null) {
1442
                                    // CPI[1203]
1443
                                    final SQLRowValues referencedCopy = copies.get(desc);
1444
                                    refCopy.put(refField.getName(), referencedCopy);
1445
                                }
1446
                            }
1447
                        }
1448
                    }
83 ilm 1449
                }, full);
17 ilm 1450
 
1451
                // we used to remove foreign links pointing outside the copy, but this was almost
1452
                // never right, e.g. : copy a batiment, its locals loose ID_FAMILLE ; copy a local,
1453
                // if a source in it points to an item in another local, its copy won't.
1454
 
1455
                return copy.insert();
1456
            }
1457
        });
1458
    }
1459
 
83 ilm 1460
    private final SQLRowValues createTransformedCopy(SQLRow desc, final boolean full, SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
1461
        final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, full, parent);
17 ilm 1462
        assert copiedVals != null : "failed to copy " + desc;
1463
        if (c != null)
1464
            c.executeChecked(copiedVals);
1465
        return copiedVals;
1466
    }
1467
 
1468
    public final SQLRow copy(int id) throws SQLException {
1469
        return this.copy(this.getTable().getRow(id));
1470
    }
1471
 
1472
    public final SQLRow copy(SQLRow row) throws SQLException {
1473
        return this.copy(row, null);
1474
    }
1475
 
1476
    public final SQLRow copy(SQLRow row, SQLRow parent) throws SQLException {
1477
        final SQLRowValues copy = this.createCopy(row, parent);
1478
        return copy == null ? row : copy.insert();
1479
    }
1480
 
1481
    public final SQLRowValues createCopy(int id) {
1482
        final SQLRow row = this.getTable().getRow(id);
1483
        return this.createCopy(row, null);
1484
    }
1485
 
1486
    /**
1487
     * Copies the passed row into an SQLRowValues. NOTE: this method does not access the DB, ie the
1488
     * copy won't be a copy of the current values in DB, but of the current values of the passed
1489
     * instance.
1490
     *
1491
     * @param row the row to copy, can be <code>null</code>.
1492
     * @param parent the parent the copy will be in, <code>null</code> meaning the same as
1493
     *        <code>row</code>.
1494
     * @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
1495
     *         copied.
1496
     */
83 ilm 1497
    public SQLRowValues createCopy(SQLRowAccessor row, SQLRow parent) {
1498
        return createCopy(row, false, parent);
1499
    }
1500
 
1501
    public SQLRowValues createCopy(SQLRowAccessor row, final boolean full, SQLRow parent) {
17 ilm 1502
        // do NOT copy the undefined
1503
        if (row == null || row.isUndefined())
1504
            return null;
1505
        this.check(row);
1506
 
1507
        final SQLRowValues copy = new SQLRowValues(this.getTable());
83 ilm 1508
        this.loadAllSafe(copy, row.asRow());
17 ilm 1509
 
19 ilm 1510
        for (final String privateName : this.getPrivateForeignFields()) {
17 ilm 1511
            final SQLElement privateElement = this.getPrivateElement(privateName);
83 ilm 1512
            final boolean deepCopy = full || !privateElement.dontDeepCopy();
1513
            if (deepCopy && !row.isForeignEmpty(privateName)) {
1514
                final SQLRowValues child = privateElement.createCopy(row.getForeign(privateName), full, null);
17 ilm 1515
                copy.put(privateName, child);
1516
            } else {
1517
                copy.putEmptyLink(privateName);
1518
            }
1519
        }
1520
        // si on a spécifié un parent, eg BATIMENT[23]
1521
        if (parent != null) {
81 ilm 1522
            final SQLTable foreignTable = this.getParentForeignField().getForeignTable();
17 ilm 1523
            if (!parent.getTable().equals(foreignTable))
1524
                throw new IllegalArgumentException(parent + " is not a parent of " + row);
81 ilm 1525
            copy.put(this.getParentForeignFieldName(), parent.getID());
17 ilm 1526
        }
1527
 
1528
        return copy;
1529
    }
1530
 
73 ilm 1531
    /**
1532
     * Load all values that can be safely copied (shared by multiple rows). This means all values
1533
     * except private, primary, order and archive.
1534
     *
1535
     * @param vals the row to modify.
1536
     * @param row the row to be loaded.
1537
     */
1538
    public final void loadAllSafe(final SQLRowValues vals, final SQLRow row) {
1539
        check(vals);
1540
        check(row);
1541
        vals.setAll(row.getAllValues());
1542
        vals.load(row, this.getNormalForeignFields());
81 ilm 1543
        if (this.getParentForeignFieldName() != null)
1544
            vals.put(this.getParentForeignFieldName(), row.getObject(this.getParentForeignFieldName()));
73 ilm 1545
        vals.load(row, this.getSharedForeignFields());
1546
    }
1547
 
17 ilm 1548
    // *** getRows
1549
 
1550
    /**
1551
     * Returns the descendant rows : the children of this element, recursively. ATTN does not carry
1552
     * the hierarchy.
1553
     *
1554
     * @param row a SQLRow.
1555
     * @return the descendant rows by SQLTable.
1556
     */
83 ilm 1557
    public final ListMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
17 ilm 1558
        check(row);
83 ilm 1559
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
17 ilm 1560
        try {
1561
            this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1562
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
83 ilm 1563
                    mm.add(joint.getTable(), child);
17 ilm 1564
                }
1565
            }, true);
1566
        } catch (SQLException e) {
1567
            // never happen
1568
            e.printStackTrace();
1569
        }
1570
        return mm;
1571
    }
1572
 
1573
    /**
83 ilm 1574
     * Returns the tree beneath the passed row.
17 ilm 1575
     *
1576
     * @param row the root of the desired tree.
1577
     * @param archived <code>true</code> if the returned rows should be archived.
83 ilm 1578
     * @return the asked tree.
17 ilm 1579
     */
83 ilm 1580
    private SQLRowValues getTree(SQLRow row, boolean archived) {
17 ilm 1581
        check(row);
83 ilm 1582
        final SQLRowValues res = row.asRowValues();
17 ilm 1583
        try {
83 ilm 1584
            this.forDescendantsDo(res, new ChildProcessor<SQLRowValues>() {
1585
                public void process(SQLRowValues parent, SQLField joint, SQLRowValues desc) throws SQLException {
1586
                    desc.put(joint.getName(), parent);
17 ilm 1587
                }
83 ilm 1588
            }, true, false, archived);
17 ilm 1589
        } catch (SQLException e) {
1590
            // never happen cause process don't throw it
1591
            e.printStackTrace();
1592
        }
83 ilm 1593
        return res;
17 ilm 1594
    }
1595
 
1596
    /**
1597
     * Returns the children of the passed row.
1598
     *
1599
     * @param row a SQLRow.
1600
     * @return the children rows by SQLTable.
1601
     */
83 ilm 1602
    public ListMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
17 ilm 1603
        check(row);
83 ilm 1604
        // List to retain order
1605
        final ListMap<SQLTable, SQLRow> mm = new ListMap<SQLTable, SQLRow>();
17 ilm 1606
        try {
1607
            this.forChildrenDo(row, new ChildProcessor<SQLRow>() {
1608
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
83 ilm 1609
                    mm.add(child.getTable(), child);
17 ilm 1610
                }
1611
            }, true, false);
1612
        } catch (SQLException e) {
1613
            // never happen
1614
            e.printStackTrace();
1615
        }
1616
        return mm;
1617
    }
1618
 
67 ilm 1619
    public SQLRowAccessor getParent(SQLRowAccessor row) {
1620
        check(row);
1621
        final List<SQLRowAccessor> parents = new ArrayList<SQLRowAccessor>();
1622
        for (final Link l : this.getParentsLinks()) {
1623
            parents.addAll(row.followLink(l));
1624
        }
1625
        if (parents.size() > 1)
1626
            throw new IllegalStateException("More than one parent for " + row + " : " + parents);
1627
        return parents.size() == 0 ? null : parents.get(0);
17 ilm 1628
    }
1629
 
67 ilm 1630
    public SQLRow getForeignParent(SQLRow row) {
1631
        return this.getForeignParent(row, SQLRowMode.VALID);
1632
    }
1633
 
1634
    // ATTN cannot replace with getParent(SQLRowAccessor) since some callers assume the result to be
1635
    // a foreign row (which isn't the case for private)
1636
    private SQLRow getForeignParent(SQLRow row, final SQLRowMode mode) {
17 ilm 1637
        check(row);
81 ilm 1638
        return this.getParentForeignFieldName() == null ? null : row.getForeignRow(this.getParentForeignFieldName(), mode);
17 ilm 1639
    }
1640
 
83 ilm 1641
    public final SQLRowValues getPrivateParent(final SQLRowAccessor row, final boolean modifyParameter) {
1642
        return this.getPrivateParent(row, modifyParameter, ArchiveMode.UNARCHIVED);
1643
    }
1644
 
1645
    /**
1646
     * Return the parent if any of the passed row. This method will access the DB.
1647
     *
1648
     * @param row the row.
1649
     * @param modifyParameter <code>true</code> if <code>row</code> can be linked to the result,
1650
     *        <code>false</code> to link a new {@link SQLRowValues}.
1651
     * @param archiveMode the parent must match this mode.
1652
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
1653
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
1654
     * @throws IllegalStateException if <code>row</code> has more than one parent matching.
1655
     */
1656
    public final SQLRowValues getPrivateParent(final SQLRowAccessor row, final boolean modifyParameter, final ArchiveMode archiveMode) {
17 ilm 1657
        check(row);
83 ilm 1658
        final List<SQLField> refFields = new ArrayList<SQLField>(this.getPrivateParentReferentFields());
1659
        if (row.isUndefined() || refFields.size() == 0)
1660
            return null;
1661
        final ListIterator<SQLField> listIter = refFields.listIterator();
1662
        final List<String> selects = new ArrayList<String>(refFields.size());
1663
        while (listIter.hasNext()) {
1664
            final SQLField refField = listIter.next();
1665
            final SQLSelect sel = new SQLSelect(true).addSelect(refField.getTable().getKey()).addRawSelect(String.valueOf(listIter.previousIndex()), "fieldIndex");
1666
            sel.setArchivedPolicy(archiveMode);
1667
            sel.setWhere(new Where(refField, "=", row.getIDNumber()));
1668
            selects.add(sel.asString());
1669
        }
1670
        final List<?> parentIDs = getTable().getDBSystemRoot().getDataSource().executeA(CollectionUtils.join(selects, "\nUNION ALL "));
1671
        if (parentIDs.size() > 1)
1672
            throw new IllegalStateException("More than one parent for " + row + " : " + parentIDs);
1673
        else if (parentIDs.size() == 0)
1674
            // e.g. no UNARCHIVED parent of an ARCHIVED private
1675
            return null;
1676
 
1677
        final Object[] idAndIndex = (Object[]) parentIDs.get(0);
1678
        final SQLField refField = refFields.get(((Number) idAndIndex[1]).intValue());
1679
        final SQLRowValues res = new SQLRowValues(refField.getTable()).setID((Number) idAndIndex[0]);
1680
        // first convert to SQLRow to avoid modifying the (graph of our) method parameter
1681
        res.put(refField.getName(), (modifyParameter ? row : row.asRow()).asRowValues());
1682
        return res;
1683
    }
1684
 
1685
    /**
1686
     * Return the main row if any of the passed row. This method will access the DB.
1687
     *
1688
     * @param row the row, if it's a {@link SQLRowValues} it will be linked to the result.
1689
     * @param archiveMode the parent must match this mode.
1690
     * @return the matching parent linked to its child, <code>null</code> if <code>row</code>
1691
     *         {@link SQLRowAccessor#isUndefined()}, if this isn't a private or if no parent exist.
1692
     * @see #getPrivateParent(SQLRowAccessor, boolean, ArchiveMode)
1693
     */
1694
    public final SQLRowValues getPrivateRoot(SQLRowAccessor row, final ArchiveMode archiveMode) {
1695
        SQLRowValues prev = null;
1696
        SQLRowValues res = getPrivateParent(row, true, archiveMode);
1697
        while (res != null) {
1698
            prev = res;
1699
            res = getElement(res.getTable()).getPrivateParent(res, true, archiveMode);
1700
        }
1701
        return prev;
1702
    }
1703
 
1704
    Map<SQLField, List<SQLRow>> getNonChildrenReferents(SQLRow row) {
1705
        check(row);
1706
        final Map<SQLField, List<SQLRow>> mm = new HashMap<SQLField, List<SQLRow>>();
17 ilm 1707
        final Set<SQLField> nonChildren = new HashSet<SQLField>(row.getTable().getDBSystemRoot().getGraph().getReferentKeys(row.getTable()));
1708
        nonChildren.removeAll(this.getChildrenReferentFields());
1709
        for (final SQLField refField : nonChildren) {
1710
            // eg CONTACT.ID_SITE => [CONTACT[12], CONTACT[13]]
83 ilm 1711
            mm.put(refField, row.getReferentRows(refField));
17 ilm 1712
        }
1713
        return mm;
1714
    }
1715
 
1716
    public Map<String, SQLRow> getNormalForeigns(SQLRow row) {
1717
        return this.getNormalForeigns(row, SQLRowMode.DEFINED);
1718
    }
1719
 
1720
    private Map<String, SQLRow> getNormalForeigns(SQLRow row, final SQLRowMode mode) {
1721
        check(row);
1722
        final Map<String, SQLRow> mm = new HashMap<String, SQLRow>();
19 ilm 1723
        final Iterator<String> iter = this.getNormalForeignFields().iterator();
17 ilm 1724
        while (iter.hasNext()) {
1725
            // eg SOURCE.ID_CPI
19 ilm 1726
            final String ff = iter.next();
17 ilm 1727
            // eg CPI[12]
1728
            final SQLRow foreignRow = row.getForeignRow(ff, mode);
1729
            if (foreignRow != null)
1730
                mm.put(ff, foreignRow);
1731
        }
1732
        return mm;
1733
    }
1734
 
1735
    /**
1736
     * Returns a java object modeling the passed row.
1737
     *
1738
     * @param row the row to model.
1739
     * @return an instance modeling the passed row or <code>null</code> if there's no class to model
1740
     *         this table.
1741
     * @see SQLRowAccessor#getModelObject()
1742
     */
1743
    public Object getModelObject(SQLRowAccessor row) {
1744
        check(row);
1745
        if (this.getModelClass() == null)
1746
            return null;
1747
 
1748
        final Object res;
1749
        // seuls les SQLRow peuvent être cachées
1750
        if (row instanceof SQLRow) {
1751
            // MAYBE make the modelObject change
1752
            final CacheResult<Object> cached = this.getModelCache().check(row);
1753
            if (cached.getState() == CacheResult.State.NOT_IN_CACHE) {
1754
                res = this.createModelObject(row);
1755
                this.getModelCache().put(row, res, Collections.singleton(row));
1756
            } else
1757
                res = cached.getRes();
1758
        } else
1759
            res = this.createModelObject(row);
1760
 
1761
        return res;
1762
    }
1763
 
1764
    private final Object createModelObject(SQLRowAccessor row) {
1765
        if (!RowBacked.class.isAssignableFrom(this.getModelClass()))
1766
            throw new IllegalStateException("modelClass must inherit from RowBacked: " + this.getModelClass());
19 ilm 1767
        final Constructor<? extends RowBacked> ctor;
17 ilm 1768
        try {
1769
            ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class });
1770
        } catch (Exception e) {
1771
            throw ExceptionUtils.createExn(IllegalStateException.class, "no SQLRowAccessor constructor", e);
1772
        }
1773
        try {
1774
            return ctor.newInstance(new Object[] { row });
1775
        } catch (Exception e) {
1776
            throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e);
1777
        }
1778
    }
1779
 
1780
    protected Class<? extends RowBacked> getModelClass() {
1781
        return null;
1782
    }
1783
 
1784
    // *** equals
1785
 
1786
    public boolean equals(SQLRow row, SQLRow row2) throws SQLException {
1787
        check(row);
1788
        if (!row2.getTable().equals(this.getTable()))
1789
            return false;
1790
        if (row.equals(row2))
1791
            return true;
1792
        // the same table but not the same id
1793
 
1794
        if (!row.getAllValues().equals(row2.getAllValues()))
1795
            return false;
1796
 
1797
        // shared doivent être partagées (!)
1798
        for (final String shared : this.getSharedForeignFields()) {
1799
            if (row.getInt(shared) != row2.getInt(shared))
1800
                return false;
1801
        }
1802
 
1803
        // les private equals
19 ilm 1804
        for (final String prvt : this.getPrivateForeignFields()) {
17 ilm 1805
            final SQLElement foreignElement = this.getForeignElement(prvt);
1806
            // ne pas tester
1807
            if (!foreignElement.dontDeepCopy() && !foreignElement.equals(row.getForeignRow(prvt), row2.getForeignRow(prvt)))
1808
                return false;
1809
        }
1810
 
1811
        return true;
1812
    }
1813
 
1814
    public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException {
1815
        // if (!equals(row, row2))
1816
        // return false;
1817
        return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2));
1818
    }
1819
 
73 ilm 1820
    @Override
17 ilm 1821
    public final boolean equals(Object obj) {
83 ilm 1822
        if (obj == this)
1823
            return true;
17 ilm 1824
        if (obj instanceof SQLElement) {
1825
            final SQLElement o = (SQLElement) obj;
73 ilm 1826
            // don't need to compare SQLField computed by initFF() & initRF() since they're function
1827
            // of this.table (and by extension its graph) & this.directory
1828
            final boolean parentEq = CompareUtils.equals(this.getParentFFName(), o.getParentFFName());
1829
            return this.getTable().equals(o.getTable()) && CompareUtils.equals(this.getDirectory(), o.getDirectory()) && parentEq && this.getPrivateFields().equals(o.getPrivateFields())
1830
                    && this.getChildren().equals(o.getChildren());
1831
        } else {
17 ilm 1832
            return false;
73 ilm 1833
        }
17 ilm 1834
    }
1835
 
73 ilm 1836
    @Override
80 ilm 1837
    public synchronized int hashCode() {
73 ilm 1838
        final int prime = 31;
1839
        int result = 1;
1840
        result = prime * result + this.primaryTable.hashCode();
1841
        result = prime * result + ((this.directory == null) ? 0 : this.directory.hashCode());
1842
        return result;
17 ilm 1843
    }
1844
 
1845
    @Override
1846
    public String toString() {
73 ilm 1847
        return this.getClass().getName() + " " + this.getTable().getSQLName();
17 ilm 1848
    }
1849
 
1850
    // *** gui
1851
 
27 ilm 1852
    public final void addComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
1853
        if (t == null)
1854
            throw new NullPointerException();
83 ilm 1855
        this.components.add(id, t);
27 ilm 1856
    }
1857
 
1858
    public final void removeComponentFactory(final String id, final ITransformer<Tuple2<SQLElement, String>, SQLComponent> t) {
1859
        if (t == null)
1860
            throw new NullPointerException();
1861
        this.components.remove(id, t);
1862
    }
1863
 
83 ilm 1864
    private final SQLComponent createComponentFromFactory(final String id, final boolean defaultItem) {
41 ilm 1865
        final String actualID = defaultItem ? DEFAULT_COMP_ID : id;
27 ilm 1866
        final Tuple2<SQLElement, String> t = Tuple2.create(this, id);
1867
        // start from the most recently added factory
83 ilm 1868
        final Iterator<ITransformer<Tuple2<SQLElement, String>, SQLComponent>> iter = this.components.getNonNull(actualID).descendingIterator();
27 ilm 1869
        while (iter.hasNext()) {
1870
            final SQLComponent res = iter.next().transformChecked(t);
1871
            if (res != null)
1872
                return res;
1873
        }
1874
        return null;
1875
    }
1876
 
1877
    public final SQLComponent createDefaultComponent() {
41 ilm 1878
        return this.createComponent(DEFAULT_COMP_ID);
27 ilm 1879
    }
1880
 
17 ilm 1881
    /**
27 ilm 1882
     * Create the component for the passed ID. First factories for the passed ID are executed, after
41 ilm 1883
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
1884
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
27 ilm 1885
     *
1886
     * @param id the requested ID.
83 ilm 1887
     * @return the component, never <code>null</code>.
1888
     * @throws IllegalStateException if no component is found.
27 ilm 1889
     */
83 ilm 1890
    public final SQLComponent createComponent(final String id) throws IllegalStateException {
1891
        return this.createComponent(id, true);
1892
    }
1893
 
1894
    /**
1895
     * Create the component for the passed ID. First factories for the passed ID are executed, after
1896
     * that if ID is the {@link #DEFAULT_COMP_ID default} then {@link #createComponent()} is called
1897
     * else factories for {@link #DEFAULT_COMP_ID} are executed.
1898
     *
1899
     * @param id the requested ID.
1900
     * @param required <code>true</code> if the result cannot be <code>null</code>.
1901
     * @return the component or <code>null</code> if all factories return <code>null</code> and
1902
     *         <code>required</code> is <code>false</code>.
1903
     * @throws IllegalStateException if <code>required</code> and no component is found.
1904
     */
1905
    public final SQLComponent createComponent(final String id, final boolean required) throws IllegalStateException {
1906
        SQLComponent res = this.createComponentFromFactory(id, false);
41 ilm 1907
        if (res == null) {
1908
            if (CompareUtils.equals(id, DEFAULT_COMP_ID)) {
1909
                // since we don't pass id to this method, only call it for DEFAULT_ID
1910
                res = this.createComponent();
1911
            } else {
83 ilm 1912
                res = this.createComponentFromFactory(id, true);
41 ilm 1913
            }
27 ilm 1914
        }
83 ilm 1915
        if (res != null)
1916
            res.setCode(id);
1917
        else if (required)
1918
            throw new IllegalStateException("No component for " + id);
41 ilm 1919
        return res;
27 ilm 1920
    }
1921
 
1922
    /**
17 ilm 1923
     * Retourne l'interface graphique de saisie.
1924
     *
1925
     * @return l'interface graphique de saisie.
1926
     */
27 ilm 1927
    protected abstract SQLComponent createComponent();
17 ilm 1928
 
73 ilm 1929
    public final void addToMDPath(final String mdVariant) {
1930
        if (mdVariant == null)
1931
            throw new NullPointerException();
1932
        synchronized (this) {
1933
            final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
1934
            newL.addFirst(mdVariant);
1935
            this.mdPath = Collections.unmodifiableList(newL);
1936
        }
57 ilm 1937
    }
1938
 
73 ilm 1939
    public synchronized final void removeFromMDPath(final String mdVariant) {
57 ilm 1940
        final LinkedList<String> newL = new LinkedList<String>(this.mdPath);
1941
        if (newL.remove(mdVariant))
1942
            this.mdPath = Collections.unmodifiableList(newL);
1943
    }
1944
 
80 ilm 1945
    /**
1946
     * The variants searched to find item metadata by
1947
     * {@link SQLFieldTranslator#getDescFor(SQLTable, String, String)}. This allow to configure this
1948
     * element to choose between the simultaneously loaded metadata.
1949
     *
1950
     * @return the variants path.
1951
     */
73 ilm 1952
    public synchronized final List<String> getMDPath() {
57 ilm 1953
        return this.mdPath;
1954
    }
1955
 
19 ilm 1956
    /**
1957
     * Allows a module to add a view for a field to this element.
1958
     *
1959
     * @param field the field of the component.
1960
     * @return <code>true</code> if no view existed.
1961
     */
1962
    public final boolean putAdditionalField(final String field) {
1963
        return this.putAdditionalField(field, (JComponent) null);
1964
    }
1965
 
1966
    public final boolean putAdditionalField(final String field, final JTextComponent comp) {
1967
        return this.putAdditionalField(field, (JComponent) comp);
1968
    }
1969
 
1970
    public final boolean putAdditionalField(final String field, final SQLTextCombo comp) {
1971
        return this.putAdditionalField(field, (JComponent) comp);
1972
    }
1973
 
1974
    // private as only a few JComponent are OK
1975
    private final boolean putAdditionalField(final String field, final JComponent comp) {
1976
        if (this.additionalFields.containsKey(field)) {
1977
            return false;
1978
        } else {
1979
            this.additionalFields.put(field, comp);
1980
            return true;
1981
        }
1982
    }
1983
 
1984
    public final Map<String, JComponent> getAdditionalFields() {
1985
        return Collections.unmodifiableMap(this.additionalFields);
1986
    }
1987
 
1988
    public final void removeAdditionalField(final String field) {
1989
        this.additionalFields.remove(field);
1990
    }
1991
 
17 ilm 1992
    public final boolean askArchive(final Component comp, final Number ids) {
1993
        return this.askArchive(comp, Collections.singleton(ids));
1994
    }
1995
 
1996
    /**
1997
     * Ask to the user before archiving.
1998
     *
1999
     * @param comp the parent component.
2000
     * @param ids which rows to archive.
2001
     * @return <code>true</code> if the rows were successfully archived, <code>false</code>
2002
     *         otherwise.
2003
     */
2004
    public boolean askArchive(final Component comp, final Collection<? extends Number> ids) {
2005
        boolean shouldArchive = false;
73 ilm 2006
        final int rowCount = ids.size();
2007
        if (rowCount == 0)
17 ilm 2008
            return true;
2009
        try {
2010
            if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
2011
                throw new SQLException("forbidden");
2012
            final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
83 ilm 2013
            final Map<SQLTable, List<SQLRowAccessor>> descs = trees.getDescendantsByTable();
17 ilm 2014
            final SortedMap<SQLField, Integer> externRefs = trees.getExternReferencesCount();
73 ilm 2015
            final String confirmDelete = getTM().trA("sqlElement.confirmDelete");
2016
            final Map<String, Object> map = new HashMap<String, Object>();
2017
            map.put("rowCount", rowCount);
2018
            final int descsSize = descs.size();
2019
            final int externsSize = externRefs.size();
2020
            if (descsSize + externsSize > 0) {
2021
                final String descsS = descsSize > 0 ? toString(descs) : null;
2022
                final String externsS = externsSize > 0 ? toStringExtern(externRefs) : null;
2023
                map.put("descsSize", descsSize);
2024
                map.put("descs", descsS);
2025
                map.put("externsSize", externsSize);
2026
                map.put("externs", externsS);
2027
                map.put("times", "once");
2028
                int i = askSerious(comp, getTM().trM("sqlElement.deleteRef.details", map) + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
17 ilm 2029
                if (i == JOptionPane.YES_OPTION) {
73 ilm 2030
                    map.put("times", "twice");
2031
                    final String msg = externsSize > 0 ? getTM().trM("sqlElement.deleteRef.details2", map) : "";
2032
                    i = askSerious(comp, msg + getTM().trM("sqlElement.deleteRef", map), confirmDelete);
17 ilm 2033
                    if (i == JOptionPane.YES_OPTION) {
2034
                        shouldArchive = true;
2035
                    } else {
73 ilm 2036
                        JOptionPane.showMessageDialog(comp, getTM().trA("sqlElement.noLinesDeleted"), getTM().trA("sqlElement.noLinesDeletedTitle"), JOptionPane.INFORMATION_MESSAGE);
17 ilm 2037
                    }
2038
                }
2039
            } else {
73 ilm 2040
                int i = askSerious(comp, getTM().trM("sqlElement.deleteNoRef", map), confirmDelete);
17 ilm 2041
                if (i == JOptionPane.YES_OPTION) {
2042
                    shouldArchive = true;
2043
                }
2044
            }
2045
            if (shouldArchive) {
2046
                this.archive(trees, true);
2047
                return true;
2048
            } else
2049
                return false;
2050
        } catch (SQLException e) {
73 ilm 2051
            ExceptionHandler.handle(comp, TM.tr("sqlElement.archiveError", this, ids), e);
17 ilm 2052
            return false;
2053
        }
2054
    }
2055
 
83 ilm 2056
    private final String toString(Map<SQLTable, List<SQLRowAccessor>> descs) {
2057
        final List<String> l = new ArrayList<String>(descs.size());
2058
        for (final Entry<SQLTable, List<SQLRowAccessor>> e : descs.entrySet()) {
2059
            final SQLTable t = e.getKey();
17 ilm 2060
            final SQLElement elem = getElement(t);
83 ilm 2061
            l.add(elemToString(e.getValue().size(), elem));
17 ilm 2062
        }
2063
        return CollectionUtils.join(l, "\n");
2064
    }
2065
 
2066
    private static final String elemToString(int count, SQLElement elem) {
73 ilm 2067
        return "- " + elem.getName().getNumeralVariant(count, Grammar.INDEFINITE_NUMERAL);
17 ilm 2068
    }
2069
 
2070
    // traduire TRANSFO.ID_ELEMENT_TABLEAU_PRI -> {TRANSFO[5], TRANSFO[12]}
2071
    // en 2 transformateurs vont perdre leurs champs 'Circuit primaire'
2072
    private final String toStringExtern(SortedMap<SQLField, Integer> externRef) {
2073
        final List<String> l = new ArrayList<String>();
73 ilm 2074
        final Map<String, Object> map = new HashMap<String, Object>(4);
17 ilm 2075
        for (final Map.Entry<SQLField, Integer> entry : externRef.entrySet()) {
2076
            final SQLField foreignKey = entry.getKey();
2077
            final int count = entry.getValue();
2078
            final String label = Configuration.getTranslator(foreignKey.getTable()).getLabelFor(foreignKey);
73 ilm 2079
            final SQLElement elem = getElement(foreignKey.getTable());
2080
            map.put("elementName", elem.getName());
2081
            map.put("count", count);
2082
            map.put("linkName", label);
2083
            l.add(getTM().trM("sqlElement.linksWillBeCut", map));
17 ilm 2084
        }
2085
        return CollectionUtils.join(l, "\n");
2086
    }
2087
 
2088
    private final int askSerious(Component comp, String msg, String title) {
2089
        return JOptionPane.showConfirmDialog(comp, msg, title + " (" + this.getPluralName() + ")", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
2090
    }
19 ilm 2091
 
17 ilm 2092
}