OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 21 | Rev 27 | 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
 
16
import org.openconcerto.sql.Configuration;
17
import org.openconcerto.sql.Log;
18
import org.openconcerto.sql.model.DBStructureItemNotFound;
19
import org.openconcerto.sql.model.SQLField;
20
import org.openconcerto.sql.model.SQLFieldsSet;
21
import org.openconcerto.sql.model.SQLRow;
22
import org.openconcerto.sql.model.SQLRowAccessor;
23
import org.openconcerto.sql.model.SQLRowMode;
24
import org.openconcerto.sql.model.SQLRowValues;
25
import org.openconcerto.sql.model.SQLSelect;
26
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
27
import org.openconcerto.sql.model.SQLTable;
19 ilm 28
import org.openconcerto.sql.model.Where;
17 ilm 29
import org.openconcerto.sql.model.graph.DatabaseGraph;
30
import org.openconcerto.sql.model.graph.Link;
31
import org.openconcerto.sql.request.ComboSQLRequest;
32
import org.openconcerto.sql.request.ListSQLRequest;
33
import org.openconcerto.sql.request.SQLCache;
19 ilm 34
import org.openconcerto.sql.sqlobject.SQLTextCombo;
17 ilm 35
import org.openconcerto.sql.users.rights.UserRightsManager;
36
import org.openconcerto.sql.utils.SQLUtils;
37
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
21 ilm 38
import org.openconcerto.sql.view.list.IListeAction;
17 ilm 39
import org.openconcerto.sql.view.list.SQLTableModelColumn;
40
import org.openconcerto.sql.view.list.SQLTableModelColumnPath;
41
import org.openconcerto.sql.view.list.SQLTableModelSourceOnline;
42
import org.openconcerto.utils.CollectionMap;
43
import org.openconcerto.utils.CollectionUtils;
44
import org.openconcerto.utils.CompareUtils;
45
import org.openconcerto.utils.ExceptionHandler;
46
import org.openconcerto.utils.ExceptionUtils;
47
import org.openconcerto.utils.StringUtils;
48
import org.openconcerto.utils.cache.CacheResult;
49
import org.openconcerto.utils.cc.IClosure;
19 ilm 50
import org.openconcerto.utils.change.ListChangeIndex;
51
import org.openconcerto.utils.change.ListChangeRecorder;
17 ilm 52
 
53
import java.awt.Component;
54
import java.lang.reflect.Constructor;
55
import java.sql.SQLException;
56
import java.util.ArrayList;
57
import java.util.Collection;
58
import java.util.Collections;
59
import java.util.HashMap;
60
import java.util.HashSet;
61
import java.util.Iterator;
62
import java.util.List;
63
import java.util.Map;
64
import java.util.Map.Entry;
65
import java.util.Set;
66
import java.util.SortedMap;
67
import java.util.logging.Level;
68
 
19 ilm 69
import javax.swing.JComponent;
17 ilm 70
import javax.swing.JOptionPane;
19 ilm 71
import javax.swing.text.JTextComponent;
17 ilm 72
 
73
import org.apache.commons.collections.MapIterator;
74
import org.apache.commons.collections.MultiMap;
75
import org.apache.commons.collections.iterators.EntrySetMapIterator;
76
 
77
/**
78
 * Décrit comment manipuler un élément de la BD (pas forcément une seule table, voir
79
 * privateForeignField).
80
 *
81
 * @author ilm
82
 */
83
public abstract class SQLElement {
84
 
85
    static final private Set<String> computingFF = Collections.unmodifiableSet(new HashSet<String>());
86
    static final private Set<SQLField> computingRF = Collections.unmodifiableSet(new HashSet<SQLField>());
87
 
88
    // from the most loss of information to the least.
89
    public static enum ReferenceAction {
90
        /** If a referenced row is archived, empty the foreign field */
91
        SET_EMPTY,
92
        /** If a referenced row is archived, archive this row too */
93
        CASCADE,
94
        /** If a referenced row is to be archived, abort the operation */
95
        RESTRICT
96
    }
97
 
98
    // must contain the article "a stone" / "an elephant"
99
    private final String singular;
100
    // no article "stones" / "elephants"
101
    private final String plural;
102
    private final SQLTable primaryTable;
103
    private ComboSQLRequest combo;
104
    private ListSQLRequest list;
105
    private SQLTableModelSourceOnline tableSrc;
21 ilm 106
    private final ListChangeRecorder<IListeAction> rowActions;
17 ilm 107
    // foreign fields
108
    private Set<String> normalFF;
109
    private String parentFF;
110
    private Set<String> sharedFF;
111
    private Map<String, SQLElement> privateFF;
112
    private final Map<String, ReferenceAction> actions;
113
    // referent fields
114
    private Set<SQLField> childRF;
115
    private Set<SQLField> privateParentRF;
116
    private Set<SQLField> otherRF;
117
    // lazy creation
118
    private SQLCache<SQLRowAccessor, Object> modelCache;
119
 
19 ilm 120
    private final Map<String, JComponent> additionalFields;
25 ilm 121
    private final List<SQLTableModelColumn> additionalListCols;
19 ilm 122
 
17 ilm 123
    public SQLElement(String singular, String plural, SQLTable primaryTable) {
124
        super();
125
        this.singular = singular;
126
        this.plural = plural;
127
        if (primaryTable == null) {
128
            throw new DBStructureItemNotFound("table is null for " + this);
129
        }
130
        this.primaryTable = primaryTable;
131
        this.combo = null;
132
        this.list = null;
21 ilm 133
        this.rowActions = new ListChangeRecorder<IListeAction>(new ArrayList<IListeAction>());
19 ilm 134
        this.actions = new HashMap<String, ReferenceAction>();
135
        this.resetRelationships();
17 ilm 136
 
19 ilm 137
        this.modelCache = null;
138
 
139
        this.additionalFields = new HashMap<String, JComponent>();
25 ilm 140
        this.additionalListCols = new ArrayList<SQLTableModelColumn>();
19 ilm 141
    }
142
 
143
    /**
144
     * Must be called if foreign/referent keys are added or removed.
145
     */
146
    public synchronized void resetRelationships() {
17 ilm 147
        this.privateFF = null;
148
        this.parentFF = null;
149
        this.normalFF = null;
150
        this.sharedFF = null;
19 ilm 151
        this.actions.clear();
17 ilm 152
 
153
        this.childRF = null;
154
        this.privateParentRF = null;
155
        this.otherRF = null;
156
    }
157
 
158
    private void checkSelfCall(boolean check, final String methodName) {
159
        assert check : this + " " + methodName + "() is calling itself, and thus the caller will only see a partial state";
160
    }
161
 
162
    private synchronized void initFF() {
163
        checkSelfCall(this.sharedFF != computingFF, "initFF");
164
        if (this.sharedFF != null)
165
            return;
166
        this.sharedFF = computingFF;
167
 
168
        final Set<String> privates = new HashSet<String>(this.getPrivateFields());
169
        this.privateFF = new HashMap<String, SQLElement>(privates.size());
170
        final Set<String> parents = new HashSet<String>();
171
        this.normalFF = new HashSet<String>();
172
        final Set<String> tmpSharedFF = new HashSet<String>();
173
        for (final SQLField ff : this.getTable().getForeignKeys()) {
174
            final String fieldName = ff.getName();
175
            final SQLElement foreignElement = this.getForeignElement(fieldName);
176
            if (privates.contains(fieldName)) {
177
                privates.remove(fieldName);
178
                this.privateFF.put(fieldName, foreignElement);
179
            } else if (foreignElement.isShared()) {
180
                tmpSharedFF.add(fieldName);
181
            } else if (foreignElement.getChildrenReferentFields().contains(ff)) {
182
                parents.add(fieldName);
183
            } else {
184
                this.normalFF.add(fieldName);
185
            }
186
        }
187
        if (parents.size() > 1)
188
            throw new IllegalStateException("for " + this + " more than one parent :" + parents);
189
        this.parentFF = parents.size() == 0 ? null : (String) parents.iterator().next();
190
        if (privates.size() > 0)
191
            throw new IllegalStateException("for " + this + " these private foreign fields are not valid :" + privates);
192
        this.sharedFF = tmpSharedFF;
193
 
194
        // MAYBE move required fields to SQLElement and use RESTRICT
195
        this.actions.put(this.parentFF, ReferenceAction.CASCADE);
196
        for (final String s : this.privateFF.keySet()) {
197
            this.actions.put(s, ReferenceAction.SET_EMPTY);
198
        }
199
        for (final String s : this.normalFF) {
200
            this.actions.put(s, ReferenceAction.SET_EMPTY);
201
        }
202
        for (final String s : this.sharedFF) {
203
            this.actions.put(s, ReferenceAction.RESTRICT);
204
        }
205
        this.ffInited();
206
    }
207
 
208
    protected void ffInited() {
209
        // MAYBE use DELETE_RULE of Link
210
    }
211
 
212
    // convert the list of String of getChildren() to a Set of SQLField pointing to this table
213
    private synchronized Set<SQLField> computeChildrenRF() {
214
        final Set<SQLField> res = new HashSet<SQLField>();
215
        // eg "BATIMENT" or "BATIMENT.ID_SITE"
216
        for (final String child : this.getChildren()) {
217
            // a field from our child to us, eg |BATIMENT.ID_SITE|
218
            final SQLField childField;
219
 
220
            final int comma = child.indexOf(',');
221
            final String tableName = comma < 0 ? child : child.substring(0, comma);
222
            final SQLTable childTable = this.getTable().getTable(tableName);
223
 
224
            if (comma < 0) {
225
                final Set<SQLField> keys = childTable.getForeignKeys(this.getTable());
226
                if (keys.size() != 1)
227
                    throw new IllegalArgumentException("cannot find a foreign from " + child + " to " + this.getTable());
228
                childField = keys.iterator().next();
229
            } else {
230
                childField = childTable.getField(child.substring(comma + 1));
231
                final SQLTable foreignTable = childField.getDBSystemRoot().getGraph().getForeignTable(childField);
232
                if (!foreignTable.equals(this.getTable())) {
233
                    throw new IllegalArgumentException(childField + " doesn't point to " + this.getTable());
234
                }
235
            }
236
            res.add(childField);
237
        }
238
        return res;
239
    }
240
 
241
    private synchronized void initRF() {
242
        checkSelfCall(this.otherRF != computingRF, "initRF");
243
        if (this.otherRF != null)
244
            return;
245
        this.otherRF = computingRF;
246
 
247
        this.privateParentRF = new HashSet<SQLField>();
248
        final Set<SQLField> tmpOtherRF = new HashSet<SQLField>();
249
        for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) {
250
            // don't force every table to have an SQLElement (eg ELEMENT_MISSION)
251
            final SQLElement refElem = this.getElementLenient(refField.getTable());
252
            if (refElem != null && refElem.getPrivateForeignFields().contains(refField.getName())) {
253
                this.privateParentRF.add(refField);
254
            } else if (!this.getChildrenReferentFields().contains(refField)) {
255
                tmpOtherRF.add(refField);
256
            }
257
        }
258
        this.otherRF = tmpOtherRF;
259
    }
