OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 61 | Rev 67 | 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
 
65 ilm 16
import org.openconcerto.sql.utils.SQLCreateMoveableTable;
17 ilm 17
import org.openconcerto.sql.utils.SQLUtils;
18
import org.openconcerto.sql.utils.SQLUtils.SQLFactory;
61 ilm 19
import org.openconcerto.utils.cc.CopyOnWriteMap;
17 ilm 20
import org.openconcerto.utils.change.CollectionChangeEventCreator;
21
import org.openconcerto.xml.JDOMUtils;
22
 
23
import java.io.IOException;
24
import java.sql.DatabaseMetaData;
25
import java.sql.ResultSet;
26
import java.sql.SQLException;
27
import java.util.Collections;
28
import java.util.Date;
29
import java.util.HashMap;
30
import java.util.HashSet;
31
import java.util.List;
32
import java.util.Map;
33
import java.util.Map.Entry;
34
import java.util.Set;
35
 
61 ilm 36
import net.jcip.annotations.GuardedBy;
37
 
17 ilm 38
import org.jdom.Element;
39
 
40
public final class SQLSchema extends SQLIdentifier {
41
 
42
    /**
43
     * set this system property to avoid writing to the db (be it CREATE TABLE or
44
     * INSERT/UPDATE/DELETE)
45
     */
46
    public static final String NOAUTO_CREATE_METADATA = "org.openconcerto.sql.noautoCreateMetadata";
47
 
41 ilm 48
    public static final String FWK_TABLENAME_PREFIX = "FWK_";
49
    static final String METADATA_TABLENAME = FWK_TABLENAME_PREFIX + "SCHEMA_METADATA";
17 ilm 50
    private static final String VERSION_MDKEY = "VERSION";
51
    private static final String VERSION_XMLATTR = "schemaVersion";
52
 
53
    public static final void getVersionAttr(final SQLSchema schema, final Appendable sb) {
54
        final String version = schema.getVersion();
55
        if (version != null) {
56
            try {
57
                sb.append(' ');
58
                sb.append(VERSION_XMLATTR);
59
                sb.append("=\"");
25 ilm 60
                sb.append(JDOMUtils.OUTPUTTER.escapeAttributeEntities(version));
17 ilm 61
                sb.append('"');
62
            } catch (IOException e) {
63
                throw new IllegalStateException("Couldn't append version of " + schema, e);
64
            }
65
        }
66
    }
67
 
68
    public static final String getVersion(final Element schemaElem) {
69
        return schemaElem.getAttributeValue(VERSION_XMLATTR);
70
    }
71
 
72
    public static final String getVersion(final SQLBase base, final String schemaName) {
73
        return base.getFwkMetadata(schemaName, VERSION_MDKEY);
74
    }
75
 
65 ilm 76
    static SQLCreateMoveableTable getCreateMetadata(SQLSyntax syntax) throws SQLException {
77
        if (Boolean.getBoolean(NOAUTO_CREATE_METADATA))
78
            return null;
79
        final SQLCreateMoveableTable create = new SQLCreateMoveableTable(syntax, METADATA_TABLENAME);
80
        create.addVarCharColumn("NAME", 100).addVarCharColumn("VALUE", 250);
81
        create.setPrimaryKey("NAME");
82
        return create;
83
    }
84
 
61 ilm 85
    private final CopyOnWriteMap<String, SQLTable> tables;
17 ilm 86
    // name -> src
87
    private final Map<String, String> procedures;
61 ilm 88
    @GuardedBy("this")
17 ilm 89
    private boolean fetchAllUndefIDs = true;
90
 
91
    SQLSchema(SQLBase base, String name) {
92
        super(base, name);
61 ilm 93
        this.tables = new CopyOnWriteMap<String, SQLTable>();
94
        this.procedures = new CopyOnWriteMap<String, String>();
17 ilm 95
    }
96
 
97
    public final SQLBase getBase() {
98
        return (SQLBase) this.getParent();
99
    }
100
 
101
    // ** procedures
102
 
103
    /**
104
     * Return the procedures names and if possible their source.
105
     *
106
     * @return the procedures in this schema.
107
     */
108
    public final Map<String, String> getProcedures() {
109
        return Collections.unmodifiableMap(this.procedures);
110
    }
111
 
61 ilm 112
    final void putProcedures(final Map<String, String> m) {
113
        this.procedures.putAll(m);
17 ilm 114
    }
115
 
116
    // clear the attributes that are not preserved (ie SQLTable) but recreated each time (ie
117
    // procedure)
118
    void clearNonPersistent() {
119
        this.procedures.clear();
120
    }
121
 
61 ilm 122
    // XMLStructureSource always pre-verify so we don't need the system root lock
17 ilm 123
    void load(Element schemaElem) {
61 ilm 124
        final List<?> l = schemaElem.getChildren("table");
17 ilm 125
        for (int i = 0; i < l.size(); i++) {
126
            final Element elementTable = (Element) l.get(i);
127
            this.refreshTable(elementTable);
128
        }
61 ilm 129
        final Map<String, String> procMap = new HashMap<String, String>();
17 ilm 130
        for (final Object proc : schemaElem.getChild("procedures").getChildren("proc")) {
131
            final Element procElem = (Element) proc;
132
            final Element src = procElem.getChild("src");
61 ilm 133
            procMap.put(procElem.getAttributeValue("name"), src == null ? null : src.getText());
17 ilm 134
        }
61 ilm 135
        this.putProcedures(procMap);
17 ilm 136
    }
137
 
65 ilm 138
    final void refetch() throws SQLException {
139
        this.getBase().fetchTables(Collections.singleton(this.getName()));
140
    }
141
 
61 ilm 142
    /**
143
     * Fetch table from the DB.
144
     *
145
     * @param tableName the name of the table to fetch.
146
     * @return the up to date table, <code>null</code> if not found
147
     * @throws SQLException if an error occurs.
148
     */
65 ilm 149
    final SQLTable fetchTable(final String tableName) throws SQLException {
61 ilm 150
        synchronized (getTreeMutex()) {
151
            synchronized (this) {
152
                final SQLTable existing = getTable(tableName);
153
                if (existing != null) {
154
                    existing.fetchFields();
155
                } else {
156
                    // like in StructureSource create temporary schema
157
                    final SQLSchema tmp = new SQLSchema(getBase(), getName());
158
                    // fetch the requested table
159
                    tmp.addTable(tableName).fetchFields(true);
160
                    // and check if it exists
161
                    final SQLTable newTable = tmp.getTable(tableName);
162
                    if (newTable != null) {
163
                        final SQLTable res = this.addTable(tableName);
164
                        res.mutateTo(newTable);
65 ilm 165
                        this.getDBSystemRoot().descendantsChanged(this, Collections.singleton(tableName), false, true);
61 ilm 166
                        res.save();
167
                    }
168
                }
169
                return this.getTable(tableName);
170
            }
171
        }
172
    }
173
 
17 ilm 174
    void mutateTo(SQLSchema newSchema) {
61 ilm 175
        assert Thread.holdsLock(this.getDBSystemRoot().getTreeMutex());
176
        synchronized (this) {
177
            this.clearNonPersistent();
178
            this.putProcedures(newSchema.procedures);
17 ilm 179
 
61 ilm 180
            for (final SQLTable t : this.getTables()) {
181
                t.mutateTo(newSchema.getTable(t.getName()));
182
            }
17 ilm 183
        }
184
    }
185
 
186
    // ** tables
187
 
61 ilm 188
    final SQLTable addTable(String tableName) {
189
        synchronized (getTreeMutex()) {
190
            return this.addTableWithoutSysRootLock(tableName);
191
        }
192
    }
193
 
194
    final SQLTable addTableWithoutSysRootLock(String tableName) {
17 ilm 195
        if (this.contains(tableName))
196
            throw new IllegalStateException(tableName + " already in " + this);
197
        final CollectionChangeEventCreator c = this.createChildrenCreator();
61 ilm 198
        final SQLTable res = new SQLTable(this, tableName);
199
        this.tables.put(tableName, res);
17 ilm 200
        this.fireChildrenChanged(c);
61 ilm 201
        return res;
17 ilm 202
    }
203
 
61 ilm 204
    private final void refreshTable(Element tableElem) {
17 ilm 205
        final String tableName = tableElem.getAttributeValue("name");
206
        this.getTable(tableName).loadFields(tableElem);
207
    }
208
 
209
    /**
210
     * Refresh the table of the current row of rs.
211
     *
212
     * @param metaData the metadata.
213
     * @param rs the resultSet from getColumns().
214
     * @return whether <code>rs</code> has a next row, <code>null</code> if the current row is not
215
     *         part of this, and thus rs hasn't moved.
216
     * @throws SQLException
217
     */
218
    final Boolean refreshTable(DatabaseMetaData metaData, ResultSet rs) throws SQLException {
61 ilm 219
        synchronized (getTreeMutex()) {
220
            synchronized (this) {
221
                final String tableName = rs.getString("TABLE_NAME");
222
                if (this.contains(tableName)) {
223
                    return this.getTable(tableName).fetchFields(metaData, rs);
224
                } else {
225
                    // eg in pg getColumns() return columns of BATIMENT_ID_seq
226
                    return null;
227
                }
228
            }
17 ilm 229
        }
230
    }
231
 
232
    final void rmTable(String tableName) {
61 ilm 233
        synchronized (getTreeMutex()) {
234
            this.rmTableWithoutSysRootLock(tableName);
235
        }
236
    }
237
 
238
    final void rmTableWithoutSysRootLock(String tableName) {
17 ilm 239
        final CollectionChangeEventCreator c = this.createChildrenCreator();
240
        final SQLTable tableToDrop = this.tables.remove(tableName);
241
        this.fireChildrenChanged(c);
242
        if (tableToDrop != null)
243
            tableToDrop.dropped();
244
    }
245
 
246
    public final SQLTable getTable(String tablename) {
247
        return this.tables.get(tablename);
248
    }
249
 
250
    /**
251
     * Return the tables in this schema.
252
     *
253
     * @return an unmodifiable Set of the tables' names.
254
     */
255
    public Set<String> getTableNames() {
256
        return Collections.unmodifiableSet(this.tables.keySet());
257
    }
258
 
259
    /**
260
     * Return all the tables in this schema.
261
     *
262
     * @return a Set of SQLTable.
263
     */
264
    public Set<SQLTable> getTables() {
265
        return new HashSet<SQLTable>(this.tables.values());
266
    }
267
 
268
    @Override
61 ilm 269
    public Map<String, SQLTable> getChildrenMap() {
270
        return this.tables.getImmutable();
17 ilm 271
    }
272
 
273
    @Override
274
    public String toString() {
275
        return this.getClass().getSimpleName() + " " + this.getName();
276
    }
277
 
278
    public String toXML() {
279
        // a table is about 16000 characters
280
        final StringBuilder sb = new StringBuilder(16000 * 16);
281
        sb.append("<schema ");
282
        if (this.getName() != null) {
283
            sb.append(" name=\"");
25 ilm 284
            sb.append(JDOMUtils.OUTPUTTER.escapeAttributeEntities(this.getName()));
17 ilm 285
            sb.append('"');
286
        }
61 ilm 287
        synchronized (getTreeMutex()) {
288
            synchronized (this) {
289
                getVersionAttr(this, sb);
290
                sb.append(" >\n");
17 ilm 291
 
61 ilm 292
                sb.append("<procedures>\n");
293
                for (final Entry<String, String> e : this.procedures.entrySet()) {
294
                    sb.append("<proc name=\"");
295
                    sb.append(JDOMUtils.OUTPUTTER.escapeAttributeEntities(e.getKey()));
296
                    sb.append("\" ");
297
                    if (e.getValue() == null) {
298
                        sb.append("/>");
299
                    } else {
300
                        sb.append("><src>");
301
                        sb.append(JDOMUtils.OUTPUTTER.escapeElementEntities(e.getValue()));
302
                        sb.append("</src></proc>\n");
303
                    }
304
                }
305
                sb.append("</procedures>\n");
306
                for (final SQLTable table : this.getTables()) {
307
                    // passing our sb to table don't go faster
308
                    sb.append(table.toXML());
309
                    sb.append("\n");
310
                }
311
                sb.append("</schema>");
17 ilm 312
            }
313
        }
314
 
315
        return sb.toString();
316
    }
317
 
318
    String getFwkMetadata(String name) {
319
        if (!this.contains(METADATA_TABLENAME))
320
            return null;
321
 
322
        return this.getBase().getFwkMetadata(this.getName(), name);
323
    }
324
 
325
    boolean setFwkMetadata(String name, String value) throws SQLException {
326
        return this.setFwkMetadata(name, value, true);
327
    }
328
 
329
    /**
330
     * Set the value of a metadata.
331
     *
332
     * @param name name of the metadata, eg "Customer".
333
     * @param value value of the metadata, eg "ACME, inc".
334
     * @param createTable whether the metadata table should be automatically created if necessary.
335
     * @return <code>true</code> if the value was set, <code>false</code> otherwise.
336
     * @throws SQLException if an error occurs while setting the value.
337
     */
338
    boolean setFwkMetadata(String name, String value, boolean createTable) throws SQLException {
339
        if (Boolean.getBoolean(NOAUTO_CREATE_METADATA))
340
            return false;
341
 
61 ilm 342
        synchronized (this.getTreeMutex()) {
343
            // don't refresh until after the insert, that way if the refresh triggers an access to
344
            // the metadata name will already be set to value.
345
            final boolean shouldRefresh;
346
            if (createTable && !this.contains(METADATA_TABLENAME)) {
65 ilm 347
                final SQLCreateMoveableTable create = getCreateMetadata(getServer().getSQLSystem().getSyntax());
348
                this.getBase().getDataSource().execute(create.asString(getDBRoot().getName()));
61 ilm 349
                shouldRefresh = true;
350
            } else
351
                shouldRefresh = false;
352
 
353
            final boolean res;
354
            if (createTable || this.contains(METADATA_TABLENAME)) {
355
                // don't use SQLRowValues, cause it means getting the SQLTable and thus calling
356
                // fetchTables(), but setFwkMetadata() might itself be called by fetchTables()
357
                // furthermore SQLRowValues support only rowable tables
358
                final SQLName tableName = new SQLName(this.getBase().getName(), this.getName(), METADATA_TABLENAME);
359
                final String del = SQLSelect.quote("DELETE FROM %i WHERE %i = %s", tableName, "NAME", name);
360
                final String ins = SQLSelect.quote("INSERT INTO %i(%i,%i) VALUES(%s,%s)", tableName, "NAME", "VALUE", name, value);
361
                SQLUtils.executeAtomic(this.getBase().getDataSource(), new SQLFactory<Object>() {
362
                    public Object create() throws SQLException {
363
                        getBase().getDataSource().execute(del);
364
                        getBase().getDataSource().execute(ins);
365
                        return null;
366
                    }
367
                });
368
                res = true;
369
            } else
370
                res = false;
371
            if (shouldRefresh)
65 ilm 372
                this.fetchTable(METADATA_TABLENAME);
61 ilm 373
            return res;
374
        }
17 ilm 375
    }
376
 
377
    public final String getVersion() {
378
        return this.getFwkMetadata(VERSION_MDKEY);
379
    }
380
 
19 ilm 381
    // TODO assure that the updated version is different that current one and unique
382
    // For now the resolution is the millisecond, this poses 2 problems :
383
    // 1/ if we have a fast in-memory DB, some additions might get lost
384
    // 2/ if not everyone's time is correct ; if client1 calls updateVersion(), then client2 makes
385
    // some changes and calls updateVersion(), his clock might be returning the same time than
386
    // client1 had when he called updateVersion().
387
    // Perhaps something like VERSION = date seconds || '_' || (select cast( substring(indexof '_')
388
    // as int ) + 1 from VERSION) ; e.g. 20110701-1021_01352
17 ilm 389
    public final String updateVersion() throws SQLException {
65 ilm 390
        return this.updateVersion(true);
391
    }
392
 
393
    final String updateVersion(boolean createTable) throws SQLException {
61 ilm 394
        final String res;
395
        synchronized (XMLStructureSource.XMLDATE_FMT) {
396
            res = XMLStructureSource.XMLDATE_FMT.format(new Date());
397
        }
65 ilm 398
        this.setFwkMetadata(SQLSchema.VERSION_MDKEY, res, createTable);
17 ilm 399
        return res;
400
    }
401
 
61 ilm 402
    public synchronized final void setFetchAllUndefinedIDs(final boolean b) {
17 ilm 403
        this.fetchAllUndefIDs = b;
404
    }
405
 
406
    /**
407
     * A boolean indicating if one {@link SQLTable#getUndefinedID()} should fetch IDs for the whole
408
     * schema or just that table. The default is true which is faster but requires that all tables
409
     * are coherent.
410
     *
411
     * @return <code>true</code> if all undefined IDs are fetched together.
412
     */
61 ilm 413
    public synchronized final boolean isFetchAllUndefinedIDs() {
17 ilm 414
        return this.fetchAllUndefIDs;
415
    }
416
}