OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 142 | 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
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 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
 /*
15
 * DataBase created on 4 mai 2004
16
 */
17
package org.openconcerto.sql.model;
18
 
19
import org.openconcerto.sql.Log;
63 ilm 20
import org.openconcerto.sql.model.LoadingListener.LoadingEvent;
21
import org.openconcerto.sql.model.LoadingListener.StructureLoadingEvent;
17 ilm 22
import org.openconcerto.sql.model.graph.DatabaseGraph;
67 ilm 23
import org.openconcerto.sql.model.graph.TablesMap;
24
import org.openconcerto.sql.utils.SQLUtils;
17 ilm 25
import org.openconcerto.utils.CollectionUtils;
26
import org.openconcerto.utils.FileUtils;
61 ilm 27
import org.openconcerto.utils.Tuple3;
28
import org.openconcerto.utils.cc.CopyOnWriteMap;
17 ilm 29
import org.openconcerto.utils.cc.IClosure;
83 ilm 30
import org.openconcerto.utils.cc.ITransformer;
17 ilm 31
import org.openconcerto.utils.change.CollectionChangeEventCreator;
32
 
33
import java.io.File;
67 ilm 34
import java.io.IOException;
35
import java.io.Writer;
80 ilm 36
import java.security.AccessController;
37
import java.security.PrivilegedAction;
17 ilm 38
import java.sql.DatabaseMetaData;
39
import java.sql.ResultSet;
40
import java.sql.SQLException;
83 ilm 41
import java.util.Collection;
17 ilm 42
import java.util.Collections;
43
import java.util.HashMap;
44
import java.util.HashSet;
83 ilm 45
import java.util.LinkedHashMap;
17 ilm 46
import java.util.List;
47
import java.util.Map;
73 ilm 48
import java.util.Map.Entry;
182 ilm 49
import java.util.Objects;
17 ilm 50
import java.util.Set;
65 ilm 51
import java.util.logging.Level;
52
import java.util.logging.Logger;
17 ilm 53
import java.util.regex.Matcher;
54
import java.util.regex.Pattern;
55
 
142 ilm 56
import org.apache.commons.dbutils.ResultSetHandler;
57
 
61 ilm 58
import net.jcip.annotations.GuardedBy;
59
import net.jcip.annotations.ThreadSafe;
60
 
17 ilm 61
/**
62
 * Une base de donnée SQL. Une base est unique, pour obtenir une instance il faut passer par
63
 * SQLServer. Une base permet d'accéder aux tables qui la composent, ainsi qu'à son graphe.
64
 *
65
 * @author ILM Informatique 4 mai 2004
61 ilm 66
 * @see org.openconcerto.sql.model.SQLServer#getOrCreateBase(String)
17 ilm 67
 * @see #getTable(String)
68
 * @see #getGraph()
69
 */
61 ilm 70
@ThreadSafe
142 ilm 71
public final class SQLBase extends SQLIdentifier {
17 ilm 72
 
73
    /**
74
     * Boolean system property, if <code>true</code> then the structure and the graph of SQL base
67 ilm 75
     * will default to be loaded from XML instead of JDBC.
76
     *
77
     * @see DBSystemRoot#useCache()
17 ilm 78
     */
79
    public static final String STRUCTURE_USE_XML = "org.openconcerto.sql.structure.useXML";
19 ilm 80
    /**
67 ilm 81
     * Boolean system property, if <code>true</code> then when the structure of SQL base cannot be
82
     * loaded from XML, the files are not deleted.
83
     */
84
    public static final String STRUCTURE_KEEP_INVALID_XML = "org.openconcerto.sql.structure.keepInvalidXML";
85
    /**
19 ilm 86
     * Boolean system property, if <code>true</code> then schemas and tables can be dropped,
67 ilm 87
     * otherwise the refresh will throw an exception.
19 ilm 88
     */
89
    public static final String ALLOW_OBJECT_REMOVAL = "org.openconcerto.sql.identifier.allowRemoval";
17 ilm 90
 
20 ilm 91
    static public final void logCacheError(final DBItemFileCache dir, Exception e) {
65 ilm 92
        final Logger logger = Log.get();
93
        if (logger.isLoggable(Level.CONFIG))
94
            logger.log(Level.CONFIG, "invalid files in " + dir, e);
95
        else
96
            logger.info("invalid files in " + dir + "\n" + e.getMessage());
20 ilm 97
    }
98
 
17 ilm 99
    // null is a valid name (MySQL doesn't support schemas)
61 ilm 100
    private final CopyOnWriteMap<String, SQLSchema> schemas;
101
    @GuardedBy("this")
17 ilm 102
    private int[] dbVersion;
103
 
104
    /**
105
     * Crée une base dans <i>server </i> nommée <i>name </i>.
106
     * <p>
61 ilm 107
     * Note: ne pas utiliser ce constructeur, utiliser {@link SQLServer#getOrCreateBase(String)}
17 ilm 108
     * </p>
109
     *
110
     * @param server son serveur.
111
     * @param name son nom.
112
     * @param login the login.
113
     * @param pass the password.
114
     */
115
    SQLBase(SQLServer server, String name, String login, String pass) {
83 ilm 116
        this(server, name, null, login, pass, null);
17 ilm 117
    }
118
 
119
    /**
120
     * Creates a base in <i>server</i> named <i>name</i>.
121
     * <p>
61 ilm 122
     * Note: don't use this constructor, use {@link SQLServer#getOrCreateBase(String)}
17 ilm 123
     * </p>
124
     *
125
     * @param server its server.
126
     * @param name its name.
83 ilm 127
     * @param systemRootInit to initialize the {@link DBSystemRoot} before setting the datasource.
17 ilm 128
     * @param login the login.
129
     * @param pass the password.
130
     * @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
131
     *        can be <code>null</code>.
132
     */
83 ilm 133
    SQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
17 ilm 134
        super(server, name);
135
        if (name == null)
136
            throw new NullPointerException("null base");
61 ilm 137
        this.schemas = new CopyOnWriteMap<String, SQLSchema>();
17 ilm 138
        this.dbVersion = null;
139
 
140
        // if this is the systemRoot we must init the datasource to be able to loadTables()
141
        final DBSystemRoot sysRoot = this.getDBSystemRoot();
142
        if (sysRoot.getJDBC() == this)
83 ilm 143
            sysRoot.setDS(systemRootInit, login, pass, dsInit);
61 ilm 144
    }