260
 
261
    // childRF is done outside initRF() to avoid :
262
    // MISSION.initRF() -> ELEMENT_MISSION.getPrivateForeignFields() ->
263
    // ELEMENT_MISSION.initFF() -> MISSION.getChildrenReferentFields() -> MISSION.initRF()
264
    private synchronized void initChildRF() {
265
        checkSelfCall(this.childRF != computingRF, "initFF");
266
        if (this.childRF != null)
267
            return;
268
        this.childRF = computingRF;
269
 
19 ilm 270
        final Set<SQLField> children = this.computeChildrenRF();
17 ilm 271
 
272
        final Set<SQLField> tmpChildRF = new HashSet<SQLField>();
273
        for (final SQLField refField : this.getTable().getBase().getGraph().getReferentKeys(this.getTable())) {
274
            // don't force every table to have an SQLElement (eg ELEMENT_MISSION)
275
            final SQLElement refElem = this.getElementLenient(refField.getTable());
276
            // if no element found, treat as elements with no parent
277
            final SQLField refParentFF = refElem == null ? null : refElem.getParentFF();
278
            // check coherence, either overload getParentFFName() or use getChildren(), but not both
279
            if (refParentFF != null && children.contains(refField))
280
                throw new IllegalStateException(refElem + " specifies this as its parent: " + refParentFF + " and is also mentioned as our (" + this + ") child: " + refField);
281
            if (children.contains(refField) || refParentFF == refField) {
282
                tmpChildRF.add(refField);
283
            }
284
        }
285
        // pas besoin de faire comme dans initFF pour vérifier children :
286
        // computeChildrenRF le fait déjà
287
        this.childRF = tmpChildRF;
288
    }
289
 
290
    final SQLElement getElement(SQLTable table) {
291
        final SQLElement res = getElementLenient(table);
292
        if (res == null)
293
            throw new IllegalStateException("no element for " + table.getSQLName());
294
        return res;
295
    }
296
 
297
    final SQLElement getElementLenient(SQLTable table) {
298
        return Configuration.getInstance().getDirectory().getElement(table);
299
    }
300
 
301
    public final SQLElement getForeignElement(String foreignField) {
302
        try {
303
            return this.getElement(this.getForeignTable(foreignField));
304
        } catch (RuntimeException e) {
305
            throw new IllegalStateException("no element for " + foreignField + " in " + this, e);
306
        }
307
    }
308
 
309
    private final SQLTable getForeignTable(String foreignField) {
310
        return this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(foreignField));
311
    }
312
 
313
    public String getPluralName() {
314
        return this.plural;
315
    }
316
 
317
    public String getSingularName() {
318
        return this.singular;
319
    }
320
 
321
    public CollectionMap<String, String> getShowAs() {
322
        // nothing by default
323
        return null;
324
    }
325
 
326
    /**
327
     * Fields that can neither be inserted nor updated.
328
     *
329
     * @return fields that cannot be modified.
330
     */
331
    public Set<String> getReadOnlyFields() {
332
        return Collections.emptySet();
333
    }
334
 
335
    /**
25 ilm 336
     * Fields that cannot be empty.
337
     *
338
     * @return fields that cannot be empty.
339
     */
340
    public Set<String> getRequiredFields() {
341
        return Collections.emptySet();
342
    }
343
 
344
    /**
17 ilm 345
     * Fields that can only be set on insertion.
346
     *
347
     * @return fields that cannot be modified.
348
     */
349
    public Set<String> getInsertOnlyFields() {
350
        return Collections.emptySet();
351
    }
352
 
353
    private final SQLCache<SQLRowAccessor, Object> getModelCache() {
354
        if (this.modelCache == null)
355
            this.modelCache = new SQLCache<SQLRowAccessor, Object>(60, -1, "modelObjects of " + this.getSingularName());
356
        return this.modelCache;
357
    }
358
 
359
    public void unarchiveNonRec(int id) throws SQLException {
360
        this.unarchiveNonRec(this.getTable().getRow(id));
361
    }
362
 
363
    private void unarchiveNonRec(SQLRow row) throws SQLException {
364
        checkUndefined(row);
365
        if (!row.isArchived())
366
            return;
367
 
368
        final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(Collections.singleton(row));
369
        for (final SQLRow desc : connectedRows) {
370
            getElement(desc.getTable()).unarchiveSingle(desc);
371
        }
372
        for (final SQLRow desc : connectedRows) {
373
            DeletionMode.UnArchiveMode.fireChange(desc);
374
        }
375
    }
376
 
377
    // *** getConnected*
378
 
379
    private Set<SQLRow> getArchivedConnectedRows(Collection<SQLRow> rows) throws SQLException {
380
        final Set<SQLRow> res = new HashSet<SQLRow>();
381
        for (final SQLRow row : rows) {
382
            this.getElement(row.getTable()).getArchivedConnectedRows(row, res);
383
        }
384
        return res;
385
    }
386
 
