OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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