17 ilm 145
 
67 ilm 146
    final TablesMap init(final boolean readCache) {
17 ilm 147
        try {
67 ilm 148
            return refresh(null, readCache, true);
17 ilm 149
        } catch (SQLException e) {
65 ilm 150
            throw new IllegalStateException("could not init " + this, e);
17 ilm 151
        }
152
    }
153
 
154
    @Override
61 ilm 155
    protected synchronized void onDrop() {
17 ilm 156
        // allow schemas (and their descendants) to be gc'd even we aren't
157
        this.schemas.clear();
158
        super.onDrop();
159
    }
160
 
67 ilm 161
    TablesMap refresh(final TablesMap namesToRefresh, final boolean readCache) throws SQLException {
162
        return this.refresh(namesToRefresh, readCache, false);
65 ilm 163
    }
164
 
67 ilm 165
    // what tables were loaded by JDBC
166
    private TablesMap refresh(final TablesMap namesToRefresh, final boolean readCache, final boolean inCtor) throws SQLException {
65 ilm 167
        if (readCache)
67 ilm 168
            return loadTables(namesToRefresh, inCtor);
65 ilm 169
        else
67 ilm 170
            return fetchTables(namesToRefresh);
65 ilm 171
    }
172
 
67 ilm 173
    private final TablesMap loadTables(TablesMap childrenNames, boolean inCtor) throws SQLException {
61 ilm 174
        this.checkDropped();
67 ilm 175
        if (childrenNames != null && childrenNames.size() == 0)
176
            return childrenNames;
177
        childrenNames = assureAllTables(childrenNames);
17 ilm 178
        final DBItemFileCache dir = getFileCache();
61 ilm 179
        synchronized (getTreeMutex()) {
67 ilm 180
            XMLStructureSource xmlStructSrc = null;
61 ilm 181
            if (dir != null) {
182
                try {
183
                    Log.get().config("for mapping " + this + " trying xmls in " + dir);
184
                    final long t1 = System.currentTimeMillis();
185
                    // don't call refreshTables() with XML :
186
                    // say you have one schema "s" and its file is missing or corrupted
187
                    // refreshTables(XML) will drop it from our children
188
                    // then we will call refreshTables(JDBC) and it will be re-added
189
                    // => so we removed our child for nothing (firing unneeded events, rendering
190
                    // java objects useless and possibly destroying the systemRoot path)
67 ilm 191
                    xmlStructSrc = new XMLStructureSource(this, childrenNames, dir);
192
                    assert xmlStructSrc.isPreVerify();
61 ilm 193
                    xmlStructSrc.init();
194
                    final long t2 = System.currentTimeMillis();
195
                    Log.get().config("XML took " + (t2 - t1) + "ms for mapping " + this.getName() + "." + xmlStructSrc.getSchemas());
196
                } catch (Exception e) {
197
                    logCacheError(dir, e);
67 ilm 198
                    // since isPreVerify() is true, schemas weren't changed.
199
                    // if an error reached us, we cannot trust the loaded structure (e.g.
200
                    // IOExceptions are handled by XMLStructureSource)
201
                    xmlStructSrc = null;
17 ilm 202
                }
203
            }
61 ilm 204
 
205
            final long t1 = System.currentTimeMillis();
206
            // always do the fetchTables() since XML do nothing anymore
67 ilm 207
            final JDBCStructureSource jdbcStructSrc = this.fetchTablesP(childrenNames, xmlStructSrc);
61 ilm 208
            final long t2 = System.currentTimeMillis();
209
            Log.get().config("JDBC took " + (t2 - t1) + "ms for mapping " + this.getName() + "." + jdbcStructSrc.getSchemas());
67 ilm 210
            return jdbcStructSrc.getTablesMap();
17 ilm 211
        }
212
    }
