OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.model;
15
 
142 ilm 16
import static org.openconcerto.xml.JDOM2Utils.OUTPUTTER;
17
 
17 ilm 18
import org.openconcerto.sql.Log;
83 ilm 19
import org.openconcerto.sql.model.SQLSelect.ArchiveMode;
142 ilm 20
import org.openconcerto.sql.model.SQLSelect.LockStrength;
17 ilm 21
import org.openconcerto.sql.model.SQLSyntax.ConstraintType;
22
import org.openconcerto.sql.model.SQLTableEvent.Mode;
182 ilm 23
import org.openconcerto.sql.model.Where.RowComparison;
17 ilm 24
import org.openconcerto.sql.model.graph.DatabaseGraph;
25
import org.openconcerto.sql.model.graph.Link;
61 ilm 26
import org.openconcerto.sql.model.graph.Link.Rule;
93 ilm 27
import org.openconcerto.sql.model.graph.SQLKey;
28
import org.openconcerto.sql.model.graph.SQLKey.Type;
67 ilm 29
import org.openconcerto.sql.model.graph.TablesMap;
17 ilm 30
import org.openconcerto.sql.request.UpdateBuilder;
31
import org.openconcerto.sql.utils.ChangeTable;
144 ilm 32
import org.openconcerto.sql.utils.PartialUniqueTrigger;
17 ilm 33
import org.openconcerto.sql.utils.SQLCreateMoveableTable;
182 ilm 34
import org.openconcerto.sql.utils.SQLUtils;
144 ilm 35
import org.openconcerto.sql.utils.UniqueConstraintCreatorHelper;
17 ilm 36
import org.openconcerto.utils.CollectionUtils;
37
import org.openconcerto.utils.CompareUtils;
38
import org.openconcerto.utils.ExceptionUtils;
73 ilm 39
import org.openconcerto.utils.ListMap;
83 ilm 40
import org.openconcerto.utils.SetMap;
41
import org.openconcerto.utils.StringUtils;
25 ilm 42
import org.openconcerto.utils.Tuple2;
61 ilm 43
import org.openconcerto.utils.Tuple3;
83 ilm 44
import org.openconcerto.utils.Value;
61 ilm 45
import org.openconcerto.utils.cc.CopyOnWriteMap;
132 ilm 46
import org.openconcerto.utils.cc.CustomEquals;
17 ilm 47
import org.openconcerto.utils.change.CollectionChangeEventCreator;
48
 
49
import java.math.BigDecimal;
50
import java.sql.DatabaseMetaData;
51
import java.sql.ResultSet;
52
import java.sql.SQLException;
182 ilm 53
import java.sql.Statement;
54
import java.sql.Types;
17 ilm 55
import java.util.ArrayList;
67 ilm 56
import java.util.Arrays;
17 ilm 57
import java.util.Collection;
58
import java.util.Collections;
132 ilm 59
import java.util.EnumSet;
17 ilm 60
import java.util.HashMap;
61
import java.util.HashSet;
62
import java.util.Iterator;
63
import java.util.LinkedHashMap;
64
import java.util.LinkedHashSet;
25 ilm 65
import java.util.LinkedList;
17 ilm 66
import java.util.List;
73 ilm 67
import java.util.ListIterator;
17 ilm 68
import java.util.Map;
67 ilm 69
import java.util.Map.Entry;
182 ilm 70
import java.util.Objects;
17 ilm 71
import java.util.Set;
83 ilm 72
import java.util.regex.Matcher;
73
import java.util.regex.Pattern;
17 ilm 74
 
132 ilm 75
import org.apache.commons.dbutils.ResultSetHandler;
76
import org.jdom2.Element;
77
 
61 ilm 78
import net.jcip.annotations.GuardedBy;
93 ilm 79
import net.jcip.annotations.Immutable;
61 ilm 80
 
17 ilm 81
/**
82
 * Une table SQL. Connait ses champs, notamment sa clef primaire et ses clefs externes. Une table
83
 * peut aussi faire des diagnostic sur son intégrité, ou sur la validité d'une valeur d'un de ses
84
 * champs. Enfin elle permet d'accéder aux lignes qui la composent.
85
 *
86
 * @author ILM Informatique 4 mai 2004
87
 * @see #getField(String)
88
 * @see #getKey()
89
 * @see #getForeignKeys()
90
 * @see #checkIntegrity()
91
 * @see #checkValidity(String, int)
92
 * @see #getRow(int)
93
 */