387
    private void getArchivedConnectedRows(SQLRow row, Set<SQLRow> rows) throws SQLException {
388
        check(row);
389
        // si on était déjà dedans, ne pas continuer
390
        if (!rows.add(row))
391
            return;
392
 
393
        // we want ARCHIVED existant and defined rows (since we never touch undefined ones)
394
        final SQLRowMode mode = new SQLRowMode(ArchiveMode.ARCHIVED, true, true);
395
        final Set<SQLRow> foreigns = new HashSet<SQLRow>(this.getNormalForeigns(row, mode).values());
396
        final SQLRow parent = this.getParent(row, mode);
397
        if (parent != null) {
398
            foreigns.add(parent);
399
        }
400
        // private ff are handled by DeletionMode
401
        // shared ff are never touched by DeletionMode
402
 
403
        // recurse
404
        for (final SQLRow foreign : foreigns) {
405
            this.getElement(foreign.getTable()).getArchivedConnectedRows(foreign, rows);
406
        }
407
    }
408
 
409
    // *** update
410
 
411
    /**
412
     * Compute the necessary steps to transform <code>from</code> into <code>to</code>.
413
     *
414
     * @param from the row currently in the db.
415
     * @param to the new values.
416
     * @return the script transforming <code>from</code> into <code>to</code>.
417
     */
418
    public final UpdateScript update(SQLRowValues from, SQLRowValues to) {
419
        check(from);
420
        check(to);
421
 
422
        if (!from.hasID())
423
            throw new IllegalArgumentException("missing id in " + from);
424
        if (from.getID() != to.getID())
425
            throw new IllegalArgumentException("not the same row: " + from + " != " + to);
426
 
427
        final Set<SQLField> fks = this.getTable().getForeignKeys();
428
        final UpdateScript res = new UpdateScript(this.getTable());
429
        for (final String field : to.getFields()) {
430
            if (!fks.contains(this.getTable().getField(field))) {
431
                res.getUpdateRow().put(field, to.getObject(field));
432
            } else {
433
                final Object fromPrivate = from.getObject(field);
434
                final Object toPrivate = to.getObject(field);
435
                if (!fromPrivate.getClass().equals(toPrivate.getClass()))
436
                    throw new IllegalArgumentException("asymmetric tree " + fromPrivate + " != " + toPrivate);
437
                final boolean isPrivate = this.getPrivateForeignFields().contains(field);
438
                if (fromPrivate instanceof SQLRowValues) {
439
                    final SQLRowValues fromPR = (SQLRowValues) fromPrivate;
440
                    final SQLRowValues toPR = (SQLRowValues) toPrivate;
441
                    if (isPrivate) {
442
                        if (from.isForeignEmpty(field) && to.isForeignEmpty(field)) {
443
                            // nothing to do, don't add to v
444
                        } else if (from.isForeignEmpty(field)) {
445
                            // insert, eg CPI.ID_OBS=1 -> CPI.ID_OBS={DES="rouillé"}
446
                            // clear referents otherwise we will merge the updateRow with the to
447
                            // graph (toPR being a private is pointed to by its owner, which itself
448
                            // points to others, but we just want the private)
449
                            res.getUpdateRow().put(field, toPR.deepCopy().clearReferents());
450
                        } else if (to.isForeignEmpty(field)) {
451
                            // archive
452
                            res.addToArchive(this.getForeignElement(field), fromPR);
453
                        } else {
454
                            // neither is empty
455
                            if (fromPR.getID() != toPR.getID())
456
                                throw new IllegalArgumentException("private have changed: " + fromPR + " != " + toPR);
457
                            res.put(field, this.getForeignElement(field).update(fromPR, toPR));
458
                        }
459
                    } else {
460
                        res.getUpdateRow().put(field, toPR.getID());
461
                    }
462
                } else {
463
                    final Number fromP_ID = (Number) fromPrivate;
464
                    final Number toP_ID = (Number) toPrivate;
465
                    if (isPrivate) {
466
                        // avoid Integer(3) != Long(3)
467
                        if (fromP_ID.longValue() != toP_ID.longValue())
468
                            throw new IllegalArgumentException("cannot change private ID");
469
                        // if they're the same, nothing to do
470
                    } else {
471
                        res.getUpdateRow().put(field, toP_ID);
472
                    }
473
                }
474
            }
475
        }
476
 
477
        return res;
478
    }
479
 
480
    public final void unarchive(int id) throws SQLException {
481
        this.unarchive(this.getTable().getRow(id));
482
    }
483
 
484
    public void unarchive(final SQLRow row) throws SQLException {
485
        checkUndefined(row);
486
        // don't test row.isArchived() (it is done by getTree())
487
        // to allow an unarchived parent to unarchive all its descendants.
488
 
489
        // nos descendants
490
        final List<SQLRow> descsAndMe = this.getTree(row, true);
491
        final Set<SQLRow> connectedRows = this.getArchivedConnectedRows(descsAndMe);
492
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
493
            @Override
494
            public Object create() throws SQLException {
495
                for (final SQLRow desc : connectedRows) {
496
                    getElement(desc.getTable()).unarchiveSingle(desc);
497
                }
498
                for (final SQLRow desc : connectedRows) {
499
                    DeletionMode.UnArchiveMode.fireChange(desc);
500
                }
501
 
502
                // reference
503
                // nothing to do : nobody points to an archived row
504
 
505
                return null;
506
            }
507
        });
508
    }
509
 
510
    public final void archive(int id) throws SQLException {
511
        this.archive(this.getTable().getRow(id));
512
    }
513
 
514
    public final void archive(SQLRow row) throws SQLException {
515
        this.archive(row, true);
516
    }
517
 
518
    /**
519
     * Archive la ligne demandée et tous ses descendants mais ne cherche pas à couper les références
520
     * pointant sur ceux-ci. ATTN peut donc laisser la base dans un état inconsistent, à n'utiliser
521
     * que si aucun lien ne pointe sur ceux ci. En revanche, accélère grandement (par exemple pour
522
     * OBSERVATION) car pas besoin de chercher toutes les références.
523
     *
524
     * @param id la ligne voulue.
525
     * @throws SQLException if pb while archiving.
526
     */
527
    public final void archiveNoCut(int id) throws SQLException {
528
        this.archive(this.getTable().getRow(id), false);
529
    }
530
 
531
    protected void archive(final SQLRow row, final boolean cutLinks) throws SQLException {
532
        this.archive(new TreesOfSQLRows(this, row), cutLinks);
533
    }
534
 
535
    protected void archive(final TreesOfSQLRows trees, final boolean cutLinks) throws SQLException {
536
        if (trees.getElem() != this)
537
            throw new IllegalArgumentException(this + " != " + trees.getElem());
538
        for (final SQLRow row : trees.getRows())
539
            checkUndefined(row);
540
 
541
        SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<Object>() {
542
            @Override
543
            public Object create() throws SQLException {
544
                // reference
545
                // d'abord couper les liens qui pointent sur les futurs archivés
546
                if (cutLinks) {
547
                    // TODO prend bcp de temps
548
                    // FIXME update tableau pour chaque observation, ecrase les changements
549
                    // faire : 'La base à changée voulez vous recharger ou garder vos modifs ?'
550
                    final MultiMap externReferences = trees.getExternReferences();
551
                    // avoid toString() which might make requests to display rows (eg archived)
552
                    if (Log.get().isLoggable(Level.FINEST))
553
                        Log.get().finest("will cut : " + externReferences);
554
                    final MapIterator refIter = new EntrySetMapIterator(externReferences);
555
                    while (refIter.hasNext()) {
556
                        final SQLField refKey = (SQLField) refIter.next();
19 ilm 557
                        final Collection<?> refList = (Collection<?>) refIter.getValue();
558
                        final Iterator<?> listIter = refList.iterator();
17 ilm 559
                        while (listIter.hasNext()) {
560
                            final SQLRow ref = (SQLRow) listIter.next();
561
                            ref.createEmptyUpdateRow().putEmptyLink(refKey.getName()).update();
562
                        }
563
                    }
564
                    Log.get().finest("done cutting links");
565
                }
566
 
567
                // on archive tous nos descendants
568
                for (final SQLRowAccessor desc : trees.getFlatDescendants()) {
569
                    getElement(desc.getTable()).archiveSingle(desc);
570
                    // ne pas faire les fire après sinon qd on efface plusieurs éléments de la même
571
                    // table :
572
                    // on fire pour le 1er => updateSearchList => IListe.select(userID)
573
                    // hors si userID a aussi été archivé (mais il n'y a pas eu son fire
574
                    // correspondant), le component va lancer un RowNotFound
575
                    DeletionMode.ArchiveMode.fireChange(desc);
576
                }
577
                // foreign field
578
                // nothing to do
579
 
580
                return null;
581
            }
582
        });