213
 
67 ilm 214
    private final TablesMap assureAllTables(final TablesMap childrenNames) {
215
        // don't allow partial schemas (we do the same in SQLServer.refresh()) since
216
        // JDBCStructureSource needs to check for SQLSchema.METADATA_TABLENAME
217
        final TablesMap res;
218
        if (childrenNames == null) {
219
            res = childrenNames;
220
        } else {
221
            res = TablesMap.create(childrenNames);
222
            for (final Entry<String, Set<String>> e : childrenNames.entrySet()) {
223
                final String schemaName = e.getKey();
224
                if (e.getValue() != null && !this.contains(schemaName)) {
225
                    res.put(schemaName, null);
226
                }
227
            }
228
        }
229
        return res;
17 ilm 230
    }
231
 
232
    /**
233
     * Load the structure from JDBC.
234
     *
67 ilm 235
     * @param childrenNames which children to refresh, <code>null</code> meaning all.
236
     * @return tables actually loaded, never <code>null</code>.
17 ilm 237
     * @throws SQLException if an error occurs.
238
     * @see DBSystemRoot#refetch(Set)
239
     */
67 ilm 240
    TablesMap fetchTables(TablesMap childrenNames) throws SQLException {
241
        if (childrenNames != null && childrenNames.size() == 0)
242
            return childrenNames;
243
        return this.fetchTablesP(assureAllTables(childrenNames), null).getTablesMap();
17 ilm 244
    }
245
 
67 ilm 246
    private JDBCStructureSource fetchTablesP(TablesMap childrenNames, StructureSource<?> external) throws SQLException {
247
        // TODO pass TablesByRoot to event
248
        final LoadingEvent evt = new StructureLoadingEvent(this, childrenNames == null ? null : childrenNames.keySet());
63 ilm 249
        final DBSystemRoot sysRoot = this.getDBSystemRoot();
250
        try {
251
            sysRoot.fireLoading(evt);
67 ilm 252
            return this.refreshTables(new JDBCStructureSource(this, childrenNames, external == null ? null : external.getNewStructure(), external == null ? null : external.getOutOfDateSchemas()));
63 ilm 253
        } finally {
254
            sysRoot.fireLoading(evt.createFinishingEvent());
255
        }
17 ilm 256
    }
257
 
67 ilm 258
    final TablesMap loadTables() throws SQLException {
17 ilm 259
        return this.loadTables(null);
260
    }
261
 
262
    /**
263
     * Tries to load the structure from XMLs, if that fails fallback to JDBC.
264
     *
265
     * @param childrenNames which children to refresh.
67 ilm 266
     * @return tables loaded with JDBC.
17 ilm 267
     * @throws SQLException if an error occurs in JDBC.
268
     */
67 ilm 269
    final TablesMap loadTables(TablesMap childrenNames) throws SQLException {
17 ilm 270
        return this.loadTables(childrenNames, false);
271
    }
272
 
61 ilm 273
    private final <T extends Exception, S extends StructureSource<T>> S refreshTables(final S src) throws T {
274
        this.checkDropped();
275
        synchronized (getTreeMutex()) {
276
            src.init();
17 ilm 277
 
61 ilm 278
            // refresh schemas
279
            final Set<String> newSchemas = src.getTotalSchemas();
67 ilm 280
            final Set<String> currentSchemas = src.getExistingSchemasToRefresh();
61 ilm 281
            mustContain(this, newSchemas, currentSchemas, "schemas");
80 ilm 282
            final CollectionChangeEventCreator c = this.createChildrenCreator();
61 ilm 283
            // remove all schemas that are not there anymore
284
            for (final String schema : CollectionUtils.substract(currentSchemas, newSchemas)) {
285
                this.schemas.remove(schema).dropped();
17 ilm 286
            }
61 ilm 287
            // delete the saved schemas that we could have fetched, but haven't
288
            // (schemas that are not in scope are simply ignored, NOT deleted)
80 ilm 289
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
290
                @Override
291
                public Object run() {
292
                    for (final DBItemFileCache savedSchema : getSavedCaches(false)) {
293
                        if (src.isInTotalScope(savedSchema.getName()) && !newSchemas.contains(savedSchema.getName())) {
294
                            savedSchema.delete();
295
                        }
296
                    }
297
                    return null;
61 ilm 298
                }
80 ilm 299
            });
300
 
