OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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