583
    }
584
 
585
    private final void archiveSingle(SQLRowAccessor r) throws SQLException {
586
        this.changeSingle(r, DeletionMode.ArchiveMode);
587
    }
588
 
589
    private final void unarchiveSingle(SQLRowAccessor r) throws SQLException {
590
        this.changeSingle(r, DeletionMode.UnArchiveMode);
591
    }
592
 
593
    private final void changeSingle(SQLRowAccessor r, DeletionMode m) throws SQLException {
594
        m.execute(this, r);
595
    }
596
 
597
    public void delete(SQLRowAccessor r) throws SQLException {
598
        this.check(r);
599
        if (true)
600
            throw new UnsupportedOperationException("not yet implemented.");
601
 
602
        this.changeSingle(r, DeletionMode.DeleteMode);
603
    }
604
 
605
    public final SQLTable getTable() {
606
        return this.primaryTable;
607
    }
608
 
609
    /**
610
     * Is the rows of this element shared, ie rows are unique and must not be copied.
611
     *
612
     * @return <code>true</code> if this element is shared.
613
     */
614
    public boolean isShared() {
615
        return false;
616
    }
617
 
618
    /**
619
     * Must the rows of this element be copied when traversing a hierarchy.
620
     *
621
     * @return <code>true</code> if the element must not be copied.
622
     */
623
    public boolean dontDeepCopy() {
624
        return false;
625
    }
626
 
627
    // *** rf
628
 
629
    public final synchronized Set<SQLField> getOtherReferentFields() {
630
        this.initRF();
631
        return this.otherRF;
632
    }
633
 
634
    public final synchronized Set<SQLField> getChildrenReferentFields() {
635
        this.initChildRF();
636
        return this.childRF;
637
    }
638
 
639
    /**
640
     * The private foreign fields pointing to this table. Eg if this is OBSERVATION,
641
     * {SOURCE.ID_OBS1, SOURCE.ID_OBS2, CPI.ID_OBS, LOCAL.ID_OBS} ; if this is LOCAL, {}.
642
     *
643
     * @return the private foreign fields pointing to this table.
644
     */
645
    public final synchronized Set<SQLField> getPrivateParentReferentFields() {
646
        this.initRF();
647
        return this.privateParentRF;
648
    }
649
 
650
    /**
651
     * Specify the tables whose rows are contained in rows of this element. They can be specified
652
     * with table names, in which case there must be exactly one foreign field from the specified
653
     * table to this element (eg "BATIMENT" for element SITE). Otherwise it must the fullname of
654
     * foreign field which points to the table of this element (eg "RECEPTEUR.ID_LOCAL").
655
     *
656
     * @return a Set of String.
657
     * @see #getParentFFName()
658
     */
659
    protected Set<String> getChildren() {
660
        return Collections.emptySet();
661
    }
662
 
663
    // *** ff
664
 
665
    public final synchronized Set<String> getNormalForeignFields() {
666
        this.initFF();
667
        return this.normalFF;
668
    }
669
 
670
    public final synchronized Set<String> getSharedForeignFields() {
671
        this.initFF();
672
        return this.sharedFF;
673
    }
674
 
675
    public final synchronized String getParentForeignField() {
676
        this.initFF();
677
        return this.parentFF;
678
    }
679
 
680
    private final SQLField getParentFF() {
681
        final String name = getParentFFName();
682
        return name == null ? null : this.getTable().getField(name);
683
    }
684
 
685
    /**
686
     * Should be overloaded to specify our parent. NOTE the relationship must be specified only once
687
     * either with this method or with {@link #getChildren()}. This method is preferred since it
688
     * avoids writing IFs to account for customer differences and there's no ambiguity (you return a
689
     * field of this table instead of a table name that must be searched in roots and then a foreign
690
     * key must be found).
691
     *
692
     * @return <code>null</code> for this implementation.
693
     * @see #getChildren()
694
     */
695
    protected String getParentFFName() {
696
        return null;
697
    }
698
 
699
    public final SQLElement getParentElement() {
700
        if (this.getParentForeignField() == null)
701
            return null;
702
        else
703
            return this.getForeignElement(this.getParentForeignField());
704
    }
705
 
706
    private final synchronized Map<String, SQLElement> getPrivateFF() {
707
        this.initFF();
708
        return this.privateFF;
709
    }
710
 
711
    /**
712
     * The fields that private to this table, ie rows pointed by these fields are referenced only by
713
     * one row of this table.
714
     *
715
     * @return private fields of this element.
716
     */
717
    public final Set<String> getPrivateForeignFields() {
718
        return Collections.unmodifiableSet(this.getPrivateFF().keySet());
719
    }
720
 
721
    public final SQLElement getPrivateElement(String foreignField) {
722
        return this.getPrivateFF().get(foreignField);
723
    }
724
 
725
    /**
726
     * The graph of this table and its private fields.
727
     *
728
     * @return a rowValues of this element's table with rowValues for each private foreign field.
729
     */
730
    public final SQLRowValues getPrivateGraph() {
731
        final SQLRowValues res = new SQLRowValues(this.getTable());
732
        res.setAllToNull();
733
        for (final Entry<String, SQLElement> e : this.getPrivateFF().entrySet()) {
734
            res.put(e.getKey(), e.getValue().getPrivateGraph());
735
        }
736
        return res;
737
    }
738
 
739
    /**
740
     * Renvoie les champs qui sont 'privé' càd que les ligne pointées par ce champ ne sont
741
     * référencées que par une et une seule ligne de cette table. Cette implementation renvoie une
742
     * liste vide. This method is intented for subclasses, call {@link #getPrivateForeignFields()}
743
     * which does some checks.
744
     *
745
     * @return la List des noms des champs privés, eg ["ID_OBSERVATION_2"].
746
     */
747
    protected List<String> getPrivateFields() {
748
        return Collections.emptyList();
749
    }
750
 
751
    public final void clearPrivateFields(SQLRowValues rowVals) {
752
        for (String s : getPrivateFF().keySet()) {
753
            rowVals.remove(s);
754
        }
755
    }
756
 
757
    final Map<String, ReferenceAction> getActions() {
758
        this.initFF();
759
        return this.actions;
760
    }
761
 
762
    /**
19 ilm 763
     * Specify an action for a normal foreign field.
17 ilm 764
     *
765
     * @param ff the foreign field name.
766
     * @param action what to do if a referenced row must be archived.
767
     * @throws IllegalArgumentException if <code>ff</code> is not a normal foreign field.
768
     */
769
    public final void setAction(final String ff, ReferenceAction action) throws IllegalArgumentException {
770
        // shared must be RESTRICT, parent at least CASCADE (to avoid child without a parent),
771
        // normal is free
772
        if (action.compareTo(ReferenceAction.RESTRICT) < 0 && !this.getNormalForeignFields().contains(ff))
25 ilm 773
            // getField() checks if the field exists
774
            throw new IllegalArgumentException(getTable().getField(ff).getSQLName() + " is not a normal foreign field : " + this.getNormalForeignFields());
17 ilm 775
        this.getActions().put(ff, action);
776
    }
777
 
778
    // *** rf and ff
779
 
780
    /**
781
     * The links towards the parents (either {@link #getParentForeignField()} or
782
     * {@link #getPrivateParentReferentFields()}) of this element.
783
     *
784
     * @return the graph links towards the parents of this element.
785
     */
786
    public final Set<Link> getParentsLinks() {
787
        final Set<SQLField> refFields = this.getPrivateParentReferentFields();
788
        final Set<Link> res = new HashSet<Link>(refFields.size());
789
        final DatabaseGraph graph = this.getTable().getDBSystemRoot().getGraph();
790
        for (final SQLField refField : refFields)
791
            res.add(graph.getForeignLink(refField));
792
        if (this.getParentForeignField() != null)
793
            res.add(graph.getForeignLink(this.getTable().getField(getParentForeignField())));
794
        return res;
795
    }
796
 