61 ilm 301
            // clearNonPersistent (will be recreated by fillTables())
302
            for (final String schema : CollectionUtils.inter(currentSchemas, newSchemas)) {
303
                this.getSchema(schema).clearNonPersistent();
304
            }
305
            // create the new ones
306
            for (final String schema : newSchemas) {
307
                this.createAndGetSchema(schema);
308
            }
17 ilm 309
 
61 ilm 310
            // refresh tables
311
            final Set<SQLName> newTableNames = src.getTotalTablesNames();
67 ilm 312
            final Set<SQLName> currentTables = src.getExistingTablesToRefresh();
61 ilm 313
            // we can only add, cause instances of SQLTable are everywhere
314
            mustContain(this, newTableNames, currentTables, "tables");
315
            // remove dropped tables
316
            for (final SQLName tableName : CollectionUtils.substract(currentTables, newTableNames)) {
317
                final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
318
                s.rmTable(tableName.getName());
319
            }
320
            // clearNonPersistent
321
            for (final SQLName tableName : CollectionUtils.inter(newTableNames, currentTables)) {
322
                final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
323
                s.getTable(tableName.getName()).clearNonPersistent();
324
            }
325
            // create new table descendants (including empty tables)
326
            for (final SQLName tableName : CollectionUtils.substract(newTableNames, currentTables)) {
327
                final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
328
                s.addTable(tableName.getName());
329
            }
17 ilm 330
 
61 ilm 331
            // fill with columns
332
            src.fillTables();
17 ilm 333
 
80 ilm 334
            this.fireChildrenChanged(c);
61 ilm 335
            // don't signal our systemRoot if our server doesn't yet reference us,
336
            // otherwise the server will create another instance and enter an infinite loop
337
            assert this.getServer().getBase(this.getName()) == this;
67 ilm 338
            final TablesMap byRoot;
339
            final TablesMap toRefresh = src.getToRefresh();
340
            if (toRefresh == null) {
341
                byRoot = TablesMap.createByRootFromChildren(this, null);
342
            } else {
343
                final DBRoot root = this.getDBRoot();
344
                if (root != null) {
345
                    byRoot = TablesMap.createFromTables(root.getName(), toRefresh.get(null));
346
                } else {
347
                    byRoot = toRefresh;
348
                }
349
            }
80 ilm 350
            this.getDBSystemRoot().descendantsChanged(byRoot, src.hasExternalStruct());
61 ilm 351
        }
17 ilm 352
        src.save();
353
        return src;
354
    }
355
 
356
    static <T> void mustContain(final DBStructureItemJDBC c, final Set<T> newC, final Set<T> oldC, final String name) {
19 ilm 357
        if (Boolean.getBoolean(ALLOW_OBJECT_REMOVAL))
17 ilm 358
            return;
359
 
360
        final Set<T> diff = CollectionUtils.contains(newC, oldC);
361
        if (diff != null)
362
            throw new IllegalStateException("some " + name + " were removed in " + c + ": " + diff);
363
    }
364
 
365
    public final String getURL() {
366
        return this.getServer().getURL(this.getName());
367
    }
368
 
369
    /**
370
     * Return the field named <i>fieldName </i> in this base.
371
     *
372
     * @param fieldName the fully qualified name of the field.
373
     * @return the matching field or null if none exists.
65 ilm 374
     * @deprecated use {@link SQLTable#getField(String)} and {@link DBRoot#getTable(String)} or at
375
     *             worst {@link #getTable(SQLName)}
17 ilm 376
     */
377
    public SQLField getField(String fieldName) {
378
        String[] parts = fieldName.split("\\.");
379
        if (parts.length != 2) {
380
            throw new IllegalArgumentException(fieldName + " is not a fully qualified name (like TABLE.FIELD_NAME).");
381
        }
382
        String table = parts[0];
383
        String field = parts[1];
384
        if (!this.containsTable(table))
385
            return null;
386
        else
387
            return this.getTable(table).getField(field);
388
    }
389
 
390
    /**
391
     * Return the table named <i>tablename </i> in this base.
392
     *
393
     * @param tablename the name of the table.
394
     * @return the matching table or null if none exists.
395
     */
396
    public SQLTable getTable(String tablename) {
397
        return this.getTable(SQLName.parse(tablename));
398
    }
399
 
400
    public SQLTable getTable(SQLName n) {
401
        if (n.getItemCount() == 0 || n.getItemCount() > 2)
402
            throw new IllegalArgumentException("'" + n + "' is not a dotted tablename");
403
 
404
        if (n.getItemCount() == 1) {
405
            return this.findTable(n.getName());
406
        } else {
407
            final SQLSchema s = this.getSchema(n.getFirst());
408
            if (s == null)
409
                return null;
410
            else
411
                return s.getTable(n.getName());
412
        }
413
    }
414
 