65 ilm 94
public final class SQLTable extends SQLIdentifier implements SQLData, TableRef {
17 ilm 95
 
132 ilm 96
    private static final String UNDEF_TABLE_TABLENAME_FIELD = "TABLENAME";
97
    private static final String UNDEF_TABLE_ID_FIELD = "UNDEFINED_ID";
98
 
17 ilm 99
    /**
100
     * The {@link DBRoot#setMetadata(String, String) meta data} configuring the policy regarding
101
     * undefined IDs for a particular root. Can be either :
102
     * <dl>
67 ilm 103
     * <dt>inDB</dt>
104
     * <dd>all undefined IDs must be in {@value #undefTable}. Allow different IDs like "min" but
105
     * without the performance penalty</dd>
17 ilm 106
     * <dt>min</dt>
107
     * <dd>for min("ID")</dd>
108
     * <dt>nonexistant</dt>
109
     * <dd>(the default) {@link SQLRow#NONEXISTANT_ID}</dd>
110
     * <dt><i>any other value</i></dt>
111
     * <dd>parsed as a number</dd>
112
     * </dl>
113
     */
114
    public static final String UNDEFINED_ID_POLICY = "undefined ID policy";
41 ilm 115
    public static final String undefTable = SQLSchema.FWK_TABLENAME_PREFIX + "UNDEFINED_IDS";
17 ilm 116
    // {SQLSchema=>{TableName=>UndefID}}
117
    private static final Map<SQLSchema, Map<String, Number>> UNDEFINED_IDs = new HashMap<SQLSchema, Map<String, Number>>();
132 ilm 118
    private static final ResultSetHandler UNDEF_RSH = new ResultSetHandler() {
119
        @Override
120
        public Object handle(ResultSet rs) throws SQLException {
121
            final Map<String, Number> res = new HashMap<String, Number>();
122
            while (rs.next()) {
123
                res.put(rs.getString(UNDEF_TABLE_TABLENAME_FIELD), (Number) rs.getObject(UNDEF_TABLE_ID_FIELD));
124
            }
125
            return res;
126
        }
127
    };
17 ilm 128
 
129
    @SuppressWarnings("unchecked")
182 ilm 130
    private static final Map<String, Number> getUndefIDs(final SQLSchema schema) {
177 ilm 131
        assert Thread.holdsLock(UNDEFINED_IDs);
17 ilm 132
        if (!UNDEFINED_IDs.containsKey(schema)) {
133
            final Map<String, Number> r;
134
            if (schema.contains(undefTable)) {
135
                final SQLBase b = schema.getBase();
136
                final SQLTable undefT = schema.getTable(undefTable);
80 ilm 137
                final SQLSelect sel = new SQLSelect().addSelectStar(undefT);
132 ilm 138
                // don't use the cache as the result is stored in UNDEFINED_IDs
139
                r = (Map<String, Number>) b.getDataSource().execute(sel.asString(), new IResultSetHandler(UNDEF_RSH, false));
73 ilm 140
                // be called early, since it's more likely that some transaction will create table,
141
                // set its undefined ID, then use it in requests, than some other transaction
142
                // needing the undefined ID. TODO The real fix is one tree per transaction.
143
                undefT.addTableModifiedListener(new ListenerAndConfig(new SQLTableModifiedListener() {
17 ilm 144
                    @Override
145
                    public void tableModified(SQLTableEvent evt) {
146
                        synchronized (UNDEFINED_IDs) {
147
                            UNDEFINED_IDs.remove(schema);
148
                            undefT.removeTableModifiedListener(this);
149
                        }
150
                    }
73 ilm 151
                }, false));
17 ilm 152
            } else {
153
                r = Collections.emptyMap();
154
            }
155
            UNDEFINED_IDs.put(schema, r);
156
        }
157
        return UNDEFINED_IDs.get(schema);
158
    }
159
 
83 ilm 160
    static final void removeUndefID(SQLSchema s) {
161
        synchronized (UNDEFINED_IDs) {
162
            UNDEFINED_IDs.remove(s);
163
        }
164
    }
165
 
67 ilm 166
    static final Tuple2<Boolean, Number> getUndefID(SQLSchema b, String tableName) {
17 ilm 167
        synchronized (UNDEFINED_IDs) {
61 ilm 168
            final Map<String, Number> map = getUndefIDs(b);
169
            return Tuple2.create(map.containsKey(tableName), map.get(tableName));
17 ilm 170
        }
171
    }
172
 
177 ilm 173
    public static final Map<String, Number> getUndefinedIDs(final SQLSchema schema) {
174
        final Map<String, Number> res;
175
        synchronized (UNDEFINED_IDs) {
176
            res = new HashMap<>(getUndefIDs(schema));
177
        }
178
        return Collections.unmodifiableMap(res);
179
    }
180
 
67 ilm 181
    private static final SQLCreateMoveableTable getCreateUndefTable(SQLSyntax syntax) {
182
        final SQLCreateMoveableTable createTable = new SQLCreateMoveableTable(syntax, undefTable);
132 ilm 183
        createTable.addVarCharColumn(UNDEF_TABLE_TABLENAME_FIELD, 250);
184
        createTable.addColumn(UNDEF_TABLE_ID_FIELD, syntax.getIDType());
185
        createTable.setPrimaryKey(UNDEF_TABLE_TABLENAME_FIELD);
67 ilm 186
        return createTable;
187
    }
188
 
189
    private static final SQLTable getUndefTable(SQLSchema schema, boolean create) throws SQLException {
190
        final SQLTable undefT = schema.getTable(undefTable);
191
        if (undefT != null || !create) {
192
            return undefT;
193
        } else {
194
            schema.getDBSystemRoot().getDataSource().execute(getCreateUndefTable(SQLSyntax.get(schema)).asString(schema.getDBRoot().getName()));
195
            schema.updateVersion();
196
            return schema.fetchTable(undefTable);
197
        }
198
    }
199
 
17 ilm 200
    public static final void setUndefID(SQLSchema schema, String tableName, Integer value) throws SQLException {
67 ilm 201
        setUndefIDs(schema, Collections.singletonMap(tableName, value));
202
    }
203
 
204
    // return modified count
205
    public static final int setUndefIDs(SQLSchema schema, Map<String, ? extends Number> values) throws SQLException {
17 ilm 206
        synchronized (UNDEFINED_IDs) {
67 ilm 207
            final SQLTable undefT = getUndefTable(schema, true);
132 ilm 208
            final SQLType undefType = undefT.getField(UNDEF_TABLE_ID_FIELD).getType();
67 ilm 209
            final List<List<String>> toInsert = new ArrayList<List<String>>();
210
            final List<List<String>> toUpdate = new ArrayList<List<String>>();
211
            final Map<String, Number> currentValues = getUndefIDs(schema);
212
            final SQLBase b = schema.getBase();
213
            final SQLSystem system = b.getServer().getSQLSystem();
214
            for (final Entry<String, ? extends Number> e : values.entrySet()) {
215
                final String tableName = e.getKey();
216
                final Number undefValue = e.getValue();
217
                final List<List<String>> l;
218
                if (!currentValues.containsKey(tableName)) {
219
                    l = toInsert;
220
                } else if (CompareUtils.equals(currentValues.get(tableName), undefValue)) {
221
                    l = null;
222
                } else {
223
                    l = toUpdate;
224
                }
225
                if (l != null) {
226
                    final String undefSQL;
227
                    if (undefValue == null && system == SQLSystem.POSTGRESQL)
228
                        // column "UNDEFINED_ID" is of type integer but expression is of type text
229
                        undefSQL = "cast( NULL as " + undefType.getTypeName() + ")";
230
                    else
231
                        undefSQL = undefType.toString(undefValue);
232
                    l.add(Arrays.asList(b.quoteString(tableName), undefSQL));
233
                }
234
            }
142 ilm 235
            final SQLSyntax syntax = schema.getDBSystemRoot().getSyntax();
67 ilm 236
            if (toInsert.size() > 0) {
17 ilm 237
                // INSERT
132 ilm 238
                SQLRowValues.insertCount(undefT, "(" + SQLSyntax.quoteIdentifiers(Arrays.asList(UNDEF_TABLE_TABLENAME_FIELD, UNDEF_TABLE_ID_FIELD)) + ") " + syntax.getValues(toInsert, 2));
67 ilm 239
            }
240
            if (toUpdate.size() > 0) {
17 ilm 241
                // UPDATE
67 ilm 242
                // h2 doesn't support multi-table UPDATE
243
                if (system == SQLSystem.H2) {
244
                    final StringBuilder updates = new StringBuilder();
245
                    for (final List<String> l : toUpdate) {
132 ilm 246
                        final UpdateBuilder update = new UpdateBuilder(undefT).set(UNDEF_TABLE_ID_FIELD, l.get(1));
247
                        update.setWhere(Where.createRaw(undefT.getField(UNDEF_TABLE_TABLENAME_FIELD).getFieldRef() + " = " + l.get(0)));
67 ilm 248
                        updates.append(update.asString());
249
                        updates.append(";\n");
250
                    }
251
                    schema.getDBSystemRoot().getDataSource().execute(updates.toString());
252
                } else {
253
                    final UpdateBuilder update = new UpdateBuilder(undefT);
254
                    final String constantTableAlias = "newUndef";
255
                    update.addRawTable(syntax.getConstantTable(toUpdate, constantTableAlias, Arrays.asList("t", "v")), null);
132 ilm 256
                    update.setWhere(Where.createRaw(undefT.getField(UNDEF_TABLE_TABLENAME_FIELD).getFieldRef() + " = " + new SQLName(constantTableAlias, "t").quote()));
257
                    update.set(UNDEF_TABLE_ID_FIELD, new SQLName(constantTableAlias, "v").quote());
67 ilm 258
                    schema.getDBSystemRoot().getDataSource().execute(update.asString());
259
                }
17 ilm 260
            }
67 ilm 261
            final int res = toInsert.size() + toUpdate.size();
262
            if (res > 0) {
182 ilm 263
                undefT.fireTableModified();
17 ilm 264
            }
67 ilm 265
            return res;
17 ilm 266
        }
267
    }
268
 
182 ilm 269
    public static final boolean unsetUndefIDs(SQLSchema schema, Set<String> tableNames) throws SQLException {
270
        final boolean tableLoaded = schema.getTable(undefTable) != null;
271
        final boolean tableExists = unsetUndefIDs(schema.getDBSystemRoot(), schema.getDBRoot().getName(), tableNames);
272
        if (tableLoaded != tableExists)
273
            throw new IllegalStateException("Root not up to date, table loaded : " + tableLoaded + ", table exists : " + tableExists);
274
        return tableExists;
275
    }
276
 
277
    public static final boolean unsetUndefIDs(final DBSystemRoot sysRoot, final String rootName, Set<String> tableNames) throws SQLException {
278
        final SQLName undefSQLName = new SQLName(Objects.requireNonNull(rootName, "Missing root name"), undefTable);
279
        final int deletedCount;
280
        try {
281
            // If already in a transaction, don't risk aborting it if a table doesn't exist.
282
            // (it's not strictly required for H2 and MySQL, since the transaction is *not*
283
            // aborted)
284
            deletedCount = SQLUtils.executeAtomic(sysRoot.getDataSource(), (ds) -> {
285
                try (final Statement stmt = ds.getConnection().createStatement()) {
286
                    final int res = stmt.executeUpdate("DELETE FROM " + undefSQLName + " WHERE "
287
                            + Where.getCompareValuesClause(SQLBase.quoteIdentifier(UNDEF_TABLE_TABLENAME_FIELD), RowComparison.IN, tableNames, SQLType.getFromSyntax(sysRoot.getSyntax(), Types.VARCHAR, 250)));
288
                    assert res >= 0;
289
                    return res;
290
                }
291
            });
292
        } catch (SQLException e) {
293
            // nothing to unset
294
            if (sysRoot.getSyntax().isTableNotFoundException(e))
295
                return false;
296
            throw e;
297
        }
298
        if (deletedCount > 0) {
299
            // rootName might exist and thus the above query might succeed, but the root might not
300
            // be loaded or up to date.
301
            final SQLTable undefT = sysRoot.getDescLenient(undefSQLName, SQLTable.class);
302
            if (undefT != null)
303
                undefT.fireTableModified();
304
        }
305
        return true;
306
    }
307
 
73 ilm 308
    static private boolean AFTER_TX_DEFAULT = true;
309
 
310
    static public void setDefaultAfterTransaction(final boolean val) {
311
        AFTER_TX_DEFAULT = val;
312
    }
313
 
314
    static public final class ListenerAndConfig {
315
 
316
        private final SQLTableModifiedListener l;
132 ilm 317
        private final Boolean afterTx;
73 ilm 318
 
132 ilm 319
        /**
320
         * Create a new instance.
321
         *
322
         * @param l the listener.
323
         * @param afterTx <code>true</code> if <code>l</code> should only be called once a
324
         *        transaction is committed, <code>false</code> to be called in real-time (i.e.
325
         *        called a second time if a transaction is aborted), <code>null</code> to be called
326
         *        both in real-time and after the transaction succeeds.
327
         */
328
        public ListenerAndConfig(SQLTableModifiedListener l, Boolean afterTx) {
73 ilm 329
            super();
330
            if (l == null)
331
                throw new NullPointerException("Null listener");
332
            this.l = l;
333
            this.afterTx = afterTx;
334
        }
335
 
336
        public final SQLTableModifiedListener getListener() {
337
            return this.l;
338
        }
339
 
132 ilm 340
        public final Boolean callOnlyAfterTx() {
73 ilm 341
            return this.afterTx;
342
        }
343
 
344
        @Override
345
        public int hashCode() {
346
            final int prime = 31;
347
            int result = 1;
132 ilm 348
            result = prime * result + (this.afterTx == null ? 0 : this.afterTx.hashCode());
73 ilm 349
            result = prime * result + this.l.hashCode();
350
            return result;
351
        }
352
 
353
        @Override
354
        public boolean equals(Object obj) {
355
            if (this == obj)
356
                return true;
357
            if (obj == null)
358
                return false;
359
            if (getClass() != obj.getClass())
360
                return false;
361
            final ListenerAndConfig other = (ListenerAndConfig) obj;
132 ilm 362
            return CompareUtils.equals(this.afterTx, other.afterTx) && this.l.equals(other.l);
73 ilm 363
        }
364
    }
365
 
67 ilm 366
    @GuardedBy("this")
367
    private String version;
61 ilm 368
    private final CopyOnWriteMap<String, SQLField> fields;
369
    @GuardedBy("this")
142 ilm 370
    private Set<SQLField> primaryKeys;
17 ilm 371
    // the vast majority of our code use getKey(), so cache it for performance
61 ilm 372
    @GuardedBy("this")
17 ilm 373
    private SQLField primaryKey;
374
    // true if there's at most 1 primary key
61 ilm 375
    @GuardedBy("this")
17 ilm 376
    private boolean primaryKeyOK;
61 ilm 377
    @GuardedBy("this")
17 ilm 378
    private Set<SQLField> keys;
61 ilm 379
    @GuardedBy("this")
93 ilm 380
    private Map<String, FieldGroup> fieldsGroups;
381
    @GuardedBy("this")
17 ilm 382
    private final Map<String, Trigger> triggers;
383
    // null means it couldn't be retrieved
61 ilm 384
    @GuardedBy("this")
17 ilm 385
    private Set<Constraint> constraints;
386
    // always immutable so that fire can iterate safely ; to modify it, simply copy it before
387
    // (adding listeners is a lot less common than firing events)
63 ilm 388
    @GuardedBy("listenersMutex")
73 ilm 389
    private List<ListenerAndConfig> tableModifiedListeners;
390
    @GuardedBy("listenersMutex")
391
    private final ListMap<TransactionPoint, FireState> transactions;
392
    private final TransactionListener txListener;
63 ilm 393
    private final Object listenersMutex = new String("tableModifiedListeners mutex");
17 ilm 394
    // the id that foreign keys pointing to this, can use instead of NULL
395
    // a null value meaning not yet known
61 ilm 396
    @GuardedBy("this")
17 ilm 397
    private Integer undefinedID;
398
 
61 ilm 399
    @GuardedBy("this")
17 ilm 400
    private String comment;
61 ilm 401
    @GuardedBy("this")
17 ilm 402
    private String type;
403
 
404
    // empty table
405
    SQLTable(SQLSchema schema, String name) {
406
        super(schema, name);
407
        this.tableModifiedListeners = Collections.emptyList();
73 ilm 408
        this.transactions = new ListMap<TransactionPoint, FireState>();
409
        this.txListener = new TransactionListener() {
410
            @Override
411
            public void transactionEnded(TransactionPoint point) {
132 ilm 412
                fireFromTransaction(point);
73 ilm 413
            }
414
        };
61 ilm 415
        // needed for getOrderedFields()
416
        this.fields = new CopyOnWriteMap<String, SQLField>() {
417
            @Override
418
            public Map<String, SQLField> copy(Map<? extends String, ? extends SQLField> src) {
419
                return new LinkedHashMap<String, SQLField>(src);
420
            }
421
        };
422
        assert isOrdered(this.fields);
142 ilm 423
        this.primaryKeys = Collections.emptySet();
17 ilm 424
        this.primaryKey = null;
425
        this.primaryKeyOK = true;
426
        this.keys = null;
93 ilm 427
        this.fieldsGroups = null;
17 ilm 428
        this.triggers = new HashMap<String, Trigger>();
429
        // by default non-null, ie ok, only set to null on error
430
        this.constraints = new HashSet<Constraint>();
431
        // not known
432
        this.undefinedID = null;
67 ilm 433
        assert !this.undefinedIDKnown();
17 ilm 434
    }
435
 
436
    // *** setter
437
 
61 ilm 438
    synchronized void clearNonPersistent() {
17 ilm 439
        this.triggers.clear();
440
        // non-null, see ctor
441
        this.constraints = new HashSet<Constraint>();
442
    }
443
 
444
    // * from XML
445
 
446
    void loadFields(Element xml) {
67 ilm 447
        synchronized (this) {
448
            this.version = SQLSchema.getVersion(xml);
449
        }
450
 
17 ilm 451
        final LinkedHashMap<String, SQLField> newFields = new LinkedHashMap<String, SQLField>();
83 ilm 452
        for (final Element elementField : xml.getChildren("field")) {
17 ilm 453
            final SQLField f = SQLField.create(this, elementField);
454
            newFields.put(f.getName(), f);
455
        }
456
 
457
        final Element primary = xml.getChild("primary");
458
        final List<String> newPrimaryKeys = new ArrayList<String>();
83 ilm 459
        for (final Element elementField : primary.getChildren("field")) {
17 ilm 460
            final String fieldName = elementField.getAttributeValue("name");
461
            newPrimaryKeys.add(fieldName);
462
        }
463
 
61 ilm 464
        synchronized (getTreeMutex()) {
465
            synchronized (this) {
67 ilm 466
                this.setState(newFields, newPrimaryKeys, null);
17 ilm 467
 
61 ilm 468
                final Element triggersElem = xml.getChild("triggers");
469
                if (triggersElem != null)
83 ilm 470
                    for (final Element triggerElem : triggersElem.getChildren()) {
61 ilm 471
                        this.addTrigger(Trigger.fromXML(this, triggerElem));
472
                    }
17 ilm 473
 
61 ilm 474
                final Element constraintsElem = xml.getChild("constraints");
475
                if (constraintsElem == null)
476
                    this.addConstraint((Constraint) null);
477
                else
83 ilm 478
                    for (final Element elem : constraintsElem.getChildren()) {
61 ilm 479
                        this.addConstraint(Constraint.fromXML(this, elem));
480
                    }
481
 
482
                final Element commentElem = xml.getChild("comment");
483
                if (commentElem != null)
484
                    this.setComment(commentElem.getText());
485
                this.setType(xml.getAttributeValue("type"));
17 ilm 486
            }
61 ilm 487
        }
17 ilm 488
    }
489
 
61 ilm 490
    synchronized private void addTrigger(final Trigger t) {
17 ilm 491
        this.triggers.put(t.getName(), t);
492
    }
493
 
61 ilm 494
    synchronized private void addConstraint(final Constraint c) {
17 ilm 495
        if (c == null) {
496
            this.constraints = null;
497
        } else {
498
            if (this.constraints == null)
499
                this.constraints = new HashSet<Constraint>();
500
            this.constraints.add(c);
501
        }
502
    }
503
 
504
    // * from JDBC
505
 
506
    public void fetchFields() throws SQLException {
67 ilm 507
        this.getBase().fetchTables(TablesMap.createBySchemaFromTable(this));
61 ilm 508
    }
17 ilm 509
 
510
    /**
511
     * Fetch fields from the passed args.
512
     *
513
     * @param metaData the metadata.
514
     * @param rs the resultSet of a getColumns(), the cursor must be on a row.
67 ilm 515
     * @param version the version of the schema.
17 ilm 516
     * @return whether the <code>rs</code> has more row.
517
     * @throws SQLException if an error occurs.
518
     * @throws IllegalStateException if the current row of <code>rs</code> doesn't describe this.
519
     */
67 ilm 520
    boolean fetchFields(DatabaseMetaData metaData, ResultSet rs, String version) throws SQLException {
17 ilm 521
        if (!this.isUs(rs))
522
            throw new IllegalStateException("rs current row does not describe " + this);
523
 
61 ilm 524
        synchronized (getTreeMutex()) {
525
            synchronized (this) {
67 ilm 526
                this.version = version;
527
 
61 ilm 528
                // we need to match the database ordering of fields
529
                final LinkedHashMap<String, SQLField> newFields = new LinkedHashMap<String, SQLField>();
530
                // fields
531
                boolean hasNext = true;
532
                while (hasNext && this.isUs(rs)) {
533
                    final SQLField f = SQLField.create(this, rs);
534
                    newFields.put(f.getName(), f);
535
                    hasNext = rs.next();
536
                }
17 ilm 537
 
61 ilm 538
                final List<String> newPrimaryKeys = new ArrayList<String>();
539
                final ResultSet pkRS = metaData.getPrimaryKeys(this.getBase().getMDName(), this.getSchema().getName(), this.getName());
540
                while (pkRS.next()) {
541
                    newPrimaryKeys.add(pkRS.getString("COLUMN_NAME"));
542
                }
17 ilm 543
 
61 ilm 544
                this.setState(newFields, newPrimaryKeys, null);
17 ilm 545
 
61 ilm 546
                return hasNext;
547
            }
548
        }
17 ilm 549
    }
550
 
551
    void emptyFields() {
552
        this.setState(new LinkedHashMap<String, SQLField>(), Collections.<String> emptyList(), null);
553
    }
554
 
555
    private boolean isUs(final ResultSet rs) throws SQLException {
556
        final String n = rs.getString("TABLE_NAME");
557
        final String s = rs.getString("TABLE_SCHEM");
558
        return n.equals(this.getName()) && CompareUtils.equals(s, this.getSchema().getName());
559
    }
560
 
80 ilm 561
    void addTrigger(Map<String, Object> m) {
17 ilm 562
        this.addTrigger(new Trigger(this, m));
563
    }
564
 
565
    void addConstraint(Map<String, Object> m) {
566
        this.addConstraint(m == null ? null : new Constraint(this, m));
567
    }
568
 
569
    // must be called in setState() after fields have been set (for isRowable())
63 ilm 570
    private int fetchUndefID() {
151 ilm 571
        final int res;
63 ilm 572
        final SQLField pk;
573
        synchronized (this) {
574
            pk = isRowable() ? this.getKey() : null;
575
        }
576
        if (pk != null) {
61 ilm 577
            final Tuple2<Boolean, Number> currentValue = getUndefID(this.getSchema(), this.getName());
578
            if (!currentValue.get0()) {
151 ilm 579
                // no row
580
                res = this.findMinID(pk);
17 ilm 581
            } else {
582
                // a row
61 ilm 583
                final Number id = currentValue.get1();
17 ilm 584
                res = id == null ? SQLRow.NONEXISTANT_ID : id.intValue();
585
            }
586
        } else
587
            res = SQLRow.NONEXISTANT_ID;
588
        return res;
589
    }
590
 
591
    // no undef id found
63 ilm 592
    private int findMinID(SQLField pk) {
17 ilm 593
        final String debugUndef = "fwk_sql.debug.undefined_id";
594
        if (System.getProperty(debugUndef) != null)
595
            Log.get().warning("The system property '" + debugUndef + "' is deprecated, use the '" + UNDEFINED_ID_POLICY + "' metadata");
596
 
597
        final String policy = getSchema().getFwkMetadata(UNDEFINED_ID_POLICY);
598
        if (Boolean.getBoolean(debugUndef) || "min".equals(policy)) {
80 ilm 599
            final SQLSelect sel = new SQLSelect(true).addSelect(pk, "min");
17 ilm 600
            final Number undef = (Number) this.getBase().getDataSource().executeScalar(sel.asString());
601
            if (undef == null) {
602
                // empty table
603
                throw new IllegalStateException(this + " is empty, can not infer UNDEFINED_ID");
604
            } else {
142 ilm 605
                final SQLSyntax syntax = SQLSyntax.get(this);
606
                final String update = syntax.getInsertOne(new SQLName(this.getDBRoot().getName(), undefTable), Arrays.asList(UNDEF_TABLE_TABLENAME_FIELD, UNDEF_TABLE_ID_FIELD),
607
                        syntax.quoteString(this.getName()), String.valueOf(undef));
17 ilm 608
                Log.get().config("the first row (which should be the undefined):\n" + update);
609
                return undef.intValue();
610
            }
67 ilm 611
        } else if ("inDB".equals(policy)) {
174 ilm 612
            return SQLRow.NONEXISTANT_ID;
613
            // FIXME : BUG A INVESTIGUER DANS LA CREATION DE MODULE throw new
614
            // throw new IllegalStateException("Not in " + new SQLName(this.getDBRoot().getName(),
615
            // undefTable) + " : " + this.getName());
17 ilm 616
        } else if (policy != null && !"nonexistant".equals(policy)) {
617
            final int res = Integer.parseInt(policy);
618
            if (res < SQLRow.MIN_VALID_ID)
619
                throw new IllegalStateException("ID is not valid : " + res);
620
            return res;
621
        } else {
622
            // by default assume NULL is used
623
            return SQLRow.NONEXISTANT_ID;
624
        }
625
    }
626
 
627
    // * from Java
628
 
629
    void mutateTo(SQLTable table) {
61 ilm 630
        synchronized (getTreeMutex()) {
631
            synchronized (this) {
632
                this.clearNonPersistent();
67 ilm 633
                this.version = table.version;
61 ilm 634
                this.setState(table.fields, table.getPKsNames(), table.undefinedID);
83 ilm 635
                for (final Trigger t : table.triggers.values()) {
636
                    this.addTrigger(new Trigger(this, t));
637
                }
638
                if (table.constraints == null) {
61 ilm 639
                    this.constraints = null;
83 ilm 640
                } else {
641
                    for (final Constraint c : table.constraints) {
642
                        this.constraints.add(new Constraint(this, c));
643
                    }
61 ilm 644
                }
645
                this.setType(table.getType());
646
                this.setComment(table.getComment());
647
            }
17 ilm 648
        }
649
    }
650
 
651
    // * update attributes
652
 
61 ilm 653
    static private <K, V> boolean isOrdered(Map<K, V> m) {
654
        if (m instanceof CopyOnWriteMap)
655
            return isOrdered(((CopyOnWriteMap<K, V>) m).copy(Collections.<K, V> emptyMap()));
656
        return (m instanceof LinkedHashMap);
657
    }
658
 
83 ilm 659
    private void setState(Map<String, SQLField> fields, final List<String> primaryKeys, final Integer undef) {
61 ilm 660
        assert isOrdered(fields);
17 ilm 661
        // checks new fields' table (don't use ==, see below)
662
        for (final SQLField newField : fields.values()) {
663
            if (!newField.getTable().getSQLName().equals(this.getSQLName()))
664
                throw new IllegalArgumentException(newField + " is in table " + newField.getTable().getSQLName() + " not us: " + this.getSQLName());
665
        }
61 ilm 666
        synchronized (getTreeMutex()) {
667
            synchronized (this) {
668
                final CollectionChangeEventCreator c = this.createChildrenCreator();
17 ilm 669
 
61 ilm 670
                if (!fields.keySet().containsAll(this.getFieldsName())) {
671
                    for (String removed : CollectionUtils.substract(this.getFieldsName(), fields.keySet())) {
65 ilm 672
                        this.fields.remove(removed).dropped();
61 ilm 673
                    }
674
                }
17 ilm 675
 
61 ilm 676
                for (final SQLField newField : fields.values()) {
677
                    if (getChildrenNames().contains(newField.getName())) {
678
                        // re-use old instances by refreshing existing ones
679
                        this.getField(newField.getName()).mutateTo(newField);
680
                    } else {
681
                        final SQLField fieldToAdd;
682
                        // happens when the new structure is loaded in-memory
683
                        // before the current one is mutated to it
684
                        // (we already checked the fullname of the table)
685
                        if (newField.getTable() != this)
686
                            fieldToAdd = new SQLField(this, newField);
687
                        else
688
                            fieldToAdd = newField;
689
                        this.fields.put(newField.getName(), fieldToAdd);
690
                    }
691
                }
692
 
142 ilm 693
                // order matters (e.g. for indexes)
694
                final Set<SQLField> newPK = new LinkedHashSet<SQLField>();
61 ilm 695
                for (final String pk : primaryKeys)
142 ilm 696
                    newPK.add(this.getField(pk));
697
                this.primaryKeys = Collections.unmodifiableSet(newPK);
61 ilm 698
                this.primaryKey = primaryKeys.size() == 1 ? this.getField(primaryKeys.get(0)) : null;
699
                this.primaryKeyOK = primaryKeys.size() <= 1;
700
 
93 ilm 701
                this.keys = null;
702
                this.fieldsGroups = null;
703
 
61 ilm 704
                // don't fetch the ID now as it could be too early (e.g. we just created the table
705
                // but haven't inserted the undefined row)
706
                this.undefinedID = undef;
707
                this.fireChildrenChanged(c);
17 ilm 708
            }
709
        }
710
    }
711
 
712
    // *** getter
713
 
61 ilm 714
    synchronized void setType(String type) {
17 ilm 715
        this.type = type;
716
    }
717
 
61 ilm 718
    public synchronized final String getType() {
17 ilm 719
        return this.type;
720
    }
721
 
61 ilm 722
    synchronized void setComment(String comm) {
17 ilm 723
        this.comment = comm;
724
    }
725
 
61 ilm 726
    public synchronized final String getComment() {
17 ilm 727
        return this.comment;
728
    }
729
 
61 ilm 730
    public synchronized final Trigger getTrigger(String name) {
17 ilm 731
        return this.triggers.get(name);
732
    }
733
 
61 ilm 734
    public synchronized final Map<String, Trigger> getTriggers() {
17 ilm 735
        return Collections.unmodifiableMap(this.triggers);
736
    }
737
 
738
    /**
739
     * The constraints on this table.
740
     *
741
     * @return the constraints or <code>null</code> if they couldn't be retrieved.
742
     */
61 ilm 743
    public synchronized final Set<Constraint> getAllConstraints() {
17 ilm 744
        return this.constraints == null ? null : Collections.unmodifiableSet(this.constraints);
745
    }
746
 
747
    /**
83 ilm 748
     * The CHECK and UNIQUE constraints on this table. This is useful since types
749
     * {@link ConstraintType#FOREIGN_KEY FOREIGN_KEY} and {@link ConstraintType#PRIMARY_KEY
132 ilm 750
     * PRIMARY_KEY} are already available through {@link #getForeignLinks()} and
83 ilm 751
     * {@link #getPrimaryKeys()} ; type {@link ConstraintType#DEFAULT DEFAULT} through
752
     * {@link SQLField#getDefaultValue()}.
41 ilm 753
     *
754
     * @return the constraints or <code>null</code> if they couldn't be retrieved.
755
     */
61 ilm 756
    public synchronized final Set<Constraint> getConstraints() {
41 ilm 757
        if (this.constraints == null)
758
            return null;
759
        final Set<Constraint> res = new HashSet<Constraint>();
760
        for (final Constraint c : this.constraints) {
83 ilm 761
            if (c.getType() == ConstraintType.CHECK || c.getType() == ConstraintType.UNIQUE) {
41 ilm 762
                res.add(c);
763
            }
764
        }
765
        return res;
766
    }
767
 
768
    /**
17 ilm 769
     * Returns a specific constraint.
770
     *
771
     * @param type type of constraint, e.g. {@link ConstraintType#UNIQUE}.
772
     * @param cols the fields names, e.g. ["NAME"].
773
     * @return the matching constraint, <code>null</code> if it cannot be found or if constraints
774
     *         couldn't be retrieved.
775
     */
144 ilm 776
    public final Constraint getConstraint(ConstraintType type, List<String> cols) {
777
        if (type == null)
778
            throw new IllegalArgumentException("Missing type");
779
        final Set<Constraint> res = this.getConstraints(type, cols, false);
780
        if (res == null)
781
            return null;
782
        if (res.size() > 1)
783
            throw new IllegalStateException("More than one constraint matches (use getConstraints()) : " + res);
784
        return CollectionUtils.getSole(res);
785
    }
786
 
787
    /**
788
     * Search for constraints.
789
     *
790
     * @param type type of constraint, can be <code>null</code>.
791
     * @param cols the fields names, not <code>null</code>.
792
     * @param containing <code>true</code> if {@link Constraint#getCols() constraints columns}
793
     *        should {@link Collection#containsAll(Collection) contains all} the passed columns,
794
     *        <code>false</code> if they should be equal.
795
     * @return the matching constraints, <code>null</code> if constraints couldn't be retrieved.
796
     */
797
    public synchronized final Set<Constraint> getConstraints(ConstraintType type, List<String> cols, final boolean containing) {
798
        if (cols == null)
799
            throw new IllegalArgumentException("Missing columns");
17 ilm 800
        if (this.constraints == null)
801
            return null;
144 ilm 802
        final Set<Constraint> res = new HashSet<>();
17 ilm 803
        for (final Constraint c : this.constraints) {
144 ilm 804
            if ((type == null || c.getType() == type) && (containing ? c.getCols().containsAll(cols) : c.getCols().equals(cols))) {
805
                res.add(c);
17 ilm 806
            }
807
        }
144 ilm 808
        return res;
17 ilm 809
    }
810
 
811
    /**
812
     * Whether rows of this table can be represented as SQLRow.
813
     *
814
     * @return <code>true</code> if rows of this table can be represented as SQLRow.
815
     */
61 ilm 816
    public synchronized boolean isRowable() {
17 ilm 817
        return this.getPrimaryKeys().size() == 1 && Number.class.isAssignableFrom(this.getKey().getType().getJavaType());
818
    }
819
 
820
    public SQLSchema getSchema() {
821
        return (SQLSchema) this.getParent();
822
    }
823
 
824
    public SQLBase getBase() {
825
        return this.getSchema().getBase();
826
    }
827
 
73 ilm 828
    synchronized final String getVersion() {
829
        return this.version;
830
    }
831
 
17 ilm 832
    /**
833
     * Return the primary key of this table.
834
     *
835
     * @return the field which is the key of this table, or <code>null</code> if it doesn't exist.
836
     * @throws IllegalStateException if there's more than one primary key.
837
     */
65 ilm 838
    @Override
61 ilm 839
    public synchronized SQLField getKey() {
17 ilm 840
        if (!this.primaryKeyOK)
841
            throw new IllegalStateException(this + " has more than 1 primary key: " + this.getPrimaryKeys());
842
        return this.primaryKey;
843
    }
844
 
845
    /**
182 ilm 846
     * Return the fields of the primary key.
17 ilm 847
     *
182 ilm 848
     * @return the fields of the primary key of this table, can be empty.
17 ilm 849
     */
182 ilm 850
    public synchronized Set<SQLField> getPrimaryKeyFields() {
142 ilm 851
        return this.primaryKeys;
17 ilm 852
    }
853
 
182 ilm 854
    @Deprecated
855
    public final Set<SQLField> getPrimaryKeys() {
856
        return this.getPrimaryKeyFields();
857
    }
858
 
859
    public final List<String> getPKsNames() {
860
        return this.getPKsNames(new ArrayList<String>());
861
    }
862
 
863
    public final <C extends Collection<String>> C getPKsNames(C pks) {
864
        for (final SQLField f : this.getPrimaryKeys()) {
865
            pks.add(f.getName());
866
        }
867
        return pks;
868
    }
869
 
870
    public final RowRef createRowRef(final Object... pk) {
871
        return this.createRowRef(Arrays.asList(pk));
872
    }
873
 
874
    public final RowRef createRowRef(final List<?> pk) {
875
        return new RowRef(this, pk);
876
    }
877
 
878
    public final RowRef createRowRef(final Number id) {
879
        return new RowRef(this, id);
880
    }
881
 
132 ilm 882
    public final Set<Link> getForeignLinks() {
883
        return this.getDBSystemRoot().getGraph().getForeignLinks(this);
884
    }
885
 
17 ilm 886
    /**
887
     * Return the foreign keys of this table.
888
     *
889
     * @return a Set of SQLField which are foreign keys of this table.
890
     */
891
    public Set<SQLField> getForeignKeys() {
892
        return this.getDBSystemRoot().getGraph().getForeignKeys(this);
893
    }
894
 
895
    public Set<String> getForeignKeysNames() {
132 ilm 896
        return DatabaseGraph.getNames(this.getForeignLinks());
17 ilm 897
    }
898
 
899
    public Set<List<SQLField>> getForeignKeysFields() {
900
        return this.getDBSystemRoot().getGraph().getForeignKeysFields(this);
901
    }
902
 
903
    public Set<SQLField> getForeignKeys(String foreignTable) {
904
        return this.getForeignKeys(this.getTable(foreignTable));
905
    }
906
 
907
    public Set<SQLField> getForeignKeys(SQLTable foreignTable) {
908
        return this.getDBSystemRoot().getGraph().getForeignFields(this, foreignTable);
909
    }
910
 
911
    public SQLTable getForeignTable(String foreignField) {
65 ilm 912
        return this.getField(foreignField).getForeignTable();
17 ilm 913
    }
914
 
19 ilm 915
    public SQLTable findReferentTable(String tableName) {
916
        return this.getDBSystemRoot().getGraph().findReferentTable(this, tableName);
917
    }
918
 
17 ilm 919
    /**
920
     * Renvoie toutes les clefs de cette table. C'est à dire les clefs primaires plus les clefs
921
     * externes.
922
     *
923
     * @return toutes les clefs de cette table, can be empty.
924
     */
61 ilm 925
    public synchronized Set<SQLField> getKeys() {
17 ilm 926
        if (this.keys == null) {
132 ilm 927
            this.keys = this.getFields(VirtualFields.KEYS);
17 ilm 928
        }
929
        return this.keys;
930
    }
931
 
93 ilm 932
    @Immutable
933
    static public final class FieldGroup {
934
        private final String field;
935
        private final SQLKey key;
936
 
937
        private FieldGroup(final SQLKey key, final String field) {
938
            assert (key == null) != (field == null);
939
            this.key = key;
940
            this.field = key == null ? field : CollectionUtils.getSole(key.getFields());
941
        }
942
 
943
        /**
944
         * The key type for this group.
945
         *
946
         * @return the key type, <code>null</code> for a simple field.
947
         */
948
        public final Type getKeyType() {
949
            if (this.key == null)
950
                return null;
951
            return this.key.getType();
952
        }
953
 
954
        /**
955
         * The key for this group.
956
         *
957
         * @return the key, <code>null</code> for a simple field.
958
         */
156 ilm 959
        public final SQLKey getRawKey() {
93 ilm 960
            return this.key;
961
        }
962
 
963
        /**
156 ilm 964
         * The key for this group.
965
         *
966
         * @return the key, never <code>null</code>.
967
         * @throws IllegalStateException for a simple field.
968
         */
969
        public final SQLKey getKey() throws IllegalStateException {
970
            if (this.key == null)
971
                throw new IllegalStateException("Not a key : " + this);
972
            return this.key;
973
        }
974
 
975
        /**
976
         * The foreign link for this group.
977
         *
978
         * @return the foreign link, never <code>null</code>.
979
         * @throws IllegalStateException for a simple field or a primary key.
980
         */
981
        public final Link getForeignLink() throws IllegalStateException {
982
            if (this.key != null) {
983
                final Link foreignLink = this.key.getForeignLink();
984
                if (foreignLink != null)
985
                    return foreignLink;
986
            }
987
            throw new IllegalStateException("Not a foreign key : " + this);
988
        }
989
 
990
        /**
93 ilm 991
         * The one and only field of this group.
992
         *
993
         * @return the only field of this group, only <code>null</code> if this group is a
994
         *         {@link #getKey() key} with more than one field.
995
         */
996
        public String getSingleField() {
997
            return this.field;
998
        }
999
 
1000
        public final List<String> getFields() {
1001
            return this.key == null ? Arrays.asList(this.field) : this.key.getFields();
1002
        }
1003
 
1004
        @Override
1005
        public String toString() {
1006
            return this.getClass().getSimpleName() + " " + (this.key != null ? this.key.toString() : this.field);
1007
        }
1008
    }
1009
 
1010
    /**
1011
     * Return the fields grouped by key.
1012
     *
1013
     * @return for each field of this table the matching group.
1014
     */
1015
    public synchronized Map<String, FieldGroup> getFieldGroups() {
1016
        if (this.fieldsGroups == null) {
1017
            final Map<String, FieldGroup> res = new LinkedHashMap<String, FieldGroup>();
1018
            // set order
1019
            for (final String field : this.getFieldsName()) {
1020
                res.put(field, new FieldGroup(null, field));
1021
            }
132 ilm 1022
            for (final Link l : this.getForeignLinks()) {
93 ilm 1023
                indexKey(res, SQLKey.createForeignKey(l));
1024
            }
1025
            final SQLKey pk = SQLKey.createPrimaryKey(this);
1026
            if (pk != null)
1027
                indexKey(res, pk);
1028
 
1029
            this.fieldsGroups = Collections.unmodifiableMap(res);
1030
        }
1031
        return this.fieldsGroups;
1032
    }
1033
 
1034
    static private final void indexKey(final Map<String, FieldGroup> m, final SQLKey k) {
1035
        final FieldGroup group = new FieldGroup(k, null);
1036
        for (final String field : k.getFields()) {
1037
            final FieldGroup previous = m.put(field, group);
1038
            assert previous.getKeyType() == null;
1039
        }
1040
    }
1041
 
17 ilm 1042
    public String toString() {
1043
        return "/" + this.getName() + "/";
1044
    }
1045
 
1046
    /**
1047
     * Return the field named <i>fieldName </i> in this table.
1048
     *
1049
     * @param fieldName the name of the field.
1050
     * @return the matching field, never <code>null</code>.
1051
     * @throws IllegalArgumentException if the field is not in this table.
1052
     * @see #getFieldRaw(String)
1053
     */
65 ilm 1054
    @Override
17 ilm 1055
    public SQLField getField(String fieldName) {
1056
        SQLField res = this.getFieldRaw(fieldName);
19 ilm 1057
        if (res == null) {
61 ilm 1058
            throw new IllegalArgumentException("unknown field " + fieldName + " in " + this.getName() + ". The table " + this.getName() + " contains the followins fields: " + this.getFieldsName());
19 ilm 1059
        }
17 ilm 1060
        return res;
1061
    }
1062
 
1063
    /**
1064
     * Return the field named <i>fieldName</i> in this table.
1065
     *
1066
     * @param fieldName the name of the field.
1067
     * @return the matching field or <code>null</code> if none exists.
1068
     */
1069
    public SQLField getFieldRaw(String fieldName) {
61 ilm 1070
        return this.fields.get(fieldName);
17 ilm 1071
    }
1072
 
1073
    /**
1074
     * Return all the fields in this table.
1075
     *
1076
     * @return a Set of the fields.
1077
     */
1078
    public Set<SQLField> getFields() {
1079
        return new HashSet<SQLField>(this.fields.values());
1080
    }
1081
 
132 ilm 1082
    /**
1083
     * An immutable set of fields.
1084
     *
1085
     * @author Sylvain
1086
     */
1087
    @Immutable
1088
    static public final class VirtualFields {
1089
 
151 ilm 1090
        // must be set first, as they are used by others (e.g. create())
1091
        static public final VirtualFields NONE = new VirtualFields(EnumSet.noneOf(VirtualFieldPartition.class));
1092
        static public final VirtualFields ALL = new VirtualFields(EnumSet.allOf(VirtualFieldPartition.class));
1093
 
132 ilm 1094
        static public final VirtualFields ORDER = new VirtualFields(VirtualFieldPartition.ORDER);
1095
        static public final VirtualFields ARCHIVE = new VirtualFields(VirtualFieldPartition.ARCHIVE);
1096
        static public final VirtualFields METADATA = new VirtualFields(VirtualFieldPartition.METADATA);
1097
        static public final VirtualFields PRIMARY_KEY = new VirtualFields(VirtualFieldPartition.PRIMARY_KEY);
1098
        static public final VirtualFields FOREIGN_KEYS = new VirtualFields(VirtualFieldPartition.FOREIGN_KEYS);
1099
        /**
1100
         * All specific fields of this table without keys.
1101
         */
1102
        static public final VirtualFields LOCAL_CONTENT = new VirtualFields(VirtualFieldPartition.LOCAL_CONTENT);
1103
 
1104
        /**
1105
         * {@link #LOCAL_CONTENT local content fields} with {@link #FOREIGN_KEYS}.
1106
         */
1107
        static public final VirtualFields CONTENT = LOCAL_CONTENT.union(FOREIGN_KEYS);
1108
        /**
1109
         * {@link #CONTENT content fields} with {@link #METADATA}.
1110
         */
1111
        static public final VirtualFields CONTENT_AND_METADATA = CONTENT.union(METADATA);
1112
        /**
1113
         * {@link #PRIMARY_KEY} with {@link #FOREIGN_KEYS}.
1114
         */
1115
        static public final VirtualFields KEYS = PRIMARY_KEY.union(FOREIGN_KEYS);
1116
 
151 ilm 1117
        static private VirtualFields create(final EnumSet<VirtualFieldPartition> set) {
1118
            if (set.isEmpty())
1119
                return NONE;
1120
            else if (set.equals(ALL.set))
1121
                return ALL;
1122
            else
1123
                return new VirtualFields(set);
1124
        }
1125
 
132 ilm 1126
        private final EnumSet<VirtualFieldPartition> set;
1127
 
1128
        // use constants above
1129
        private VirtualFields(final VirtualFieldPartition single) {
1130
            this(EnumSet.of(single));
1131
        }
1132
 
1133
        // private since parameter is not copied
1134
        private VirtualFields(final EnumSet<VirtualFieldPartition> set) {
1135
            if (set == null)
1136
                throw new NullPointerException("Null set");
1137
            this.set = set;
1138
        }
1139
 
151 ilm 1140
        public final boolean contains(VirtualFields other) {
1141
            return this == other || this == ALL || this != NONE && this.set.containsAll(other.set);
1142
        }
1143
 
132 ilm 1144
        public final VirtualFields union(VirtualFields... other) {
151 ilm 1145
            // optimizations
1146
            if (this == ALL || other.length == 0 || other.length == 1 && this.contains(other[0]))
1147
                return this;
1148
            if (other.length == 1 && other[0].contains(this))
1149
                return other[0];
1150
 
132 ilm 1151
            final EnumSet<VirtualFieldPartition> set = this.set.clone();
1152
            for (final VirtualFields o : other)
1153
                set.addAll(o.set);
151 ilm 1154
            return create(set);
132 ilm 1155
        }
1156
 
1157
        public final VirtualFields intersection(VirtualFields... other) {
151 ilm 1158
            // optimizations
1159
            if (this == NONE || other.length == 0 || other.length == 1 && other[0].contains(this))
1160
                return this;
1161
            if (other.length == 1 && this.contains(other[0]))
1162
                return other[0];
1163
 
132 ilm 1164
            final EnumSet<VirtualFieldPartition> set = this.set.clone();
1165
            for (final VirtualFields o : other)
1166
                set.retainAll(o.set);
151 ilm 1167
            return create(set);
132 ilm 1168
        }
1169
 
1170
        public final VirtualFields difference(VirtualFields... other) {
151 ilm 1171
            // optimizations
1172
            if (this == NONE || other.length == 0 || other.length == 1 && Collections.disjoint(this.set, other[0].set))
1173
                return this;
1174
 
132 ilm 1175
            final EnumSet<VirtualFieldPartition> set = this.set.clone();
1176
            for (final VirtualFields o : other)
1177
                set.removeAll(o.set);
151 ilm 1178
            return create(set);
132 ilm 1179
        }
1180
 
1181
        public final VirtualFields complement() {
1182
            // optimizations
1183
            if (this == ALL)
1184
                return NONE;
1185
            else if (this == NONE)
1186
                return ALL;
1187
            return new VirtualFields(EnumSet.complementOf(this.set));
1188
        }
1189
 
1190
        @Override
1191
        public int hashCode() {
1192
            final int prime = 31;
1193
            return prime + this.set.hashCode();
1194
        }
1195
 
1196
        @Override
1197
        public boolean equals(Object obj) {
1198
            if (this == obj)
1199
                return true;
1200
            if (obj == null)
1201
                return false;
1202
            if (getClass() != obj.getClass())
1203
                return false;
1204
            final VirtualFields other = (VirtualFields) obj;
1205
            return this.set.equals(other.set);
1206
        }
151 ilm 1207
 
1208
        @Override
1209
        public String toString() {
1210
            return this.getClass().getSimpleName() + " " + this.set;
1211
        }
132 ilm 1212
    }
1213
 
1214
    /**
1215
     * A partition of the fields (except that some can be empty). Being a partition allow to use
1216
     * {@link EnumSet#complementOf(EnumSet)}.
1217
     *
1218
     * @author Sylvain
1219
     * @see VirtualFields
1220
     */
1221
    static public enum VirtualFieldPartition {
83 ilm 1222
        ORDER {
1223
            @Override
132 ilm 1224
            Set<SQLField> getFields(SQLTable t) {
83 ilm 1225
                final SQLField orderField = t.getOrderField();
1226
                return orderField == null ? Collections.<SQLField> emptySet() : Collections.singleton(orderField);
1227
            }
1228
        },
1229
        ARCHIVE {
1230
            @Override
1231
            Set<SQLField> getFields(SQLTable t) {
1232
                final SQLField f = t.getArchiveField();
1233
                return f == null ? Collections.<SQLField> emptySet() : Collections.singleton(f);
1234
            }
1235
        },
1236
        METADATA {
1237
            @Override
1238
            Set<SQLField> getFields(SQLTable t) {
1239
                final Set<SQLField> res = new HashSet<SQLField>(4);
1240
                res.add(t.getCreationDateField());
1241
                res.add(t.getCreationUserField());
1242
                res.add(t.getModifDateField());
1243
                res.add(t.getModifUserField());
1244
                res.remove(null);
1245
                return res;
1246
            }
1247
        },
1248
        PRIMARY_KEY {
1249
            @Override
1250
            Set<SQLField> getFields(SQLTable t) {
1251
                return t.getPrimaryKeys();
1252
            }
1253
        },
1254
        FOREIGN_KEYS {
1255
            @Override
132 ilm 1256
            Set<SQLField> getFields(SQLTable t) {
1257
                return DatabaseGraph.getColsUnion(t.getForeignLinks());
83 ilm 1258
            }
93 ilm 1259
        },
1260
        /**
1261
         * {@link #CONTENT content fields} without {@link #FOREIGN_KEYS}
1262
         */
1263
        LOCAL_CONTENT {
1264
            @Override
132 ilm 1265
            Set<SQLField> getFields(SQLTable t) {
1266
                throw new IllegalStateException(this + " is any field not in another set");
93 ilm 1267
            }
83 ilm 1268
        };
1269
 
1270
        abstract Set<SQLField> getFields(final SQLTable t);
1271
    }
1272
 
132 ilm 1273
    public final Set<SQLField> getFields(final VirtualFields vfs) {
1274
        return getFields(vfs.set);
83 ilm 1275
    }
1276
 
132 ilm 1277
    final Set<SQLField> getFields(final Set<VirtualFieldPartition> vf) {
1278
        if (vf.isEmpty())
1279
            return Collections.emptySet();
144 ilm 1280
        else if (vf.equals(VirtualFields.ALL.set))
1281
            return Collections.unmodifiableSet(this.getFields());
83 ilm 1282
 
132 ilm 1283
        final Set<SQLField> res;
1284
        // LOCAL_CONTENT is just ALL minus every other set
1285
        if (!vf.contains(VirtualFieldPartition.LOCAL_CONTENT)) {
1286
            res = new HashSet<SQLField>();
1287
            for (final VirtualFieldPartition v : vf) {
1288
                res.addAll(v.getFields(this));
83 ilm 1289
            }
132 ilm 1290
        } else {
1291
            res = this.getFields();
1292
            // don't use EnumSet.complementOf(EnumSet.copyOf()) as it makes multiple copies
1293
            for (final VirtualFieldPartition v : VirtualFieldPartition.values()) {
1294
                if (!vf.contains(v)) {
1295
                    res.removeAll(v.getFields(this));
1296
                }
1297
            }
83 ilm 1298
        }
142 ilm 1299
        return Collections.unmodifiableSet(res);
83 ilm 1300
    }
1301
 
132 ilm 1302
    public final Set<String> getFieldsNames(final VirtualFields vfs) {
144 ilm 1303
        if (vfs.equals(VirtualFields.ALL))
1304
            return this.getFieldsName();
132 ilm 1305
        final Set<String> res = new HashSet<String>();
1306
        for (final SQLField f : this.getFields(vfs)) {
1307
            res.add(f.getName());
83 ilm 1308
        }
1309
        return res;
1310
    }
1311
 
142 ilm 1312
    public final List<SQLField> getFields(final Collection<String> names) {
1313
        return this.getFields(names, new ArrayList<SQLField>());
1314
    }
1315
 
1316
    public final <T extends Collection<SQLField>> T getFields(final Collection<String> names, final T res) {
1317
        return this.getFields(names, res, true);
1318
    }
1319
 
1320
    public final <T extends Collection<SQLField>> T getFields(final Collection<String> names, final T res, final boolean required) {
1321
        for (final String name : names) {
1322
            final SQLField f = required ? this.getField(name) : this.getFieldRaw(name);
1323
            if (f != null)
1324
                res.add(f);
1325
        }
1326
        return res;
1327
    }
1328
 
17 ilm 1329
    /**
1330
     * Retourne les champs du contenu de cette table. C'est à dire ni la clef primaire, ni les
1331
     * champs d'archive et d'ordre.
1332
     *
1333
     * @return les champs du contenu de cette table.
132 ilm 1334
     * @see VirtualFields#CONTENT
17 ilm 1335
     */
1336
    public Set<SQLField> getContentFields() {
1337
        return this.getContentFields(false);
1338
    }
1339
 
61 ilm 1340
    public synchronized Set<SQLField> getContentFields(final boolean includeMetadata) {
132 ilm 1341
        return this.getFields(includeMetadata ? VirtualFields.CONTENT_AND_METADATA : VirtualFields.CONTENT);
17 ilm 1342
    }
1343
 
1344
    /**
1345
     * Retourne les champs du contenu local de cette table. C'est à dire uniquement les champs du
1346
     * contenu qui ne sont pas des clefs externes.
1347
     *
1348
     * @return les champs du contenu local de cette table.
1349
     * @see #getContentFields()
1350
     */
61 ilm 1351
    public synchronized Set<SQLField> getLocalContentFields() {
132 ilm 1352
        return this.getFields(VirtualFields.LOCAL_CONTENT);
17 ilm 1353
    }
1354
 
1355
    /**
1356
     * Return the names of all the fields.
1357
     *
1358
     * @return the names of all the fields.
1359
     */
1360
    public Set<String> getFieldsName() {
1361
        return this.fields.keySet();
1362
    }
1363
 
1364
    /**
1365
     * Return all the fields in this table. The order is the same across reboot.
1366
     *
1367
     * @return a List of the fields.
1368
     */
1369
    public List<SQLField> getOrderedFields() {
1370
        return new ArrayList<SQLField>(this.fields.values());
1371
    }
1372
 
1373
    @Override
61 ilm 1374
    public Map<String, SQLField> getChildrenMap() {
1375
        return this.fields.getImmutable();
17 ilm 1376
    }
1377
 
1378
    public final SQLTable getTable(String name) {
1379
        return this.getDesc(name, SQLTable.class);
1380
    }
1381
 
1382
    /**
1383
     * Retourne le nombre total de lignes contenues dans cette table.
1384
     *
1385
     * @return le nombre de lignes de cette table.
1386
     */
1387
    public int getRowCount() {
67 ilm 1388
        return this.getRowCount(true);
1389
    }
1390
 
1391
    public int getRowCount(final boolean includeUndefined) {
83 ilm 1392
        return this.getRowCount(includeUndefined, ArchiveMode.BOTH);
1393
    }
1394
 
1395
    public int getRowCount(final boolean includeUndefined, final ArchiveMode archiveMode) {
67 ilm 1396
        final SQLSelect sel = new SQLSelect(true).addSelectFunctionStar("count").addFrom(this);
1397
        sel.setExcludeUndefined(!includeUndefined);
83 ilm 1398
        sel.setArchivedPolicy(archiveMode);
17 ilm 1399
        final Number count = (Number) this.getBase().getDataSource().execute(sel.asString(), new IResultSetHandler(SQLDataSource.SCALAR_HANDLER, false));
1400
        return count.intValue();
1401
    }
1402
 
1403
    /**
1404
     * The maximum value of the order field.
1405
     *
1406
     * @return the maximum value of the order field, or -1 if this table is empty.
1407
     */
1408
    public BigDecimal getMaxOrder() {
1409
        return this.getMaxOrder(true);
1410
    }
1411
 
83 ilm 1412
    public BigDecimal getMaxOrder(Boolean useCache) {
1413
        final SQLField orderField = this.getOrderField();
1414
        if (orderField == null)
17 ilm 1415
            throw new IllegalStateException(this + " is not ordered");
83 ilm 1416
        final SQLSelect sel = new SQLSelect(true).addSelect(orderField, "max");
17 ilm 1417
        try {
1418
            final BigDecimal maxOrder = (BigDecimal) this.getBase().getDataSource().execute(sel.asString(), new IResultSetHandler(SQLDataSource.SCALAR_HANDLER, useCache));
1419
            return maxOrder == null ? BigDecimal.ONE.negate() : maxOrder;
1420
        } catch (ClassCastException e) {
144 ilm 1421
            throw new IllegalStateException(orderField.getSQLName() + " must be " + SQLSyntax.get(this).getOrderDefinition(false), e);
17 ilm 1422
        }
1423
    }
1424
 
1425
    /**
1426
     * Retourne la ligne correspondant à l'ID passé.
1427
     *
1428
     * @param ID l'identifiant de la ligne à retourner.
1429
     * @return une ligne existant dans la base sinon <code>null</code>.
1430
     * @see #getValidRow(int)
1431
     */
1432
    public SQLRow getRow(int ID) {
1433
        SQLRow row = this.getUncheckedRow(ID);
1434
        return row.exists() ? row : null;
1435
    }
1436
 
1437
    /**
1438
     * Retourne une la ligne demandée sans faire aucune vérification.
1439
     *
1440
     * @param ID l'identifiant de la ligne à retourner.
1441
     * @return la ligne demandée, jamais <code>null</code>.
1442
     */
1443
    private SQLRow getUncheckedRow(int ID) {
1444
        return new SQLRow(this, ID);
1445
    }
1446
 
1447
    /**
1448
     * Retourne la ligne valide correspondant à l'ID passé.
1449
     *
1450
     * @param ID l'identifiant de la ligne à retourner.
1451
     * @return une ligne existante et non archivée dans la base sinon <code>null</code>.
1452
     * @see SQLRow#isValid()
1453
     */
1454
    public SQLRow getValidRow(int ID) {
1455
        SQLRow row = this.getRow(ID);
1456
        return row.isValid() ? row : null;
1457
    }
1458
 
1459
    /**
1460
     * Vérifie la validité de cet ID. C'est à dire qu'il existe une ligne non archivée avec cet ID,
1461
     * dans cette table.
1462
     *
1463
     * @param ID l'identifiant.
1464
     * @return <code>null</code> si l'ID est valide, sinon une SQLRow qui est soit inexistante, soit
1465
     *         archivée.
1466
     */
1467
    public SQLRow checkValidity(int ID) {
144 ilm 1468
        // don't bother locking if we're outside a transaction
1469
        final SQLRow row = SQLRow.createFromSelect(this, VirtualFields.PRIMARY_KEY.union(VirtualFields.ARCHIVE), ID, LockStrength.NONE);
17 ilm 1470
        // l'inverse de getValidRow()
1471
        return row.isValid() ? null : row;
1472
    }
1473
 
1474
    /**
1475
     * Vérifie cette table est intègre. C'est à dire que toutes ses clefs externes pointent sur des
1476
     * lignes existantes et non effacées. Cette méthode retourne une liste constituée de triplet :
1477
     * SQLRow (la ligne incohérente), SQLField (le champ incohérent), SQLRow (la ligne invalide de
1478
     * la table étrangère).
1479
     *
61 ilm 1480
     * @return a list of inconsistencies or <code>null</code> if this table is not rowable.
17 ilm 1481
     */
61 ilm 1482
    public List<Tuple3<SQLRow, SQLField, SQLRow>> checkIntegrity() {
1483
        final SQLField pk;
1484
        final Set<SQLField> fks;
1485
        synchronized (this) {
1486
            if (!this.isRowable())
1487
                return null;
1488
            pk = this.getKey();
1489
            fks = this.getForeignKeys();
1490
        }
17 ilm 1491
 
61 ilm 1492
        final List<Tuple3<SQLRow, SQLField, SQLRow>> inconsistencies = new ArrayList<Tuple3<SQLRow, SQLField, SQLRow>>();
17 ilm 1493
        // si on a pas de relation externe, c'est OK
61 ilm 1494
        if (!fks.isEmpty()) {
80 ilm 1495
            final SQLSelect sel = new SQLSelect();
17 ilm 1496
            // on ne vérifie pas les lignes archivées mais l'indéfinie oui.
1497
            sel.setExcludeUndefined(false);
61 ilm 1498
            sel.addSelect(pk);
1499
            sel.addAllSelect(fks);
17 ilm 1500
            this.getBase().getDataSource().execute(sel.asString(), new ResultSetHandler() {
1501
                public Object handle(ResultSet rs) throws SQLException {
1502
                    while (rs.next()) {
61 ilm 1503
                        for (final SQLField fk : fks) {
1504
                            final SQLRow pb = SQLTable.this.checkValidity(fk.getName(), rs.getInt(fk.getFullName()));
17 ilm 1505
                            if (pb != null) {
61 ilm 1506
                                final SQLRow row = SQLTable.this.getRow(rs.getInt(pk.getFullName()));
1507
                                inconsistencies.add(Tuple3.create(row, fk, pb));
17 ilm 1508
                            }
1509
                        }
1510
                    }
1511
                    // on s'en sert pas
1512
                    return null;
1513
                }
1514
            });
1515
        }
1516
 
1517
        return inconsistencies;
1518
    }
1519
 
1520
    /**
1521
     * Vérifie que l'on peut affecter <code>foreignID</code> au champ <code>foreignKey</code> de
1522
     * cette table. C'est à dire vérifie que la table sur laquelle pointe <code>foreignKey</code>
1523
     * contient bien une ligne d'ID <code>foreignID</code> et de plus qu'elle n'a pas été archivée.
1524
     *
1525
     * @param foreignKey le nom du champ.
1526
     * @param foreignID l'ID que l'on souhaite tester.
1527
     * @return une SQLRow décrivant l'incohérence ou <code>null</code> sinon.
1528
     * @throws IllegalArgumentException si le champ passé n'est pas une clef étrangère.
1529
     * @see #checkValidity(int)
1530
     */
1531
    public SQLRow checkValidity(String foreignKey, int foreignID) {
1532
        final SQLField fk = this.getField(foreignKey);
1533
        final SQLTable foreignTable = this.getDBSystemRoot().getGraph().getForeignTable(fk);
1534
        if (foreignTable == null)
1535
            throw new IllegalArgumentException("Impossible de tester '" + foreignKey + "' avec " + foreignID + " dans " + this + ". Ce n'est pas une clef étrangère.");
1536
        return foreignTable.checkValidity(foreignID);
1537
    }
1538
 
1539
    public SQLRow checkValidity(String foreignKey, Number foreignID) {
1540
        // NULL is valid
1541
        if (foreignID == null)
1542
            return null;
1543
        else
1544
            return this.checkValidity(foreignKey, foreignID.intValue());
1545
    }
1546
 
1547
    public boolean isOrdered() {
1548
        return this.getOrderField() != null;
1549
    }
1550
 
1551
    public SQLField getOrderField() {
1552
        return this.getFieldRaw(orderField);
1553
    }
1554
 
19 ilm 1555
    /**
1556
     * The number of fractional digits of the order field.
1557
     *
1558
     * @return the number of fractional digits of the order field.
1559
     */
1560
    public final int getOrderDecimalDigits() {
1561
        return this.getOrderField().getType().getDecimalDigits().intValue();
1562
    }
1563
 
1564
    public final BigDecimal getOrderULP() {
1565
        return BigDecimal.ONE.scaleByPowerOfTen(-this.getOrderDecimalDigits());
1566
    }
1567
 
17 ilm 1568
    public boolean isArchivable() {
1569
        return this.getArchiveField() != null;
1570
    }
1571
 
1572
    public SQLField getArchiveField() {
1573
        return this.getFieldRaw(archiveField);
1574
    }
1575
 
1576
    public SQLField getCreationDateField() {
1577
        return this.getFieldRaw("CREATION_DATE");
1578
    }
1579
 
1580
    public SQLField getCreationUserField() {
1581
        return this.getFieldRaw("ID_USER_COMMON_CREATE");
1582
    }
1583
 
1584
    public SQLField getModifDateField() {
1585
        return this.getFieldRaw("MODIFICATION_DATE");
1586
    }
1587
 
1588
    public SQLField getModifUserField() {
1589
        return this.getFieldRaw("ID_USER_COMMON_MODIFY");
1590
    }
1591
 
1592
    /**
1593
     * The id of this table which means empty. Tables that aren't rowable or which use NULL to
1594
     * signify empty have no UNDEFINED_ID.
1595
     *
1596
     * @return the empty id or {@link SQLRow#NONEXISTANT_ID} if this table has no UNDEFINED_ID.
1597
     */
1598
    public final int getUndefinedID() {
63 ilm 1599
        return this.getUndefinedID(false).intValue();
1600
    }
1601
 
67 ilm 1602
    // if false getUndefinedID() might contact the DB
1603
    synchronized final boolean undefinedIDKnown() {
1604
        return this.undefinedID != null;
1605
    }
1606
 
1607
    /*
1608
     * No longer save the undefined IDs. We mustn't search undefined IDs when loading structure
1609
     * since the undefined rows might not yet be inserted. When getUndefinedID() was called, we used
1610
     * to save the ID alongside the table structure with the new structure version. Which is wrong
1611
     * since we haven't refreshed the table structure. One solution would be to create an undefined
1612
     * ID version : when loading, as with the structure, we now have to check the saved version
1613
     * against the one in the metadata table, but since FWK_UNDEFINED_ID is small and already
1614
     * cached, we might as well simplify and forego the version altogether.
1615
     */
1616
 
63 ilm 1617
    private final Integer getUndefinedID(final boolean internal) {
61 ilm 1618
        Integer res = null;
1619
        synchronized (this) {
1620
            if (this.undefinedID != null)
1621
                res = this.undefinedID;
1622
        }
1623
        if (res == null) {
63 ilm 1624
            if (!internal && this.getSchema().isFetchAllUndefinedIDs()) {
61 ilm 1625
                // init all undefined, MAYBE one request with UNION ALL
1626
                for (final SQLTable sibling : this.getSchema().getTables()) {
67 ilm 1627
                    Integer siblingRes = sibling.getUndefinedID(true);
63 ilm 1628
                    assert siblingRes != null;
1629
                    if (sibling == this)
1630
                        res = siblingRes;
17 ilm 1631
                }
61 ilm 1632
            } else {
1633
                res = this.fetchUndefID();
1634
                synchronized (this) {
1635
                    this.undefinedID = res;
1636
                }
17 ilm 1637
            }
1638
        }
67 ilm 1639
        assert this.undefinedIDKnown();
63 ilm 1640
        return res;
17 ilm 1641
    }
1642
 
21 ilm 1643
    public final Number getUndefinedIDNumber() {
1644
        final int res = this.getUndefinedID();
1645
        if (res == SQLRow.NONEXISTANT_ID)
1646
            return null;
1647
        else
1648
            return res;
1649
    }
1650
 
17 ilm 1651
    // static
1652
 
1653
    static private final String orderField = "ORDRE";
1654
    static private final String archiveField = "ARCHIVE";
1655
 
1656
    // /////// ******** OLD CODE ********
1657
 
1658
    /*
1659
     * Gestion des événements
1660
     */
1661
 
1662
    public void addTableModifiedListener(SQLTableModifiedListener l) {
73 ilm 1663
        this.addTableModifiedListener(new ListenerAndConfig(l, AFTER_TX_DEFAULT));
1664
    }
1665
 
1666
    public void addTableModifiedListener(ListenerAndConfig l) {
25 ilm 1667
        this.addTableModifiedListener(l, false);
1668
    }
1669
 
73 ilm 1670
    public void addPremierTableModifiedListener(ListenerAndConfig l) {
25 ilm 1671
        this.addTableModifiedListener(l, true);
1672
    }
1673
 
73 ilm 1674
    private void addTableModifiedListener(ListenerAndConfig l, final boolean before) {
63 ilm 1675
        synchronized (this.listenersMutex) {
73 ilm 1676
            final List<ListenerAndConfig> newListeners = new ArrayList<ListenerAndConfig>(this.tableModifiedListeners.size() + 1);
25 ilm 1677
            if (before)
1678
                newListeners.add(l);
1679
            newListeners.addAll(this.tableModifiedListeners);
1680
            if (!before)
1681
                newListeners.add(l);
17 ilm 1682
            this.tableModifiedListeners = Collections.unmodifiableList(newListeners);
1683
        }
1684
    }
1685
 
1686
    public void removeTableModifiedListener(SQLTableModifiedListener l) {
73 ilm 1687
        this.removeTableModifiedListener(new ListenerAndConfig(l, AFTER_TX_DEFAULT));
1688
    }
1689
 
1690
    public void removeTableModifiedListener(ListenerAndConfig l) {
63 ilm 1691
        synchronized (this.listenersMutex) {
73 ilm 1692
            final List<ListenerAndConfig> newListeners = new ArrayList<ListenerAndConfig>(this.tableModifiedListeners);
17 ilm 1693
            if (newListeners.remove(l))
1694
                this.tableModifiedListeners = Collections.unmodifiableList(newListeners);
1695
        }
1696
    }
1697
 
25 ilm 1698
    private static final class BridgeListener implements SQLTableModifiedListener {
1699
 
1700
        private final SQLTableListener l;
1701
 
1702
        private BridgeListener(SQLTableListener l) {
1703
            super();
1704
            this.l = l;
1705
        }
1706
 
1707
        @Override
1708
        public void tableModified(SQLTableEvent evt) {
1709
            final Mode mode = evt.getMode();
1710
            if (mode == Mode.ROW_ADDED)
1711
                this.l.rowAdded(evt.getTable(), evt.getId());
1712
            else if (mode == Mode.ROW_UPDATED)
1713
                this.l.rowModified(evt.getTable(), evt.getId());
1714
            else if (mode == Mode.ROW_DELETED)
1715
                this.l.rowDeleted(evt.getTable(), evt.getId());
1716
        }
1717
 
1718
        @Override
1719
        public int hashCode() {
1720
            return this.l.hashCode();
1721
        }
1722
 
1723
        @Override
1724
        public boolean equals(Object obj) {
1725
            return obj instanceof BridgeListener && this.l.equals(((BridgeListener) obj).l);
1726
        }
1727
    }
1728
 
17 ilm 1729
    /**
1730
     * Ajoute un listener sur cette table.
1731
     *
1732
     * @param l the listener.
1733
     * @deprecated use {@link #addTableModifiedListener(SQLTableModifiedListener)}
1734
     */
1735
    public void addTableListener(SQLTableListener l) {
25 ilm 1736
        this.addTableModifiedListener(new BridgeListener(l));
17 ilm 1737
    }
1738
 
1739
    public void removeTableListener(SQLTableListener l) {
25 ilm 1740
        this.removeTableModifiedListener(new BridgeListener(l));
17 ilm 1741
    }
1742
 
182 ilm 1743
    public final void fireTableModified() {
1744
        this.fireTableModified(SQLRow.NONEXISTANT_ID);
1745
    }
1746
 
17 ilm 1747
    /**
182 ilm 1748
     * Previent tous les listeners de la table qu'il y a eu une modification.
17 ilm 1749
     *
182 ilm 1750
     * @param id which ID was modified, {@link SQLRow#NONEXISTANT_ID} meaning all rows.
17 ilm 1751
     */
1752
    public void fireTableModified(final int id) {
1753
        this.fire(Mode.ROW_UPDATED, id);
1754
    }
1755
 
1756
    public void fireRowAdded(final int id) {
1757
        this.fire(Mode.ROW_ADDED, id);
1758
    }
1759
 
1760
    public void fireRowDeleted(final int id) {
1761
        this.fire(Mode.ROW_DELETED, id);
1762
    }
1763
 
1764
    public void fireTableModified(final int id, Collection<String> fields) {
1765
        this.fire(new SQLTableEvent(this, id, Mode.ROW_UPDATED, fields));
1766
    }
1767
 
1768
    private void fire(final Mode mode, final int id) {
1769
        this.fire(new SQLTableEvent(this, id, mode, null));
1770
    }
1771
 
1772
    public final void fire(SQLTableEvent evt) {
25 ilm 1773
        this.fireTableModified(evt);
1774
    }
1775
 
73 ilm 1776
    // the listeners and the event that was notified to them
1777
    static private class FireState extends Tuple2<List<ListenerAndConfig>, SQLTableEvent> {
1778
        public FireState(final List<ListenerAndConfig> listeners, final SQLTableEvent evt) {
1779
            super(listeners, evt);
1780
        }
1781
 
1782
        private DispatchingState createDispatchingState(final Boolean callbackAfterTxListeners, final boolean oppositeEvt) {
1783
            final List<SQLTableModifiedListener> listeners = new LinkedList<SQLTableModifiedListener>();
1784
            for (final ListenerAndConfig l : get0()) {
132 ilm 1785
                if (callbackAfterTxListeners == null || l.callOnlyAfterTx() == null || callbackAfterTxListeners == l.callOnlyAfterTx())
73 ilm 1786
                    listeners.add(l.getListener());
1787
            }
1788
            return new DispatchingState(listeners, oppositeEvt ? get1().opposite() : get1());
1789
        }
1790
    }
1791
 
1792
    static private class DispatchingState extends Tuple2<Iterator<SQLTableModifiedListener>, SQLTableEvent> {
1793
        public DispatchingState(final List<SQLTableModifiedListener> listeners, final SQLTableEvent evt) {
1794
            super(listeners.iterator(), evt);
1795
        }
1796
    }
1797
 
1798
    static private final ThreadLocal<LinkedList<DispatchingState>> events = new ThreadLocal<LinkedList<DispatchingState>>() {
25 ilm 1799
        @Override
73 ilm 1800
        protected LinkedList<DispatchingState> initialValue() {
1801
            return new LinkedList<DispatchingState>();
25 ilm 1802
        }
1803
    };
1804
 
1805
    // allow to maintain the dispatching of events in order when a listener itself fires an event
73 ilm 1806
    static private void fireTableModified(DispatchingState newTuple) {
1807
        final LinkedList<DispatchingState> linkedList = events.get();
25 ilm 1808
        // add new event
1809
        linkedList.addLast(newTuple);
1810
        // process all pending events
73 ilm 1811
        DispatchingState currentTuple;
25 ilm 1812
        while ((currentTuple = linkedList.peekFirst()) != null) {
1813
            final Iterator<SQLTableModifiedListener> iter = currentTuple.get0();
1814
            final SQLTableEvent currentEvt = currentTuple.get1();
1815
            while (iter.hasNext()) {
1816
                final SQLTableModifiedListener l = iter.next();
1817
                l.tableModified(currentEvt);
17 ilm 1818
            }
25 ilm 1819
            // not removeFirst() since the item might have been already removed
1820
            linkedList.pollFirst();
17 ilm 1821
        }
1822
    }
1823
 
1824
    private void fireTableModified(final SQLTableEvent evt) {
132 ilm 1825
        if (evt.getTable() != this)
1826
            throw new IllegalArgumentException("Wrong table : " + this + " ; " + evt);
73 ilm 1827
        final FireState fireState;
132 ilm 1828
        final TransactionPoint point = evt.getTransactionPoint();
73 ilm 1829
        final Boolean callbackAfterTxListeners;
63 ilm 1830
        synchronized (this.listenersMutex) {
73 ilm 1831
            // no need to copy since this.tableModifiedListeners is immutable
1832
            fireState = new FireState(this.tableModifiedListeners, evt);
1833
            if (point == null) {
1834
                // call back every listener
1835
                callbackAfterTxListeners = null;
132 ilm 1836
            } else if (point.isActive()) {
1837
                addFireStates(point, Collections.singleton(fireState));
1838
                callbackAfterTxListeners = false;
1839
                // to free DB resources, it is allowed to fire events after the transaction ended
1840
            } else if (!point.wasCommitted()) {
1841
                throw new IllegalStateException("Fire after an aborted transaction point");
1842
            } else if (point.getSavePoint() != null) {
1843
                addFireStates(point, Collections.singleton(fireState));
1844
                callbackAfterTxListeners = false;
73 ilm 1845
            } else {
132 ilm 1846
                callbackAfterTxListeners = null;
73 ilm 1847
            }
17 ilm 1848
        }
73 ilm 1849
        fireTableModified(fireState.createDispatchingState(callbackAfterTxListeners, false));
17 ilm 1850
    }
1851
 
132 ilm 1852
    private void addFireStates(TransactionPoint point, final Collection<FireState> fireStates) {
1853
        assert Thread.holdsLock(this.listenersMutex) : "Unsafe to access this.transactions";
1854
        // if multiple save points are released before firing, we must go back to the still active
1855
        // point
1856
        while (!point.isActive())
1857
            point = point.getPrevious();
1858
        if (!this.transactions.containsKey(point))
1859
            point.addListener(this.txListener);
1860
        this.transactions.addAll(point, fireStates);
1861
    }
1862
 
73 ilm 1863
    // a transaction was committed or aborted, we must either notify listeners that wanted the
1864
    // transaction to commit, or re-notify the listeners that didn't want to wait
132 ilm 1865
    protected void fireFromTransaction(final TransactionPoint point) {
1866
        final boolean committed = point.wasCommitted();
1867
        // if it's a released savePoint, add all our states to the previous point (and thus don't
1868
        // fire now)
1869
        final boolean releasedSavePoint = committed && point.getSavePoint() != null;
73 ilm 1870
        final List<FireState> states;
1871
        synchronized (this.listenersMutex) {
1872
            states = this.transactions.remove(point);
132 ilm 1873
            if (releasedSavePoint) {
1874
                this.addFireStates(point, states);
1875
            }
73 ilm 1876
        }
132 ilm 1877
        if (!releasedSavePoint) {
1878
            final ListIterator<FireState> iter = CollectionUtils.getListIterator(states, !committed);
1879
            while (iter.hasNext()) {
1880
                final FireState state = iter.next();
1881
                fireTableModified(state.createDispatchingState(committed, !committed));
1882
            }
73 ilm 1883
        }
1884
    }
1885
 
61 ilm 1886
    public synchronized String toXML() {
17 ilm 1887
        final StringBuilder sb = new StringBuilder(16000);
1888
        sb.append("<table name=\"");
142 ilm 1889
        sb.append(OUTPUTTER.escapeAttributeEntities(this.getName()));
17 ilm 1890
        sb.append("\"");
1891
 
1892
        final String schemaName = this.getSchema().getName();
1893
        if (schemaName != null) {
1894
            sb.append(" schema=\"");
142 ilm 1895
            sb.append(OUTPUTTER.escapeAttributeEntities(schemaName));
17 ilm 1896
            sb.append('"');
1897
        }
1898
 
67 ilm 1899
        SQLSchema.appendVersionAttr(this.version, sb);
17 ilm 1900
 
1901
        if (getType() != null) {
1902
            sb.append(" type=\"");
142 ilm 1903
            sb.append(OUTPUTTER.escapeAttributeEntities(getType()));
17 ilm 1904
            sb.append('"');
1905
        }
1906
 
1907
        sb.append(">\n");
1908
 
1909
        if (this.getComment() != null) {
1910
            sb.append("<comment>");
142 ilm 1911
            sb.append(OUTPUTTER.escapeElementEntities(this.getComment()));
17 ilm 1912
            sb.append("</comment>\n");
1913
        }
65 ilm 1914
        for (SQLField field : this.fields.values()) {
17 ilm 1915
            sb.append(field.toXML());
1916
        }
1917
        sb.append("<primary>\n");
1918
        for (SQLField element : this.primaryKeys) {
1919
            sb.append(element.toXML());
1920
        }
1921
        sb.append("</primary>\n");
1922
        // avoid writing unneeded chars
1923
        if (this.triggers.size() > 0) {
1924
            sb.append("<triggers>\n");
1925
            for (Trigger t : this.triggers.values()) {
1926
                sb.append(t.toXML());
1927
            }
1928
            sb.append("</triggers>\n");
1929
        }
1930
        if (this.constraints != null) {
1931
            sb.append("<constraints>\n");
1932
            for (Constraint t : this.constraints) {
1933
                sb.append(t.toXML());
1934
            }
1935
            sb.append("</constraints>\n");
1936
        }
1937
        sb.append("</table>");
1938
        return sb.toString();
1939
    }
1940
 
25 ilm 1941
    @Override
1942
    public SQLTableModifiedListener createTableListener(final SQLDataListener l) {
1943
        return new SQLTableModifiedListener() {
1944
            @Override
1945
            public void tableModified(SQLTableEvent evt) {
132 ilm 1946
                l.dataChanged(evt);
17 ilm 1947
            }
1948
        };
1949
    }
1950
 
65 ilm 1951
    @Override
17 ilm 1952
    public SQLTable getTable() {
1953
        return this;
1954
    }
1955
 
65 ilm 1956
    @Override
1957
    public String getAlias() {
1958
        return getName();
1959
    }
1960
 
1961
    @Override
1962
    public String getSQL() {
93 ilm 1963
        // always use fullname, otherwise must check the datasource's
1964
        // default schema
65 ilm 1965
        return getSQLName().quote();
1966
    }
1967
 
17 ilm 1968
    public boolean equalsDesc(SQLTable o) {
1969
        return this.equalsDesc(o, true) == null;
1970
    }
1971
 
1972
    /**
1973
     * Compare this table and its descendants. This do not compare undefinedID as it isn't part of
1974
     * the structure per se.
1975
     *
1976
     * @param o the table to compare.
1977
     * @param compareName whether to also compare the name, useful for comparing 2 tables in the
1978
     *        same schema.
1979
     * @return <code>null</code> if attributes and children of this and <code>o</code> are equals,
1980
     *         otherwise a String explaining the differences.
1981
     */
1982
    public String equalsDesc(SQLTable o, boolean compareName) {
1983
        return this.equalsDesc(o, null, compareName);
1984
    }
1985
 
61 ilm 1986
    // ATTN otherSystem can be null, meaning compare exactly (even if the system of this table and
1987
    // the system of the other table do not support the same features and thus tables cannot be
1988
    // equal)
1989
    // if otherSystem isn't null, then this method is more lenient and return true if the two tables
1990
    // are the closest possible. NOTE that otherSystem is not required to be the system of the other
1991
    // table, it might be something else if the other table was loaded into a system different than
1992
    // the one which created the dump.
182 ilm 1993
    public synchronized String equalsDesc(SQLTable o, SQLSyntax otherSyntax, boolean compareName) {
17 ilm 1994
        if (o == null)
1995
            return "other table is null";
1996
        final boolean name = !compareName || this.getName().equals(o.getName());
1997
        if (!name)
1998
            return "name unequal : " + this.getName() + " " + o.getName();
1999
        // TODO triggers, but wait for the dumping of functions
2000
        // which mean wait for psql 8.4 pg_get_functiondef()
2001
        // if (this.getServer().getSQLSystem() == o.getServer().getSQLSystem()) {
2002
        // if (!this.getTriggers().equals(o.getTriggers()))
2003
        // return "triggers unequal : " + this.getTriggers() + " " + o.getTriggers();
2004
        // } else {
2005
        // if (!this.getTriggers().keySet().equals(o.getTriggers().keySet()))
2006
        // return "triggers names unequal : " + this.getTriggers() + " " + o.getTriggers();
2007
        // }
182 ilm 2008
        final boolean checkComment = otherSyntax == null || this.getServer().getSQLSystem().isTablesCommentSupported() && otherSyntax.getSystem().isTablesCommentSupported();
17 ilm 2009
        if (checkComment && !CompareUtils.equals(this.getComment(), o.getComment()))
132 ilm 2010
            return "comment unequal : " + SQLBase.quoteStringStd(this.getComment()) + " != " + SQLBase.quoteStringStd(o.getComment());
182 ilm 2011
        return this.equalsChildren(o, otherSyntax);
17 ilm 2012
    }
2013
 
182 ilm 2014
    private synchronized String equalsChildren(SQLTable o, SQLSyntax otherSyntax) {
17 ilm 2015
        if (!this.getChildrenNames().equals(o.getChildrenNames()))
2016
            return "fields differences: " + this.getChildrenNames() + "\n" + o.getChildrenNames();
2017
 
182 ilm 2018
        final String noLink = equalsChildrenNoLink(o, otherSyntax);
17 ilm 2019
        if (noLink != null)
2020
            return noLink;
2021
 
2022
        // foreign keys
132 ilm 2023
        final Set<Link> thisLinks = this.getForeignLinks();
2024
        final Set<Link> oLinks = o.getForeignLinks();
17 ilm 2025
        if (thisLinks.size() != oLinks.size())
2026
            return "different number of foreign keys " + thisLinks + " != " + oLinks;
132 ilm 2027
        final SQLSystem thisSystem = this.getServer().getSQLSystem();
182 ilm 2028
        final SQLSystem otherSystem = otherSyntax == null ? null : otherSyntax.getSystem();
17 ilm 2029
        for (final Link l : thisLinks) {
2030
            final Link ol = o.getDBSystemRoot().getGraph().getForeignLink(o, l.getCols());
2031
            if (ol == null)
2032
                return "no foreign key for " + l.getLabel();
2033
            final SQLName thisPath = l.getTarget().getContextualSQLName(this);
2034
            final SQLName oPath = ol.getTarget().getContextualSQLName(o);
2035
            if (thisPath.getItemCount() != oPath.getItemCount())
2036
                return "unequal path size : " + thisPath + " != " + oPath;
2037
            if (!thisPath.getName().equals(oPath.getName()))
2038
                return "unequal referenced table name : " + thisPath.getName() + " != " + oPath.getName();
61 ilm 2039
            if (!getRule(l.getUpdateRule(), thisSystem, otherSystem).equals(getRule(ol.getUpdateRule(), thisSystem, otherSystem)))
57 ilm 2040
                return "unequal update rule for " + l + ": " + l.getUpdateRule() + " != " + ol.getUpdateRule();
61 ilm 2041
            if (!getRule(l.getDeleteRule(), thisSystem, otherSystem).equals(getRule(ol.getDeleteRule(), thisSystem, otherSystem)))
57 ilm 2042
                return "unequal delete rule for " + l + ": " + l.getDeleteRule() + " != " + ol.getDeleteRule();
17 ilm 2043
        }
2044
 
83 ilm 2045
        final Set<Constraint> thisConstraints;
2046
        final Set<Constraint> otherConstraints;
17 ilm 2047
        try {
83 ilm 2048
            final Tuple2<Set<Constraint>, Set<Index>> thisConstraintsAndIndexes = this.getConstraintsAndIndexes();
2049
            final Tuple2<Set<Constraint>, Set<Index>> otherConstraintsAndIndexes = o.getConstraintsAndIndexes();
17 ilm 2050
            // order irrelevant
83 ilm 2051
            final Set<Index> thisIndexesSet = thisConstraintsAndIndexes.get1();
2052
            final Set<Index> oIndexesSet = otherConstraintsAndIndexes.get1();
17 ilm 2053
            if (!thisIndexesSet.equals(oIndexesSet))
2054
                return "indexes differences: " + thisIndexesSet + "\n" + oIndexesSet;
83 ilm 2055
            thisConstraints = thisConstraintsAndIndexes.get0();
2056
            otherConstraints = otherConstraintsAndIndexes.get0();
17 ilm 2057
        } catch (SQLException e) {
2058
            // MAYBE fetch indexes with the rest to avoid exn now
2059
            return "couldn't get indexes: " + ExceptionUtils.getStackTrace(e);
2060
        }
132 ilm 2061
        if (!CustomEquals.equals(thisConstraints, otherConstraints, otherSystem == null || otherSystem.equals(thisSystem) ? null : Constraint.getInterSystemHashStrategy()))
83 ilm 2062
            return "constraints unequal : '" + thisConstraints + "' != '" + otherConstraints + "'";
17 ilm 2063
 
2064
        return null;
2065
    }
2066
 
83 ilm 2067
    private final Tuple2<Set<Constraint>, Set<Index>> getConstraintsAndIndexes() throws SQLException {
2068
        final Set<Constraint> thisConstraints;
2069
        final Set<Index> thisIndexes;
2070
        if (this.getServer().getSQLSystem() != SQLSystem.MSSQL) {
2071
            thisConstraints = this.getConstraints();
2072
            thisIndexes = new HashSet<Index>(this.getIndexes(true));
2073
        } else {
2074
            thisConstraints = new HashSet<Constraint>(this.getConstraints());
2075
            thisIndexes = new HashSet<Index>();
2076
            for (final Index i : this.getIndexes()) {
2077
                final Value<String> where = i.getMSUniqueWhere();
2078
                if (!where.hasValue()) {
2079
                    // regular index
2080
                    thisIndexes.add(i);
2081
                } else if (where.getValue() == null) {
2082
                    final Map<String, Object> map = new HashMap<String, Object>();
2083
                    map.put("CONSTRAINT_NAME", i.getName());
2084
                    map.put("CONSTRAINT_TYPE", "UNIQUE");
2085
                    map.put("COLUMN_NAMES", i.getCols());
2086
                    map.put("DEFINITION", null);
2087
                    thisConstraints.add(new Constraint(this, map));
2088
                } else {
2089
                    // remove extra IS NOT NULL, but does *not* translate [ARCHIVE]=(0) into
2090
                    // "ARCHIVE" = 0
2091
                    thisIndexes.add(this.createUniqueIndex(i.getName(), i.getCols(), where.getValue()));
2092
                }
2093
            }
2094
        }
2095
        return Tuple2.create(thisConstraints, thisIndexes);
2096
    }
2097
 
61 ilm 2098
    private final Rule getRule(Rule r, SQLSystem thisSystem, SQLSystem otherSystem) {
2099
        // compare exactly
2100
        if (otherSystem == null)
2101
            return r;
2102
        // see http://code.google.com/p/h2database/issues/detail?id=352
2103
        if (r == Rule.NO_ACTION && (thisSystem == SQLSystem.H2 || otherSystem == SQLSystem.H2))
2104
            return Rule.RESTRICT;
2105
        else
2106
            return r;
2107
    }
2108
 
17 ilm 2109
    /**
2110
     * Compare the fields of this table, ignoring foreign constraints.
2111
     *
2112
     * @param o the table to compare.
2113
     * @param otherSystem the system <code>o</code> originates from, can be <code>null</code>.
2114
     * @return <code>null</code> if each fields of this exists in <code>o</code> and is equal to it.
2115
     */
182 ilm 2116
    public synchronized final String equalsChildrenNoLink(SQLTable o, SQLSyntax otherSystem) {
17 ilm 2117
        for (final SQLField f : this.getFields()) {
2118
            final SQLField oField = o.getField(f.getName());
2119
            final boolean isPrimary = this.getPrimaryKeys().contains(f);
2120
            if (isPrimary != o.getPrimaryKeys().contains(oField))
2121
                return f + " is a primary not in " + o.getPrimaryKeys();
2122
            final String equalsDesc = f.equalsDesc(oField, otherSystem, !isPrimary);
2123
            if (equalsDesc != null)
2124
                return equalsDesc;
2125
        }
2126
        return null;
2127
    }
2128
 
2129
    public final SQLCreateMoveableTable getCreateTable() {
142 ilm 2130
        return this.getCreateTable(SQLSyntax.get(this));
17 ilm 2131
    }
2132
 
142 ilm 2133
    public synchronized final SQLCreateMoveableTable getCreateTable(final SQLSyntax syntax) {
2134
        final SQLSystem system = syntax.getSystem();
67 ilm 2135
        final SQLCreateMoveableTable res = new SQLCreateMoveableTable(syntax, this.getDBRoot().getName(), this.getName());
17 ilm 2136
        for (final SQLField f : this.getOrderedFields()) {
2137
            res.addColumn(f);
2138
        }
2139
        // primary keys
2140
        res.setPrimaryKey(getPKsNames());
2141
        // foreign keys
132 ilm 2142
        for (final Link l : this.getForeignLinks())
17 ilm 2143
            // don't generate explicit CREATE INDEX for fk, we generate all indexes below
2144
            // (this also avoid creating a fk index that wasn't there)
2145
            res.addForeignConstraint(l, false);
2146
        // constraints
2147
        if (this.constraints != null)
41 ilm 2148
            for (final Constraint added : this.getConstraints()) {
17 ilm 2149
                if (added.getType() == ConstraintType.UNIQUE) {
2150
                    res.addUniqueConstraint(added.getName(), added.getCols());
2151
                } else
2152
                    throw new UnsupportedOperationException("unsupported constraint: " + added);
2153
            }
2154
        // indexes
2155
        try {
83 ilm 2156
            // MS unique constraint are not standard so we're forced to create indexes "where col is
2157
            // not null" in addUniqueConstraint(). Thus when converting to another system we must
2158
            // parse indexes to recreate actual constraints.
2159
            final boolean convertMSIndex = this.getServer().getSQLSystem() == SQLSystem.MSSQL && system != SQLSystem.MSSQL;
2160
            for (final Index i : this.getIndexes(true)) {
2161
                Value<String> msWhere = null;
2162
                if (convertMSIndex && (msWhere = i.getMSUniqueWhere()).hasValue()) {
2163
                    if (msWhere.getValue() != null)
2164
                        Log.get().warning("MS filter might not be valid in " + system + " : " + msWhere.getValue());
2165
                    res.addUniqueConstraint(i.getName(), i.getCols(), msWhere.getValue());
142 ilm 2166
                } else {
83 ilm 2167
                    // partial unique index sometimes cannot be handled natively by the DB system
2168
                    if (i.isUnique() && i.getFilter() != null && !system.isIndexFilterConditionSupported())
144 ilm 2169
                        res.addUniqueConstraint(i.getName(), i.createUniqueHelper());
83 ilm 2170
                    else
142 ilm 2171
                        res.addIndex(i);
17 ilm 2172
                }
83 ilm 2173
            }
17 ilm 2174
        } catch (SQLException e) {
2175
            // MAYBE fetch indexes with the rest to avoid exn now
2176
            throw new IllegalStateException("could not get indexes", e);
2177
        }
83 ilm 2178
        // TODO triggers, but they are system dependent and we would have to parse the SQL
2179
        // definitions to replace the different root/table name in DeferredClause.asString()
17 ilm 2180
        if (this.getComment() != null)
2181
            res.addOutsideClause(syntax.getSetTableComment(getComment()));
2182
        return res;
2183
    }
2184
 
2185
    /**
2186
     * Return the indexes mapped by column names. Ie a key will have as value every index that
65 ilm 2187
     * mentions it, and a multi-column index will be in several entries. Note: this is not robust
2188
     * since {@link Index#getCols()} isn't.
17 ilm 2189
     *
2190
     * @return the indexes mapped by column names.
2191
     * @throws SQLException if an error occurs.
2192
     */
83 ilm 2193
    public final SetMap<String, Index> getIndexesByField() throws SQLException {
17 ilm 2194
        final List<Index> indexes = this.getIndexes();
83 ilm 2195
        final SetMap<String, Index> res = new SetMap<String, Index>(indexes.size()) {
2196
            @Override
2197
            public Set<Index> createCollection(Collection<? extends Index> v) {
2198
                final HashSet<Index> res = new HashSet<Index>(4);
2199
                res.addAll(v);
2200
                return res;
2201
            }
2202
        };
17 ilm 2203
        for (final Index i : indexes)
2204
            for (final String col : i.getCols())
83 ilm 2205
                res.add(col, i);
17 ilm 2206
        return res;
2207
    }
2208
 
2209
    /**
65 ilm 2210
     * Return the indexes on the passed columns names. Note: this is not robust since
2211
     * {@link Index#getCols()} isn't.
2212
     *
2213
     * @param cols fields names.
2214
     * @return the matching indexes.
2215
     * @throws SQLException if an error occurs.
2216
     */
2217
    public final List<Index> getIndexes(final List<String> cols) throws SQLException {
2218
        final List<Index> res = new ArrayList<Index>();
2219
        for (final Index i : this.getIndexes())
2220
            if (i.getCols().equals(cols))
2221
                res.add(i);
2222
        return res;
2223
    }
2224
 
2225
    /**
17 ilm 2226
     * Return the indexes of this table. Except the primary key as every system generates it
2227
     * automatically.
2228
     *
2229
     * @return the list of indexes.
142 ilm 2230
     * @throws SQLException if an error occurs while accessing the DB.
17 ilm 2231
     */
142 ilm 2232
    public final List<Index> getIndexes() throws SQLException {
83 ilm 2233
        return this.getIndexes(false);
2234
    }
2235
 
142 ilm 2236
    public synchronized final List<Index> getIndexes(final boolean normalized) throws SQLException {
17 ilm 2237
        // in pg, a unique constraint creates a unique index that is not removeable
2238
        // (except of course if we drop the constraint)
2239
        // in mysql unique constraints and indexes are one and the same thing
2240
        // so we must return them only in one (either getConstraints() or getIndexes())
2241
        // anyway in all systems, a unique constraint or index achieve the same function
2242
        // and so only generates the constraint and not the index
2243
        final Set<List<String>> uniqConstraints;
2244
        if (this.constraints != null) {
2245
            uniqConstraints = new HashSet<List<String>>();
2246
            for (final Constraint c : this.constraints) {
2247
                if (c.getType() == ConstraintType.UNIQUE)
2248
                    uniqConstraints.add(c.getCols());
2249
            }
2250
        } else
2251
            uniqConstraints = Collections.emptySet();
2252
 
2253
        final List<Index> indexes = new ArrayList<Index>();
2254
        Index currentIndex = null;
142 ilm 2255
        for (final Map<String, Object> norm : this.getDBSystemRoot().getSyntax().getIndexInfo(this)) {
17 ilm 2256
            final Index index = new Index(norm);
2257
            final short seq = ((Number) norm.get("ORDINAL_POSITION")).shortValue();
2258
            if (seq == 1) {
2259
                if (canAdd(currentIndex, uniqConstraints))
2260
                    indexes.add(currentIndex);
2261
                currentIndex = index;
2262
            } else {
2263
                // continuing a multi-field index
2264
                currentIndex.add(index);
2265
            }
2266
        }
2267
        if (canAdd(currentIndex, uniqConstraints))
2268
            indexes.add(currentIndex);
2269
 
83 ilm 2270
        if (normalized) {
2271
            indexes.addAll(this.getPartialUniqueIndexes());
2272
        }
2273
 
17 ilm 2274
        // MAYBE another request to find out index.getMethod() (eg pg.getIndexesReq())
2275
        return indexes;
2276
    }
2277
 
2278
    private boolean canAdd(final Index currentIndex, final Set<List<String>> uniqConstraints) {
2279
        if (currentIndex == null || currentIndex.isPKIndex())
2280
            return false;
2281
 
2282
        return !currentIndex.isUnique() || !uniqConstraints.contains(currentIndex.getCols());
2283
    }
2284
 
83 ilm 2285
    // MAYBE inline
2286
    protected synchronized final List<Index> getPartialUniqueIndexes() throws SQLException {
2287
        final SQLSystem thisSystem = this.getServer().getSQLSystem();
2288
        final List<Index> indexes = new ArrayList<Index>();
2289
        // parse triggers, TODO remove them from triggers to output in getCreateTable()
2290
        if (thisSystem == SQLSystem.H2) {
2291
            for (final Trigger t : this.triggers.values()) {
144 ilm 2292
                Matcher matcher = ChangeTable.H2_UNIQUE_TRIGGER_PATTERN.matcher(t.getSQL());
83 ilm 2293
                if (matcher.find()) {
2294
                    final String indexName = ChangeTable.getIndexName(t.getName(), thisSystem);
2295
                    final String[] javaCols = ChangeTable.H2_LIST_PATTERN.split(matcher.group(1).trim());
2296
                    final List<String> cols = new ArrayList<String>(javaCols.length);
2297
                    for (final String javaCol : javaCols) {
2298
                        cols.add(StringUtils.unDoubleQuote(javaCol));
2299
                    }
2300
                    final String where = StringUtils.unDoubleQuote(matcher.group(2).trim());
2301
                    indexes.add(createUniqueIndex(indexName, cols, where));
144 ilm 2302
                } else {
2303
                    matcher = ChangeTable.H2_UNIQUE_TRIGGER_CLASS_PATTERN.matcher(t.getSQL());
2304
                    if (matcher.find()) {
2305
                        final String className = matcher.group(1);
2306
                        final Class<?> triggerClass;
2307
                        try {
2308
                            triggerClass = Class.forName(className);
2309
                        } catch (ClassNotFoundException e) {
151 ilm 2310
                            // throw new SQLException("Class not found for " + t, e);
2311
                            e.printStackTrace();
2312
                            continue;
144 ilm 2313
                        }
2314
                        PartialUniqueTrigger n;
2315
                        try {
2316
                            n = (PartialUniqueTrigger) triggerClass.newInstance();
2317
                        } catch (Exception e) {
151 ilm 2318
                            // throw new SQLException("Couldn't instantiate class for " + t, e);
2319
                            e.printStackTrace();
2320
                            continue;
144 ilm 2321
                        }
2322
                        final String indexName = ChangeTable.getIndexName(t.getName(), thisSystem);
2323
                        final Index idx = createUniqueIndex(indexName, n.getColumns(), n.getWhere());
2324
                        idx.setH2Class(n.getClass());
2325
                        indexes.add(idx);
2326
                    }
83 ilm 2327
                }
2328
            }
2329
        } else if (thisSystem == SQLSystem.MYSQL) {
2330
            for (final Trigger t : this.triggers.values()) {
2331
                if (t.getAction().contains(ChangeTable.MYSQL_TRIGGER_EXCEPTION)) {
2332
                    final String indexName = ChangeTable.getIndexName(t.getName(), thisSystem);
2333
                    // MySQL needs a pair of triggers
2334
                    final Trigger t2 = indexName == null ? null : this.triggers.get(indexName + ChangeTable.MYSQL_TRIGGER_SUFFIX_2);
2335
                    // and their body must match
2336
                    if (t2 != null && t2.getAction().equals(t.getAction())) {
2337
                        final Matcher matcher = ChangeTable.MYSQL_UNIQUE_TRIGGER_PATTERN.matcher(t.getAction());
2338
                        if (!matcher.find())
2339
                            throw new IllegalStateException("Couldn't parse " + t.getAction());
2340
                        // parse table name
2341
                        final SQLName parsedName = SQLName.parse(matcher.group(1).trim());
2342
                        if (!this.getName().equals(parsedName.getName()))
2343
                            throw new IllegalStateException("Name mismatch : " + this.getSQLName() + " != " + parsedName);
17 ilm 2344
 
83 ilm 2345
                        final String[] wheres = ChangeTable.MYSQL_WHERE_PATTERN.split(matcher.group(2).trim());
2346
                        final String userWhere = wheres[0];
2347
 
2348
                        final List<String> cols = new ArrayList<String>(wheres.length - 1);
2349
                        for (int i = 1; i < wheres.length; i++) {
2350
                            final Matcher eqMatcher = ChangeTable.MYSQL_WHERE_EQ_PATTERN.matcher(wheres[i].trim());
2351
                            if (!eqMatcher.matches())
2352
                                throw new IllegalStateException("Invalid where clause " + wheres[i]);
2353
                            cols.add(SQLName.parse(eqMatcher.group(2).trim()).getName());
2354
                        }
2355
                        if (cols.isEmpty())
2356
                            throw new IllegalStateException("No columns in " + Arrays.asList(wheres));
2357
                        indexes.add(createUniqueIndex(indexName, cols, userWhere));
2358
                    }
2359
                }
2360
            }
2361
        }
2362
        return indexes;
2363
    }
2364
 
2365
    public static class SQLIndex {
2366
 
2367
        private static final Pattern NORMALIZE_SPACES = Pattern.compile("\\s+");
2368
 
17 ilm 2369
        private final String name;
83 ilm 2370
        // SQL, e.g. : lower("name"), "age"
17 ilm 2371
        private final List<String> attrs;
2372
        private final boolean unique;
2373
        private String method;
83 ilm 2374
        private final String filter;
17 ilm 2375
 
83 ilm 2376
        public SQLIndex(final String name, final List<String> attributes, final boolean unique, final String filter) {
2377
            this(name, attributes, false, unique, filter);
17 ilm 2378
        }
2379
 
83 ilm 2380
        public SQLIndex(final String name, final List<String> attributes, final boolean quoteAll, final boolean unique, final String filter) {
17 ilm 2381
            super();
2382
            this.name = name;
83 ilm 2383
            this.attrs = new ArrayList<String>(attributes.size());
2384
            for (final String attr : attributes)
2385
                this.addAttr(quoteAll ? SQLBase.quoteIdentifier(attr) : attr);
2386
            this.unique = unique;
17 ilm 2387
            this.method = null;
83 ilm 2388
            // helps when comparing
2389
            this.filter = filter == null ? null : NORMALIZE_SPACES.matcher(filter.trim()).replaceAll(" ");
17 ilm 2390
        }
2391
 
2392
        public final String getName() {
2393
            return this.name;
2394
        }
2395
 
2396
        public final boolean isUnique() {
2397
            return this.unique;
2398
        }
2399
 
2400
        /**
2401
         * All attributes forming this index.
2402
         *
2403
         * @return the components of this index, eg ["lower(name)", "age"].
2404
         */
2405
        public final List<String> getAttrs() {
83 ilm 2406
            return Collections.unmodifiableList(this.attrs);
17 ilm 2407
        }
2408
 
83 ilm 2409
        protected final void addAttr(final String attr) {
2410
            this.attrs.add(attr);
17 ilm 2411
        }
2412
 
2413
        public final void setMethod(String method) {
2414
            this.method = method;
2415
        }
2416
 
2417
        public final String getMethod() {
2418
            return this.method;
2419
        }
2420
 
2421
        /**
2422
         * Filter for partial index.
2423
         *
2424
         * @return the where clause or <code>null</code>.
2425
         */
2426
        public final String getFilter() {
2427
            return this.filter;
2428
        }
2429
 
2430
        @Override
2431
        public String toString() {
83 ilm 2432
            return getClass().getSimpleName() + " " + this.getName() + " unique: " + this.isUnique() + " cols: " + this.getAttrs() + " filter: " + this.getFilter();
17 ilm 2433
        }
2434
 
2435
        // ATTN don't use name since it is often auto-generated (eg by a UNIQUE field)
2436
        @Override
2437
        public boolean equals(Object obj) {
83 ilm 2438
            if (obj instanceof SQLIndex) {
2439
                final SQLIndex o = (SQLIndex) obj;
2440
                return this.isUnique() == o.isUnique() && this.getAttrs().equals(o.getAttrs()) && CompareUtils.equals(this.getFilter(), o.getFilter())
2441
                        && CompareUtils.equals(this.getMethod(), o.getMethod());
2442
            } else {
17 ilm 2443
                return false;
83 ilm 2444
            }
17 ilm 2445
        }
2446
 
2447
        // ATTN use cols, so use only after cols are done
2448
        @Override
2449
        public int hashCode() {
2450
            return this.getAttrs().hashCode() + ((Boolean) this.isUnique()).hashCode();
2451
        }
2452
    }
83 ilm 2453
 
2454
    private final Index createUniqueIndex(final String name, final List<String> cols, final String where) {
2455
        final Index res = new Index(name, cols.get(0), false, where);
2456
        for (int i = 1; i < cols.size(); i++) {
2457
            res.addFromMD(cols.get(i));
2458
        }
2459
        return res;
2460
    }
2461
 
2462
    private final String removeParens(String filter) {
2463
        if (filter != null) {
2464
            filter = filter.trim();
2465
            final SQLSystem sys = this.getServer().getSQLSystem();
2466
            // postgreSQL always wrap filter with parens, ATTN we shouldn't remove from
2467
            // "(A) and (B)" but still support "(A = (0))"
2468
            if ((sys == SQLSystem.POSTGRESQL || sys == SQLSystem.MSSQL) && filter.startsWith("(") && filter.endsWith(")")) {
2469
                filter = filter.substring(1, filter.length() - 1);
2470
            }
2471
        }
2472
        return filter;
2473
    }
2474
 
144 ilm 2475
    @GuardedBy("uniqueH2Triggers")
2476
    private final Map<Tuple2<List<String>, String>, Class<? extends PartialUniqueTrigger>> uniqueH2Triggers = new HashMap<>();
2477
 
2478
    public final void registerH2UniqueTrigger(Class<? extends PartialUniqueTrigger> uniqTriggerClass) throws InstantiationException, IllegalAccessException {
2479
        final PartialUniqueTrigger newInstance = uniqTriggerClass.newInstance();
2480
        final Tuple2<List<String>, String> key = Tuple2.create(newInstance.getColumns(), newInstance.getWhere().toUpperCase());
2481
        synchronized (this.uniqueH2Triggers) {
2482
            this.uniqueH2Triggers.put(key, uniqTriggerClass);
2483
        }
2484
    }
2485
 
2486
    public static final String workAroundForH2WhereTrigger(final String where) {
2487
        return where.toUpperCase().replaceAll("[()]", "");
2488
    }
2489
 
2490
    public final Class<? extends PartialUniqueTrigger> getH2UniqueTriggerClass(final List<String> cols, final String where) {
2491
        final Tuple2<List<String>, String> key = Tuple2.create(cols, workAroundForH2WhereTrigger(where));
2492
        synchronized (this.uniqueH2Triggers) {
2493
            return this.uniqueH2Triggers.get(key);
2494
        }
2495
    }
2496
 
83 ilm 2497
    public final class Index extends SQLIndex {
2498
 
2499
        private final List<String> cols;
144 ilm 2500
        private Class<? extends PartialUniqueTrigger> h2Class;
83 ilm 2501
 
2502
        Index(final Map<String, Object> row) {
2503
            this((String) row.get("INDEX_NAME"), (String) row.get("COLUMN_NAME"), (Boolean) row.get("NON_UNIQUE"), (String) row.get("FILTER_CONDITION"));
2504
        }
2505
 
2506
        Index(final String name, String col, Boolean nonUnique, String filter) {
2507
            super(name, Collections.<String> emptyList(), !nonUnique, removeParens(filter));
2508
            this.cols = new ArrayList<String>();
2509
            this.addFromMD(col);
2510
        }
2511
 
2512
        public final SQLTable getTable() {
2513
            return SQLTable.this;
2514
        }
2515
 
2516
        /**
2517
         * The table columns in this index. Note that due to DB system limitation this list is
2518
         * incomplete (e.g. missing expressions).
2519
         *
2520
         * @return the unquoted columns, e.g. ["age"].
2521
         */
2522
        public final List<String> getCols() {
2523
            return this.cols;
2524
        }
2525
 
2526
        public final List<SQLField> getFields() {
2527
            final List<SQLField> res = new ArrayList<SQLField>(this.getCols().size());
2528
            for (final String f : this.getCols())
2529
                res.add(getTable().getField(f));
2530
            return res;
2531
        }
2532
 
2533
        /**
2534
         * Adds a column to this multi-field index.
2535
         *
2536
         * @param name the name of the index.
2537
         * @param col the column to add.
2538
         * @param unique whether the index is unique.
2539
         * @throws IllegalStateException if <code>name</code> and <code>unique</code> are not the
2540
         *         same as these.
2541
         */
2542
        private final void add(final Index o) {
2543
            assert o.getAttrs().size() == 1;
2544
            if (!o.getName().equals(this.getName()) || this.isUnique() != o.isUnique())
2545
                throw new IllegalStateException("incoherence");
2546
            this.cols.addAll(o.getCols());
2547
            this.addAttr(o.getAttrs().get(0));
2548
        }
2549
 
2550
        // col is either an expression or a column name
2551
        protected void addFromMD(String col) {
2552
            if (getTable().contains(col)) {
2553
                // e.g. age
2554
                this.cols.add(col);
2555
                this.addAttr(SQLBase.quoteIdentifier(col));
2556
            } else {
2557
                // e.g. lower("name")
2558
                this.addAttr(col);
2559
            }
2560
        }
2561
 
2562
        final boolean isPKIndex() {
2563
            return this.isUnique() && this.getCols().equals(getTable().getPKsNames()) && this.getCols().size() == this.getAttrs().size();
2564
        }
2565
 
2566
        private final Pattern getColPattern(final String col) {
2567
            // e.g. ([NOM] IS NOT NULL AND [PRENOM] IS NOT NULL AND [ARCHIVE]=(0))
2568
            return Pattern.compile("(?i:\\s+AND\\s+)?" + Pattern.quote(new SQLName(col).quoteMS()) + "\\s+(?i)IS\\s+NOT\\s+NULL(\\s+AND\\s+)?");
2569
        }
2570
 
2571
        // in MS SQL we're forced to add IS NOT NULL to get the standard behaviour
2572
        // return none if it's not a unique index, otherwise the value of the where for the partial
2573
        // index (can be null)
2574
        final Value<String> getMSUniqueWhere() {
2575
            assert getServer().getSQLSystem() == SQLSystem.MSSQL;
2576
            if (this.isUnique() && this.getFilter() != null) {
2577
                String filter = this.getFilter().trim();
2578
                // for each column, remove its NOT NULL clause
2579
                for (final String col : getCols()) {
2580
                    final Matcher matcher = this.getColPattern(col).matcher(filter);
2581
                    if (matcher.find()) {
2582
                        filter = matcher.replaceFirst("").trim();
2583
                    } else {
2584
                        return Value.getNone();
2585
                    }
2586
                }
2587
                // what is the left is the actual filter
2588
                filter = filter.trim();
2589
                return Value.getSome(filter.isEmpty() ? null : filter);
2590
            }
2591
            return Value.getNone();
2592
        }
144 ilm 2593
 
2594
        final void setH2Class(final Class<? extends PartialUniqueTrigger> triggerClass) {
2595
            this.h2Class = triggerClass;
2596
        }
2597
 
2598
        final UniqueConstraintCreatorHelper createUniqueHelper() {
2599
            final Class<? extends PartialUniqueTrigger> h2Class = this.h2Class == null ? getH2UniqueTriggerClass(getCols(), getFilter()) : this.h2Class;
2600
            final String comment = h2Class == null ? null : "Unique constraint on " + getCols() + (getFilter() == null ? "" : " where " + getFilter());
2601
            return new UniqueConstraintCreatorHelper(getCols(), getFilter(), comment) {
2602
                @Override
2603
                public Object getObject(SQLSyntax s) {
2604
                    if (s.getSystem() == SQLSystem.H2)
2605
                        return h2Class;
2606
                    else
2607
                        return super.getObject(s);
2608
                }
2609
            };
2610
        }
83 ilm 2611
    }
17 ilm 2612
}