797
    /**
798
     * The elements beneath this, ie both children and privates.
799
     *
800
     * @return our children elements.
801
     */
802
    public final Set<SQLElement> getChildrenElements() {
803
        final Set<SQLElement> res = new HashSet<SQLElement>();
804
        res.addAll(this.getPrivateFF().values());
805
        for (final SQLTable child : new SQLFieldsSet(this.getChildrenReferentFields()).getTables())
806
            res.add(getElement(child));
807
        return res;
808
    }
809
 
810
    public final SQLElement getChildElement(final String tableName) {
811
        final SQLField field = CollectionUtils.getSole(new SQLFieldsSet(this.getChildrenReferentFields()).getFields(tableName));
812
        if (field == null)
813
            throw new IllegalStateException("no child table named " + tableName);
814
        else
815
            return this.getElement(field.getTable());
816
    }
817
 
818
    /**
819
     * The tables beneath this.
820
     *
821
     * @return our descendants, including this.
822
     * @see #getChildrenElements()
823
     */
824
    public final Set<SQLTable> getDescendantTables() {
825
        final Set<SQLTable> res = new HashSet<SQLTable>();
826
        this.getDescendantTables(res);
827
        return res;
828
    }
829
 
830
    private final void getDescendantTables(Set<SQLTable> res) {
831
        res.add(this.getTable());
832
        for (final SQLElement elem : this.getChildrenElements()) {
833
            res.addAll(elem.getDescendantTables());
834
        }
835
    }
836
 
837
    // *** request
838
 
839
    public ComboSQLRequest getComboRequest() {
840
        if (this.combo == null) {
841
            this.combo = new ComboSQLRequest(this.getTable(), this.getComboFields());
842
        }
843
        return this.combo;
844
    }
845
 
846
    abstract protected List<String> getComboFields();
847
 
19 ilm 848
    public final synchronized ListSQLRequest getListRequest() {
17 ilm 849
        if (this.list == null) {
19 ilm 850
            this.list = createListRequest();
17 ilm 851
        }
852
        return this.list;
853
    }
854
 
19 ilm 855
    protected ListSQLRequest createListRequest() {
856
        return new ListSQLRequest(this.getTable(), this.getListFields());
857
    }
858
 
17 ilm 859
    public final SQLTableModelSourceOnline getTableSource() {
860
        return this.getTableSource(!cacheTableSource());
861
    }
862
 
863
    /**
864
     * Return a table source for this element.
865
     *
866
     * @param create <code>true</code> if a new instance should be returned, <code>false</code> to
867
     *        return a shared instance.
868
     * @return a table source for this.
869
     */
870
    public final synchronized SQLTableModelSourceOnline getTableSource(final boolean create) {
871
        if (!create) {
872
            if (this.tableSrc == null) {
873
                this.tableSrc = createAndInitTableSource();
874
            }
875
            return this.tableSrc;
876
        } else
877
            return this.createAndInitTableSource();
878
    }
879
 
19 ilm 880
    public final SQLTableModelSourceOnline createTableSource(final List<String> fields) {
881
        return initTableSource(new SQLTableModelSourceOnline(new ListSQLRequest(this.getTable(), fields)));
882
    }
883
 
884
    public final SQLTableModelSourceOnline createTableSource(final Where w) {
885
        final SQLTableModelSourceOnline res = this.getTableSource(true);
886
        res.getReq().setWhere(w);
887
        return res;
888
    }
889
 
17 ilm 890
    private final SQLTableModelSourceOnline createAndInitTableSource() {
891
        final SQLTableModelSourceOnline res = this.createTableSource();
25 ilm 892
        res.getColumns().addAll(this.additionalListCols);
19 ilm 893
        return initTableSource(res);
894
    }
895
 
896
    protected synchronized void _initTableSource(final SQLTableModelSourceOnline res) {
897
    }
898
 
899
    public final synchronized SQLTableModelSourceOnline initTableSource(final SQLTableModelSourceOnline res) {
900
        // do init first since it can modify the columns
901
        this._initTableSource(res);
17 ilm 902
        // setEditable(false) on read only fields
903
        // MAYBE setReadOnlyFields() on SQLTableModelSource, so that SQLTableModelLinesSource can
904
        // check in commit()
905
        final Set<String> dontModif = CollectionUtils.union(this.getReadOnlyFields(), this.getInsertOnlyFields());
906
        for (final String f : dontModif)
907
            for (final SQLTableModelColumn col : res.getColumns(getTable().getField(f)))
908
                if (col instanceof SQLTableModelColumnPath)
909
                    ((SQLTableModelColumnPath) col).setEditable(false);
910
        return res;
911
    }
912
 
913
    protected SQLTableModelSourceOnline createTableSource() {
19 ilm 914
        // also create a new ListSQLRequest, otherwise it's a backdoor to change the behaviour of
915
        // the new TableModelSource
916
        return new SQLTableModelSourceOnline(this.createListRequest());
17 ilm 917
    }
918
 
919
    /**
920
     * Whether to cache our tableSource.
921
     *
922
     * @return <code>true</code> to call {@link #createTableSource()} only once, or
923
     *         <code>false</code> to call it each time {@link #getTableSource()} is.
924
     */
925
    protected boolean cacheTableSource() {
926
        return true;
927
    }
928
 
929
    abstract protected List<String> getListFields();
930
 
25 ilm 931
    public final void addListFields(final List<String> fields) {
932
        for (final String f : fields)
933
            this.addListColumn(new SQLTableModelColumnPath(getTable().getField(f)));
934
    }
935
 
936
    public final void addListColumn(SQLTableModelColumn col) {
937
        this.additionalListCols.add(col);
938
    }
939
 
21 ilm 940
    public final Collection<IListeAction> getRowActions() {
19 ilm 941
        return this.rowActions;
942
    }
943
 
21 ilm 944
    public final void addRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
19 ilm 945
        this.rowActions.getRecipe().addListener(listener);
946
    }
947
 
21 ilm 948
    public final void removeRowActionsListener(final IClosure<ListChangeIndex<IListeAction>> listener) {
19 ilm 949
        this.rowActions.getRecipe().rmListener(listener);
950
    }
951
 
17 ilm 952
    public String getDescription(SQLRow fromRow) {
953
        return fromRow.toString();
954
    }
955
 
956
    // *** iterators
957
 
958
    static interface ChildProcessor<R extends SQLRowAccessor> {
959
        public void process(R parent, SQLField joint, R child) throws SQLException;
960
    }
961
 
962
    /**
963
     * Execute <code>c</code> for each children of <code>row</code>. NOTE: <code>c</code> will be
964
     * called with <code>row</code> as its first parameter, and with its child of the same type
965
     * (SQLRow or SQLRowValues) for the third parameter.
966
     *
967
     * @param <R> type of SQLRowAccessor to use.
968
     * @param row the parent row.
969
     * @param c what to do for each children.
970
     * @param deep <code>true</code> to ignore {@link #dontDeepCopy()}.
971
     * @param archived <code>true</code> to iterate over archived children.
972
     * @throws SQLException if <code>c</code> raises an exn.
973
     */
974
    private <R extends SQLRowAccessor> void forChildrenDo(R row, ChildProcessor<? super R> c, boolean deep, boolean archived) throws SQLException {
975
        for (final SQLField childField : this.getChildrenReferentFields()) {
976
            if (deep || !this.getElement(childField.getTable()).dontDeepCopy()) {
977
                final List<SQLRow> children = row.asRow().getReferentRows(childField, archived ? SQLSelect.ARCHIVED : SQLSelect.UNARCHIVED);
978
                // eg BATIMENT[516]
979
                for (final SQLRow child : children) {
980
                    c.process(row, childField, convert(child, row));
981
                }
982
            }
983
        }
984
    }
985
 
986
    // convert toConv to same type as row
987
    @SuppressWarnings("unchecked")
988
    private <R extends SQLRowAccessor> R convert(final SQLRow toConv, R row) {
989
        final R ch;
990
        if (row instanceof SQLRow)
991
            ch = (R) toConv;
992
        else if (row instanceof SQLRowValues)
993
            ch = (R) toConv.createUpdateRow();
994
        else
995
            throw new IllegalStateException("SQLRowAccessor is neither SQLRow nor SQLRowValues: " + toConv);
996
        return ch;
997
    }