415
    private SQLTable findTable(String name) {
416
        final DBRoot guessed = this.guessDBRoot();
417
        return guessed == null ? this.getDBSystemRoot().findTable(name) : guessed.findTable(name);
418
    }
419
 
420
    /**
421
     * Return whether this base contains the table.
422
     *
423
     * @param tableName the name of the table.
424
     * @return true if the tableName exists.
425
     */
426
    public boolean containsTable(String tableName) {
427
        return contains(SQLName.parse(tableName));
428
    }
429
 
430
    private boolean contains(final SQLName n) {
431
        return this.getTable(n) != null;
432
    }
433
 
434
    /**
435
     * Return the tables in the default schema.
436
     *
437
     * @return an unmodifiable Set of the tables' names.
438
     */
439
    public Set<String> getTableNames() {
440
        return this.getDefaultSchema().getTableNames();
441
    }
442
 
443
    /**
444
     * Return the tables in the default schema.
445
     *
446
     * @return a Set of SQLTable.
447
     */
448
    public Set<SQLTable> getTables() {
449
        return this.getDefaultSchema().getTables();
450
    }
451
 
452
    // *** all*
453
 
454
    public Set<SQLName> getAllTableNames() {
455
        final Set<SQLName> res = new HashSet<SQLName>();
456
        for (final SQLTable t : this.getAllTables()) {
457
            res.add(t.getSQLName(this, false));
458
        }
459
        return res;
460
    }
461
 
462
    public Set<SQLTable> getAllTables() {
463
        final Set<SQLTable> res = new HashSet<SQLTable>();
464
        for (final SQLSchema s : this.getSchemas()) {
465
            res.addAll(s.getTables());
466
        }
467
        return res;
468
    }
469
 
470
    // *** schemas
471
 
472
    @Override
61 ilm 473
    public Map<String, SQLSchema> getChildrenMap() {
474
        return this.schemas.getImmutable();
17 ilm 475
    }
476
 
477
    public final Set<SQLSchema> getSchemas() {
478
        return new HashSet<SQLSchema>(this.schemas.values());
479
    }
480
 
481
    public final SQLSchema getSchema(String name) {
482
        return this.schemas.get(name);
483
    }
484
 
485
    /**
486
     * The current default schema.
487
     *
488
     * @return the default schema or <code>null</code>.
489
     */
490
    final SQLSchema getDefaultSchema() {
61 ilm 491
        final Map<String, SQLSchema> children = this.getChildrenMap();
492
        if (children.size() == 0) {
17 ilm 493
            return null;
61 ilm 494
        } else if (children.size() == 1) {
495
            return children.values().iterator().next();
496
        } else if (this.getServer().getSQLSystem().getLevel(DBRoot.class) == HierarchyLevel.SQLSCHEMA) {
497
            final List<String> path = this.getDBSystemRoot().getRootPath();
498
            if (path.size() > 0)
499
                return children.get(path.get(0));
500
        }
501
        throw new IllegalStateException();
17 ilm 502
    }
503
 
504
    private SQLSchema createAndGetSchema(String name) {
505
        SQLSchema res = this.getSchema(name);
506
        if (res == null) {
507
            res = new SQLSchema(this, name);
508
            this.schemas.put(name, res);
509
        }
510
        return res;
511
    }
512
 
513
    public final DBRoot guessDBRoot() {
514
        if (this.getDBRoot() != null)
515
            return this.getDBRoot();
516
        else
517
            return this.getDBSystemRoot().getDefaultRoot();
518
    }
519
 
520
    public DatabaseGraph getGraph() {
521
        if (this.getDBRoot() == null)
522
            return this.getDBSystemRoot().getGraph();
523
        else
524
            return this.getDBRoot().getGraph();
525
    }
526
 
527
    /**
528
     * Vérifie l'intégrité de la base. C'est à dire que les clefs étrangères pointent sur des lignes
529
     * existantes. Cette méthode renvoie une Map dont les clefs sont les tables présentant des
530
     * inconsistences. Les valeurs de cette Map sont des List de SQLRow.
531
     *
532
     * @return les inconsistences.
533
     * @see SQLTable#checkIntegrity()
534
     */
61 ilm 535
    public Map<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>> checkIntegrity() {
536
        final Map<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>> inconsistencies = new HashMap<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>>();
17 ilm 537
        for (final SQLTable table : this.getAllTables()) {
61 ilm 538
            List<Tuple3<SQLRow, SQLField, SQLRow>> tableInc = table.checkIntegrity();
17 ilm 539
            if (tableInc.size() > 0)
540
                inconsistencies.put(table, tableInc);
541
        }
542
        return inconsistencies;
543
    }
544
 
545
    /**
546
     * Exécute la requête dans le contexte de cette base et retourne le résultat. Le résultat d'une
547
     * insertion étant les clefs auto-générées, eg le nouvel ID.
548
     *
549
     * @deprecated use getDataSource()
550
     * @param query le requête à exécuter.
551
     * @return le résultat de la requête.
552
     * @see java.sql.Statement#getGeneratedKeys()
553
     */
