OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

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