998
 
999
    // first the leaves
1000
    private void forDescendantsDo(final SQLRow row, final ChildProcessor<SQLRow> c, final boolean deep) throws SQLException {
1001
        this.forDescendantsDo(row, c, deep, true, false);
1002
    }
1003
 
1004
    <R extends SQLRowAccessor> void forDescendantsDo(final R row, final ChildProcessor<R> c, final boolean deep, final boolean leavesFirst, final boolean archived) throws SQLException {
1005
        this.check(row);
1006
        this.forChildrenDo(row, new ChildProcessor<R>() {
1007
            public void process(R parent, SQLField joint, R child) throws SQLException {
1008
                if (!leavesFirst)
1009
                    c.process(parent, joint, child);
1010
                getElement(child.getTable()).forDescendantsDo(child, c, deep, leavesFirst, archived);
1011
                if (leavesFirst)
1012
                    c.process(parent, joint, child);
1013
            }
1014
        }, deep, archived);
1015
    }
1016
 
1017
    void check(SQLRowAccessor row) {
1018
        if (!row.getTable().equals(this.getTable()))
1019
            throw new IllegalArgumentException("row must of table " + this.getTable() + " : " + row);
1020
    }
1021
 
1022
    private void checkUndefined(SQLRow row) {
1023
        this.check(row);
1024
        if (row.isUndefined())
1025
            throw new IllegalArgumentException("row is undefined: " + row);
1026
    }
1027
 
1028
    // *** copy
1029
 
1030
    public final SQLRow copyRecursive(int id) throws SQLException {
1031
        return this.copyRecursive(this.getTable().getRow(id));
1032
    }
1033
 
1034
    public final SQLRow copyRecursive(SQLRow row) throws SQLException {
1035
        return this.copyRecursive(row, null);
1036
    }
1037
 
1038
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent) throws SQLException {
1039
        return this.copyRecursive(row, parent, null);
1040
    }
1041
 
1042
    /**
1043
     * Copy <code>row</code> and its children into <code>parent</code>.
1044
     *
1045
     * @param row which row to clone.
1046
     * @param parent which parent the clone will have, <code>null</code> meaning the same than
1047
     *        <code>row</code>.
1048
     * @param c allow one to modify the copied rows before they are inserted, can be
1049
     *        <code>null</code>.
1050
     * @return the new copy.
1051
     * @throws SQLException if an error occurs.
1052
     */
1053
    public SQLRow copyRecursive(final SQLRow row, final SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
1054
        check(row);
1055
        if (row.isUndefined())
1056
            return row;
1057
 
1058
        // current => new copy
1059
        final Map<SQLRow, SQLRowValues> copies = new HashMap<SQLRow, SQLRowValues>();
1060
 
1061
        return SQLUtils.executeAtomic(this.getTable().getBase().getDataSource(), new SQLFactory<SQLRow>() {
1062
            @Override
1063
            public SQLRow create() throws SQLException {
1064
 
1065
                // eg SITE[128]
1066
                final SQLRowValues copy = createTransformedCopy(row, parent, c);
1067
                copies.put(row, copy);
1068
 
1069
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1070
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
1071
                        final SQLRowValues parentCopy = copies.get(parent);
1072
                        if (parentCopy == null)
1073
                            throw new IllegalStateException("null copy of " + parent);
1074
                        final SQLRowValues descCopy = createTransformedCopy(desc, null, c);
1075
                        descCopy.put(joint.getName(), parentCopy);
1076
                        copies.put(desc, descCopy);
1077
                    }
1078
                }, false, false, false);
1079
                // ne pas descendre en deep
1080
 
1081
                // reference
1082
                forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1083
                    public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
1084
                        final CollectionMap<SQLField, SQLRow> normalReferents = getElement(desc.getTable()).getNonChildrenReferents(desc);
1085
                        for (final Entry<SQLField, Collection<SQLRow>> e : normalReferents.entrySet()) {
1086
                            // eg SOURCE.ID_CPI
1087
                            final SQLField refField = e.getKey();
1088
                            for (final SQLRow ref : e.getValue()) {
1089
                                // eg copy of SOURCE[12] is SOURCE[354]
1090
                                final SQLRowValues refCopy = copies.get(ref);
1091
                                if (refCopy != null) {
1092
                                    // CPI[1203]
1093
                                    final SQLRowValues referencedCopy = copies.get(desc);
1094
                                    refCopy.put(refField.getName(), referencedCopy);
1095
                                }
1096
                            }
1097
                        }
1098
                    }
1099
                }, false);
1100
 
1101
                // we used to remove foreign links pointing outside the copy, but this was almost
1102
                // never right, e.g. : copy a batiment, its locals loose ID_FAMILLE ; copy a local,
1103
                // if a source in it points to an item in another local, its copy won't.
1104
 
1105
                return copy.insert();
1106
            }
1107
        });
1108
    }
1109
 
1110
    private final SQLRowValues createTransformedCopy(SQLRow desc, SQLRow parent, final IClosure<SQLRowValues> c) throws SQLException {
1111
        final SQLRowValues copiedVals = getElement(desc.getTable()).createCopy(desc, parent);
1112
        assert copiedVals != null : "failed to copy " + desc;
1113
        if (c != null)
1114
            c.executeChecked(copiedVals);
1115
        return copiedVals;
1116
    }
1117
 
1118
    public final SQLRow copy(int id) throws SQLException {
1119
        return this.copy(this.getTable().getRow(id));
1120
    }
1121
 
1122
    public final SQLRow copy(SQLRow row) throws SQLException {
1123
        return this.copy(row, null);
1124
    }
1125
 
1126
    public final SQLRow copy(SQLRow row, SQLRow parent) throws SQLException {
1127
        final SQLRowValues copy = this.createCopy(row, parent);
1128
        return copy == null ? row : copy.insert();
1129
    }
1130
 
1131
    public final SQLRowValues createCopy(int id) {
1132
        final SQLRow row = this.getTable().getRow(id);
1133
        return this.createCopy(row, null);
1134
    }
1135
 
1136
    /**
1137
     * Copies the passed row into an SQLRowValues. NOTE: this method does not access the DB, ie the
1138
     * copy won't be a copy of the current values in DB, but of the current values of the passed
1139
     * instance.
1140
     *
1141
     * @param row the row to copy, can be <code>null</code>.
1142
     * @param parent the parent the copy will be in, <code>null</code> meaning the same as
1143
     *        <code>row</code>.
1144
     * @return a copy ready to be inserted, or <code>null</code> if <code>row</code> cannot be
1145
     *         copied.
1146
     */
1147
    public SQLRowValues createCopy(SQLRow row, SQLRow parent) {
1148
        // do NOT copy the undefined
1149
        if (row == null || row.isUndefined())
1150
            return null;
1151
        this.check(row);
1152
 
1153
        final SQLRowValues copy = new SQLRowValues(this.getTable());
1154
        copy.loadAllSafe(row);
1155
 
19 ilm 1156
        for (final String privateName : this.getPrivateForeignFields()) {
17 ilm 1157
            final SQLElement privateElement = this.getPrivateElement(privateName);
1158
            if (!privateElement.dontDeepCopy() && !row.isForeignEmpty(privateName)) {
1159
                final SQLRowValues child = privateElement.createCopy(row.getInt(privateName));
1160
                copy.put(privateName, child);
1161
            } else {
1162
                copy.putEmptyLink(privateName);
1163
            }
1164
        }
1165
        // si on a spécifié un parent, eg BATIMENT[23]
1166
        if (parent != null) {
1167
            final SQLTable foreignTable = this.getTable().getBase().getGraph().getForeignTable(this.getTable().getField(this.getParentForeignField()));
1168
            if (!parent.getTable().equals(foreignTable))
1169
                throw new IllegalArgumentException(parent + " is not a parent of " + row);
1170
            copy.put(this.getParentForeignField(), parent.getID());
1171
        }
1172
 
1173
        return copy;
1174
    }
1175
 
1176
    // *** getRows
1177
 
1178
    /**
1179
     * Returns the descendant rows : the children of this element, recursively. ATTN does not carry
1180
     * the hierarchy.
1181
     *
1182
     * @param row a SQLRow.
1183
     * @return the descendant rows by SQLTable.
1184
     */