554
    public ResultSet execute(String query) {
555
        return this.getDataSource().executeRaw(query);
556
    }
557
 
558
    public SQLDataSource getDataSource() {
559
        return this.getDBSystemRoot().getDataSource();
560
    }
561
 
562
    public String toString() {
563
        return this.getName();
564
    }
565
 
566
    // ** metadata
567
 
67 ilm 568
    /**
569
     * Get a metadata.
570
     *
571
     * @param schema the name of the schema.
572
     * @param name the name of the meta data.
573
     * @return the requested meta data, can be <code>null</code> (including if
574
     *         {@value SQLSchema#METADATA_TABLENAME} does not exist).
575
     */
83 ilm 576
    String getFwkMetadata(String schema, String name) {
577
        return getFwkMetadata(Collections.singletonList(schema), name).get(schema);
578
    }
579
 
580
    private final String getSel(final String schema, final String name, final boolean selSchema) {
17 ilm 581
        final SQLName tableName = new SQLName(this.getName(), schema, SQLSchema.METADATA_TABLENAME);
83 ilm 582
        return "SELECT " + (selSchema ? this.quoteString(schema) + ", " : "") + "\"VALUE\" FROM " + tableName.quote() + " WHERE \"NAME\"= " + this.quoteString(name);
583
    }
584
 
585
    private final void exec(final Collection<String> schemas, final String name, final ResultSetHandler rsh) {
586
        this.getDataSource().execute(CollectionUtils.join(schemas, "\nUNION ALL ", new ITransformer<String, String>() {
587
            @Override
588
            public String transformChecked(String schema) {
589
                // schema name needed since missing values will result in missing rows not
590
                // null values
591
                return getSel(schema, name, true);
592
            }
593
        }), new IResultSetHandler(rsh, false));
594
    }
595
 
596
    Map<String, String> getFwkMetadata(final Collection<String> schemas, final String name) {
597
        if (schemas.isEmpty())
598
            return Collections.emptyMap();
599
        final Map<String, String> res = new LinkedHashMap<String, String>();
600
        CollectionUtils.fillMap(res, schemas);
601
        final ResultSetHandler rsh = new ResultSetHandler() {
602
            @Override
603
            public Object handle(ResultSet rs) throws SQLException {
604
                while (rs.next()) {
605
                    res.put(rs.getString(1), rs.getString(2));
606
                }
607
                return null;
608
            }
609
        };
610
        try {
611
            if (this.getDataSource().getTransactionPoint() == null) {
612
                exec(schemas, name, rsh);
613
            } else {
614
                // If already in a transaction, don't risk aborting it if a table doesn't exist.
615
                // (it's not strictly required for H2 and MySQL, since the transaction is *not*
616
                // aborted)
617
                SQLUtils.executeAtomic(this.getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
67 ilm 618
                    @Override
83 ilm 619
                    public Object handle(SQLDataSource ds) throws SQLException {
620
                        exec(schemas, name, rsh);
621
                        return null;
67 ilm 622
                    }
83 ilm 623
                }, false);
67 ilm 624
            }
83 ilm 625
        } catch (Exception exn) {
626
            final SQLException sqlExn = SQLUtils.findWithSQLState(exn);
627
            final boolean tableNotFound = sqlExn != null && (sqlExn.getSQLState().equals("42S02") || sqlExn.getSQLState().equals("42P01"));
628
            if (!tableNotFound)
132 ilm 629
                throw new IllegalStateException("Not a missing table exception", exn);
83 ilm 630
 
631
            // The following fall back should not currently be needed since the table is created
632
            // by JDBCStructureSource.getNames(). Even without that most DB should contain the
633
            // metadata tables.
634
 
635
            // if only one schema, there's no ambiguity : just return null value
636
            // otherwise retry with each single schema to find out which ones are missing
637
            if (schemas.size() > 1) {
638
                // this won't loop indefinetly since schemas.size() will be 1
639
                for (final String schema : schemas)
640
                    res.put(schema, this.getFwkMetadata(schema, name));
67 ilm 641
            }
642
        }
83 ilm 643
        return res;
17 ilm 644
    }
645
 
646
    public final String getMDName() {
647
        return this.getServer().getSQLSystem().getMDName(this.getName());
648
    }
649
 
61 ilm 650
    public synchronized int[] getVersion() throws SQLException {
17 ilm 651
        if (this.dbVersion == null) {
652
            this.dbVersion = this.getDataSource().useConnection(new ConnectionHandlerNoSetup<int[], SQLException>() {
653
                @Override
654
                public int[] handle(SQLDataSource ds) throws SQLException, SQLException {
655
                    final DatabaseMetaData md = ds.getConnection().getMetaData();
656
                    return new int[] { md.getDatabaseMajorVersion(), md.getDatabaseMinorVersion() };
657
                }
658
            });
659
        }
660
        return this.dbVersion;
661
    }
