OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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