1185
    public final CollectionMap<SQLTable, SQLRow> getDescendants(SQLRow row) {
1186
        check(row);
1187
        final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>();
1188
        try {
1189
            this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1190
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
1191
                    mm.put(joint.getTable(), child);
1192
                }
1193
            }, true);
1194
        } catch (SQLException e) {
1195
            // never happen
1196
            e.printStackTrace();
1197
        }
1198
        return mm;
1199
    }
1200
 
1201
    /**
1202
     * Returns the tree beneath the passed row. The list is ordered "leaves-first", ie the last item
1203
     * is the root.
1204
     *
1205
     * @param row the root of the desired tree.
1206
     * @param archived <code>true</code> if the returned rows should be archived.
1207
     * @return a List of SQLRow.
1208
     */
1209
    private List<SQLRow> getTree(SQLRow row, boolean archived) {
1210
        check(row);
1211
        // nos descendants
1212
        final List<SQLRow> descsAndMe = new ArrayList<SQLRow>();
1213
        try {
1214
            this.forDescendantsDo(row, new ChildProcessor<SQLRow>() {
1215
                public void process(SQLRow parent, SQLField joint, SQLRow desc) throws SQLException {
1216
                    descsAndMe.add(desc);
1217
                }
1218
            }, true, true, archived);
1219
        } catch (SQLException e) {
1220
            // never happen cause process don't throw it
1221
            e.printStackTrace();
1222
        }
1223
        if (row.isArchived() == archived)
1224
            descsAndMe.add(row);
1225
        return descsAndMe;
1226
    }
1227
 
1228
    /**
1229
     * Returns the children of the passed row.
1230
     *
1231
     * @param row a SQLRow.
1232
     * @return the children rows by SQLTable.
1233
     */
1234
    public CollectionMap<SQLTable, SQLRow> getChildrenRows(SQLRow row) {
1235
        check(row);
1236
        // ArrayList
1237
        final CollectionMap<SQLTable, SQLRow> mm = new CollectionMap<SQLTable, SQLRow>();
1238
        try {
1239
            this.forChildrenDo(row, new ChildProcessor<SQLRow>() {
1240
                public void process(SQLRow parent, SQLField joint, SQLRow child) throws SQLException {
1241
                    mm.put(child.getTable(), child);
1242
                }
1243
            }, true, false);
1244
        } catch (SQLException e) {
1245
            // never happen
1246
            e.printStackTrace();
1247
        }
1248
        return mm;
1249
    }
1250
 
1251
    public SQLRow getParent(SQLRow row) {
1252
        return this.getParent(row, SQLRowMode.VALID);
1253
    }
1254
 
1255
    private SQLRow getParent(SQLRow row, final SQLRowMode mode) {
1256
        check(row);
1257
        return this.getParentForeignField() == null ? null : row.getForeignRow(this.getParentForeignField(), mode);
1258
    }
1259
 
1260
    // {SQLField => List<SQLRow>}
1261
    CollectionMap<SQLField, SQLRow> getNonChildrenReferents(SQLRow row) {
1262
        check(row);
1263
        final CollectionMap<SQLField, SQLRow> mm = new CollectionMap<SQLField, SQLRow>();
1264
        final Set<SQLField> nonChildren = new HashSet<SQLField>(row.getTable().getDBSystemRoot().getGraph().getReferentKeys(row.getTable()));
1265
        nonChildren.removeAll(this.getChildrenReferentFields());
1266
        for (final SQLField refField : nonChildren) {
1267
            // eg CONTACT.ID_SITE => [CONTACT[12], CONTACT[13]]
1268
            mm.putAll(refField, row.getReferentRows(refField));
1269
        }
1270
        return mm;
1271
    }
1272
 
1273
    public Map<String, SQLRow> getNormalForeigns(SQLRow row) {
1274
        return this.getNormalForeigns(row, SQLRowMode.DEFINED);
1275
    }
1276
 
1277
    private Map<String, SQLRow> getNormalForeigns(SQLRow row, final SQLRowMode mode) {
1278
        check(row);
1279
        final Map<String, SQLRow> mm = new HashMap<String, SQLRow>();
19 ilm 1280
        final Iterator<String> iter = this.getNormalForeignFields().iterator();
17 ilm 1281
        while (iter.hasNext()) {
1282
            // eg SOURCE.ID_CPI
19 ilm 1283
            final String ff = iter.next();
17 ilm 1284
            // eg CPI[12]
1285
            final SQLRow foreignRow = row.getForeignRow(ff, mode);
1286
            if (foreignRow != null)
1287
                mm.put(ff, foreignRow);
1288
        }
1289
        return mm;
1290
    }
1291
 
1292
    /**
1293
     * Returns a java object modeling the passed row.
1294
     *
1295
     * @param row the row to model.
1296
     * @return an instance modeling the passed row or <code>null</code> if there's no class to model
1297
     *         this table.
1298
     * @see SQLRowAccessor#getModelObject()
1299
     */
1300
    public Object getModelObject(SQLRowAccessor row) {
1301
        check(row);
1302
        if (this.getModelClass() == null)
1303
            return null;
1304
 
1305
        final Object res;
1306
        // seuls les SQLRow peuvent être cachées
1307
        if (row instanceof SQLRow) {
1308
            // MAYBE make the modelObject change
1309
            final CacheResult<Object> cached = this.getModelCache().check(row);
1310
            if (cached.getState() == CacheResult.State.NOT_IN_CACHE) {
1311
                res = this.createModelObject(row);
1312
                this.getModelCache().put(row, res, Collections.singleton(row));
1313
            } else
1314
                res = cached.getRes();
1315
        } else
1316
            res = this.createModelObject(row);
1317
 
1318
        return res;
1319
    }
1320
 
1321
    private final Object createModelObject(SQLRowAccessor row) {
1322
        if (!RowBacked.class.isAssignableFrom(this.getModelClass()))
1323
            throw new IllegalStateException("modelClass must inherit from RowBacked: " + this.getModelClass());
19 ilm 1324
        final Constructor<? extends RowBacked> ctor;
17 ilm 1325
        try {
1326
            ctor = this.getModelClass().getConstructor(new Class[] { SQLRowAccessor.class });
1327
        } catch (Exception e) {
1328
            throw ExceptionUtils.createExn(IllegalStateException.class, "no SQLRowAccessor constructor", e);
1329
        }
1330
        try {
1331
            return ctor.newInstance(new Object[] { row });
1332
        } catch (Exception e) {
1333
            throw ExceptionUtils.createExn(RuntimeException.class, "pb creating instance", e);
1334
        }
1335
    }
1336
 
1337
    protected Class<? extends RowBacked> getModelClass() {
1338
        return null;
1339
    }
1340
 
1341
    // *** equals
1342
 
1343
    public boolean equals(SQLRow row, SQLRow row2) throws SQLException {
1344
        check(row);
1345
        if (!row2.getTable().equals(this.getTable()))
1346
            return false;
1347
        if (row.equals(row2))
1348
            return true;
1349
        // the same table but not the same id
1350
 
1351
        if (!row.getAllValues().equals(row2.getAllValues()))
1352
            return false;
1353
 
1354
        // shared doivent être partagées (!)
1355
        for (final String shared : this.getSharedForeignFields()) {
1356
            if (row.getInt(shared) != row2.getInt(shared))
1357
                return false;
1358
        }
1359
 
1360
        // les private equals
19 ilm 1361
        for (final String prvt : this.getPrivateForeignFields()) {
17 ilm 1362
            final SQLElement foreignElement = this.getForeignElement(prvt);
1363
            // ne pas tester
1364
            if (!foreignElement.dontDeepCopy() && !foreignElement.equals(row.getForeignRow(prvt), row2.getForeignRow(prvt)))
1365
                return false;
1366
        }
1367
 
1368
        return true;
1369
    }
1370
 
1371
    public boolean equalsRecursive(SQLRow row, SQLRow row2) throws SQLException {
1372
        // if (!equals(row, row2))
1373
        // return false;
1374
        return new SQLElementRowR(this, row).equals(new SQLElementRowR(this, row2));
1375
    }
1376
 