662
 
663
    // ** files
664
 
67 ilm 665
    static final String FILENAME = "structure.xml";
17 ilm 666
 
667
    static final boolean isSaved(final SQLServer s, final String base, final String schema) {
668
        return s.getFileCache().getChild(base, schema).getFile(SQLBase.FILENAME).exists();
669
    }
670
 
671
    /**
67 ilm 672
     * Where xml dumps are saved, always <code>null</code> if {@link DBSystemRoot#useCache()} is
17 ilm 673
     * <code>false</code>.
674
     *
675
     * @return the directory of xmls dumps, <code>null</code> if it can't be found.
676
     */
677
    private final DBItemFileCache getFileCache() {
67 ilm 678
        final boolean useXML = this.getDBSystemRoot().useCache();
17 ilm 679
        final DBFileCache fileCache = this.getServer().getFileCache();
680
        if (!useXML || fileCache == null)
681
            return null;
682
        else {
683
            return fileCache.getChild(this.getName());
684
        }
685
    }
686
 
67 ilm 687
    private final DBItemFileCache getSchemaFileCache(String schema) {
17 ilm 688
        final DBItemFileCache item = this.getFileCache();
689
        if (item == null)
690
            return null;
67 ilm 691
        return item.getChild(schema);
17 ilm 692
    }
693
 
67 ilm 694
    final List<DBItemFileCache> getSavedShemaCaches() {
695
        return this.getSavedCaches(true);
696
    }
697
 
17 ilm 698
    private final List<DBItemFileCache> getSavedCaches(boolean withStruct) {
699
        final DBItemFileCache item = this.getFileCache();
700
        if (item == null)
701
            return Collections.emptyList();
702
        else {
703
            return item.getSavedDesc(SQLSchema.class, withStruct ? FILENAME : null);
704
        }
705
    }
706
 
707
    final boolean isSaved(String schema) {
708
        return isSaved(this.getServer(), this.getName(), schema);
709
    }
710
 
711
    /**
712
     * Deletes all files containing information about this base's structure.
713
     */
714
    public void deleteStructureFiles() {
715
        for (final DBItemFileCache f : this.getSavedCaches(true)) {
716
            f.getFile(FILENAME).delete();
717
        }
718
    }
719
 
80 ilm 720
    boolean save(final String schemaName) {
67 ilm 721
        final DBItemFileCache schemaFileCache = this.getSchemaFileCache(schemaName);
722
        if (schemaFileCache == null) {
17 ilm 723
            return false;
67 ilm 724
        } else {
725
            final File schemaFile = schemaFileCache.getFile(FILENAME);
80 ilm 726
            return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
727
                @Override
728
                public Boolean run() {
729
                    Writer pWriter = null;
730
                    try {
731
                        final String schema = getSchema(schemaName).toXML();
732
                        if (schema == null)
733
                            return false;
734
                        FileUtils.mkdir_p(schemaFile.getParentFile());
735
                        // Might save garbage if two threads open the same file
736
                        synchronized (this) {
737
                            pWriter = FileUtils.createXMLWriter(schemaFile);
738
                            pWriter.write("<root codecVersion=\"" + XMLStructureSource.version + "\" >\n" + schema + "\n</root>\n");
739
                        }
17 ilm 740
 
80 ilm 741
                        return true;
742
                    } catch (Exception e) {
743
                        Log.get().log(Level.WARNING, "unable to save files in " + schemaFile, e);
744
                        return false;
745
                    } finally {
746
                        if (pWriter != null) {
747
                            try {
748
                                pWriter.close();
749
                            } catch (IOException e) {
750
                                e.printStackTrace();
751
                            }
752
                        }
67 ilm 753
                    }
754
                }
80 ilm 755
            });
67 ilm 756
        }
17 ilm 757
    }
758
 
759
    // *** quoting
760
 
761
    // * quote
762
 
763
    /**
764
     * Quote %-escaped parameters. %% : %, %s : {@link #quoteString(String)}, %i : an identifier
765
     * string, if it's a SQLName calls {@link SQLName#quote()} else {@link #quoteIdentifier(String)}
766
     * , %f or %n : respectively fullName and name of an SQLIdentifier of a DBStructureItem.
767
     *
768
     * @param pattern a string with %, eg "SELECT * FROM %n where %f like '%%a%%'".
769
     * @param params the parameters, eg [ /TENSION/, |TENSION.LABEL| ].
770
     * @return pattern with % replaced, eg SELECT * FROM "TENSION" where "TENSION.LABEL" like '%a%'.
771
     */
772
    public final String quote(final String pattern, Object... params) {
182 ilm 773
        return quote(this.getDBSystemRoot().getSyntax(), pattern, params);
17 ilm 774
    }