1377
    public final boolean equals(Object obj) {
1378
        if (obj instanceof SQLElement) {
1379
            final SQLElement o = (SQLElement) obj;
1380
            final boolean parentEq = CompareUtils.equals(this.getParentForeignField(), o.getParentForeignField());
1381
            return this.getTable().equals(o.getTable()) && this.getSharedForeignFields().equals(o.getSharedForeignFields()) && parentEq
1382
                    && this.getPrivateForeignFields().equals(o.getPrivateForeignFields()) && this.getChildrenReferentFields().equals(o.getChildrenReferentFields());
1383
            // MAYBE also check getPrivateElement(String foreignField);
1384
        } else
1385
            return false;
1386
    }
1387
 
1388
    public final int hashCode() {
1389
        // ne pas mettre getParent car des fois null
25 ilm 1390
        return this.getTable().hashCode(); // + this.getSharedForeignFields().hashCode() +
1391
                                           // this.getPrivateForeignFields().hashCode();
17 ilm 1392
    }
1393
 
1394
    @Override
1395
    public String toString() {
1396
        return this.getClass().getName() + " '" + this.plural + "'";
1397
    }
1398
 
1399
    // *** gui
1400
 
1401
    /**
1402
     * Retourne l'interface graphique de saisie.
1403
     *
1404
     * @return l'interface graphique de saisie.
1405
     */
1406
    public abstract SQLComponent createComponent();
1407
 
19 ilm 1408
    /**
1409
     * Allows a module to add a view for a field to this element.
1410
     *
1411
     * @param field the field of the component.
1412
     * @return <code>true</code> if no view existed.
1413
     */
1414
    public final boolean putAdditionalField(final String field) {
1415
        return this.putAdditionalField(field, (JComponent) null);
1416
    }
1417
 
1418
    public final boolean putAdditionalField(final String field, final JTextComponent comp) {
1419
        return this.putAdditionalField(field, (JComponent) comp);
1420
    }
1421
 
1422
    public final boolean putAdditionalField(final String field, final SQLTextCombo comp) {
1423
        return this.putAdditionalField(field, (JComponent) comp);
1424
    }
1425
 
1426
    // private as only a few JComponent are OK
1427
    private final boolean putAdditionalField(final String field, final JComponent comp) {
1428
        if (this.additionalFields.containsKey(field)) {
1429
            return false;
1430
        } else {
1431
            this.additionalFields.put(field, comp);
1432
            return true;
1433
        }
1434
    }
1435
 
1436
    public final Map<String, JComponent> getAdditionalFields() {
1437
        return Collections.unmodifiableMap(this.additionalFields);
1438
    }
1439
 
1440
    public final void removeAdditionalField(final String field) {
1441
        this.additionalFields.remove(field);
1442
    }
1443
 
17 ilm 1444
    public final boolean askArchive(final Component comp, final Number ids) {
1445
        return this.askArchive(comp, Collections.singleton(ids));
1446
    }
1447
 
1448
    /**
1449
     * Ask to the user before archiving.
1450
     *
1451
     * @param comp the parent component.
1452
     * @param ids which rows to archive.
1453
     * @return <code>true</code> if the rows were successfully archived, <code>false</code>
1454
     *         otherwise.
1455
     */
1456
    public boolean askArchive(final Component comp, final Collection<? extends Number> ids) {
1457
        boolean shouldArchive = false;
1458
        if (ids.isEmpty())
1459
            return true;
1460
        final boolean plural = ids.size() > 1;
1461
        final String lines = plural ? "ces " + ids.size() + " lignes" : "cette ligne";
1462
        try {
1463
            if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
1464
                throw new SQLException("forbidden");
1465
            final TreesOfSQLRows trees = TreesOfSQLRows.createFromIDs(this, ids);
1466
            final CollectionMap<SQLTable, SQLRowAccessor> descs = trees.getDescendantsByTable();
1467
            final SortedMap<SQLField, Integer> externRefs = trees.getExternReferencesCount();
1468
            if (descs.size() + externRefs.size() > 0) {
1469
                String msg = "";
1470
                if (descs.size() > 0)
1471
                    msg = StringUtils.firstUpThenLow(lines) + (plural ? " sont utilisées" : " est utilisée") + " par : \n" + toString(descs);
1472
                if (externRefs.size() > 0) {
1473
                    msg += descs.size() > 0 ? "\n\nDe plus les" : "Les";
1474
                    msg += " liens suivant vont être IRREMEDIABLEMENT détruit :\n" + toStringExtern(externRefs);
1475
                }
1476
 
1477
                int i = askSerious(comp, msg + "\n\nVoulez vous effacer " + lines + " ainsi que toutes les lignes liées ?", "Confirmation d'effacement");
1478
                if (i == JOptionPane.YES_OPTION) {
1479
                    msg = "";
1480
                    if (externRefs.size() > 0)
1481
                        msg = "Les liens suivant vont être IRREMEDIABLEMENT détruit, ils ne pourront pas être 'désarchivés' :\n" + toStringExtern(externRefs) + "\n\n";
1482
                    i = askSerious(comp, msg + "Voulez vous VRAIMENT effacer " + lines + " ainsi que toutes les lignes liées ?", "Confirmation d'effacement");
1483
                    if (i == JOptionPane.YES_OPTION) {
1484
                        shouldArchive = true;
1485
                    } else {
1486
                        JOptionPane.showMessageDialog(comp, "Aucune ligne effacée.", "Information", JOptionPane.INFORMATION_MESSAGE);
1487
                    }
1488
                }
1489
            } else {
1490
                int i = askSerious(comp, "Voulez vous effacer " + lines + " ?", "Confirmation d'effacement");
1491
                if (i == JOptionPane.YES_OPTION) {
1492
                    shouldArchive = true;
1493
                }
1494
            }
1495
            if (shouldArchive) {
1496
                this.archive(trees, true);
1497
                return true;
1498
            } else
1499
                return false;
1500
        } catch (SQLException e) {
1501
            ExceptionHandler.handle(comp, "Impossible d'archiver " + this + " IDs " + ids, e);
1502
            return false;
1503
        }
1504
    }
1505
 
19 ilm 1506
    @SuppressWarnings("rawtypes")
17 ilm 1507
    private final String toString(MultiMap descs) {
1508
        final List<String> l = new ArrayList<String>();
1509
        final Iterator iter = descs.keySet().iterator();
1510
        while (iter.hasNext()) {
1511
            final SQLTable t = (SQLTable) iter.next();
1512
            final Collection rows = (Collection) descs.get(t);
1513
            final SQLElement elem = getElement(t);
1514
            l.add(elemToString(rows.size(), elem));
1515
        }
1516
        return CollectionUtils.join(l, "\n");
1517
    }
1518
 
1519
    private static final String elemToString(int count, SQLElement elem) {
1520
        // don't use count for 1 as the article is in singularName
1521
        return "- " + (count == 1 ? elem.getSingularName() : count + " " + elem.getPluralName());
1522
    }
1523
 
1524
    // traduire TRANSFO.ID_ELEMENT_TABLEAU_PRI -> {TRANSFO[5], TRANSFO[12]}
1525
    // en 2 transformateurs vont perdre leurs champs 'Circuit primaire'
1526
    private final String toStringExtern(SortedMap<SQLField, Integer> externRef) {
1527
        final List<String> l = new ArrayList<String>();
1528
        for (final Map.Entry<SQLField, Integer> entry : externRef.entrySet()) {
1529
            final SQLField foreignKey = entry.getKey();
1530
            final int count = entry.getValue();
1531
            final String end;
1532
            final String label = Configuration.getTranslator(foreignKey.getTable()).getLabelFor(foreignKey);
1533
            if (count > 1)
1534
                end = " vont perdre leurs champs '" + label + "'";
1535
            else
1536
                end = " va perdre son champ '" + label + "'";
1537
            l.add(elemToString(count, getElement(foreignKey.getTable())) + end);
1538
        }
1539
        return CollectionUtils.join(l, "\n");
1540
    }
1541
 
1542
    private final int askSerious(Component comp, String msg, String title) {
1543
        return JOptionPane.showConfirmDialog(comp, msg, title + " (" + this.getPluralName() + ")", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
1544
    }
19 ilm 1545
 
17 ilm 1546
}