775
 
776
    static private final Pattern percent = Pattern.compile("%.");
777
 
182 ilm 778
    final static String quote(final SQLSyntax s, final String pattern, Object... params) {
779
        Objects.requireNonNull(s, "Missing syntax");
17 ilm 780
        final Matcher m = percent.matcher(pattern);
781
        final StringBuffer sb = new StringBuffer();
782
        int i = 0;
783
        int lastAppendPosition = 0;
784
        while (m.find()) {
785
            final String replacement;
786
            final char modifier = m.group().charAt(m.group().length() - 1);
787
            if (modifier == '%') {
788
                replacement = "%";
789
            } else {
790
                final Object param = params[i++];
791
                if (modifier == 's') {
182 ilm 792
                    replacement = s.quoteString(param.toString());
17 ilm 793
                } else if (modifier == 'i') {
794
                    if (param instanceof SQLName)
795
                        replacement = ((SQLName) param).quote();
796
                    else
797
                        replacement = quoteIdentifier(param.toString());
798
                } else {
80 ilm 799
                    final SQLIdentifier ident = (SQLIdentifier) ((DBStructureItem<?>) param).getJDBC();
17 ilm 800
                    if (modifier == 'f') {
801
                        replacement = ident.getSQLName().quote();
802
                    } else if (modifier == 'n')
803
                        replacement = quoteIdentifier(ident.getName());
804
                    else
805
                        throw new IllegalArgumentException("unknown modifier: " + modifier);
806
                }
807
            }
808
 
809
            // do NOT use appendReplacement() (and appendTail()) since it parses \ and $
810
            // Append the intervening text
811
            sb.append(pattern.subSequence(lastAppendPosition, m.start()));
812
            // Append the match substitution
813
            sb.append(replacement);
814
            lastAppendPosition = m.end();
815
        }
816
        sb.append(pattern.substring(lastAppendPosition));
817
        return sb.toString();
818
    }
819
 
820
    // * quoteString
821
 
822
    /**
823
     * Quote an sql string specifically for this base.
824
     *
825
     * @param s an arbitrary string, eg "salut\ l'ami".
826
     * @return the quoted form, eg "'salut\\ l''ami'".
827
     * @see #quoteStringStd(String)
828
     */
829
    public String quoteString(String s) {
142 ilm 830
        return SQLSyntax.get(this).quoteString(s);
17 ilm 831
    }
832
 
67 ilm 833
    static private final Pattern singleQuote = Pattern.compile("'", Pattern.LITERAL);
142 ilm 834
    static public final Pattern quotedPatrn = Pattern.compile("'(('')|[^'])*'");
67 ilm 835
    static private final Pattern twoSingleQuote = Pattern.compile("''", Pattern.LITERAL);
17 ilm 836
 
837
    /**
838
     * Quote an sql string the standard way. See section 4.1.2.1. String Constants of postgresql
839
     * documentation.
840
     *
841
     * @param s an arbitrary string, eg "salut\ l'ami".
842
     * @return the quoted form, eg "'salut\ l''ami'".
843
     */
844
    public final static String quoteStringStd(String s) {
83 ilm 845
        return s == null ? "NULL" : "'" + singleQuote.matcher(s).replaceAll("''") + "'";
17 ilm 846
    }
847
 
67 ilm 848
    /**
849
     * Unquote an SQL string the standard way.
850
     * <p>
851
     * NOTE : There's no unquoteString() instance method since it can be affected by session
852
     * parameters. So to be correct the method should execute a request each time to find out these
853
     * values. But if it did that, it might as well execute <code>"SELECT ?"</code> with the string
854
     * (and <b>not</b> <code>executeScalar("SELECT " + s)</code> to avoid SQL injection).
855
     * </p>
856
     *
857
     * @param s an arbitrary SQL string, e.g. 'salu\t l''ami'.
858
     * @return the java string, e.g. "salu\\t l'ami".
859
     * @see #quoteStringStd(String)
860
     */
861
    public final static String unquoteStringStd(String s) {
142 ilm 862
        if (!quotedPatrn.matcher(s).matches())
67 ilm 863
            throw new IllegalArgumentException("Invalid quoted string " + s);
864
        return twoSingleQuote.matcher(s.substring(1, s.length() - 1)).replaceAll("'");
865
    }
866
 
17 ilm 867
    // * quoteIdentifier
868
 
869
    static private final Pattern doubleQuote = Pattern.compile("\"");
870
 
871
    /**
872
     * Quote a sql identifier to prevent it from being folded and allow any character.
873
     *
874
     * @param identifier a SQL identifier, eg 'My"Table'.
875
     * @return the quoted form, eg '"My""Table"'.
876
     */
877
    public static final String quoteIdentifier(String identifier) {
878
        return '"' + doubleQuote.matcher(identifier).replaceAll("\"\"") + '"';
879
    }
880
}