OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 142 | 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.Log;
17
import org.openconcerto.sql.State;
18
import org.openconcerto.sql.request.SQLCache;
174 ilm 19
import org.openconcerto.sql.utils.SQLUtils;
65 ilm 20
import org.openconcerto.utils.CompareUtils;
17 ilm 21
import org.openconcerto.utils.ExceptionHandler;
22
import org.openconcerto.utils.ExceptionUtils;
23
import org.openconcerto.utils.RTInterruptedException;
24
import org.openconcerto.utils.ThreadFactory;
174 ilm 25
import org.openconcerto.utils.Tuple2;
17 ilm 26
import org.openconcerto.utils.cache.CacheResult;
132 ilm 27
import org.openconcerto.utils.cache.ICacheSupport;
17 ilm 28
 
80 ilm 29
import java.beans.PropertyChangeEvent;
30
import java.beans.PropertyChangeListener;
17 ilm 31
import java.sql.Connection;
32
import java.sql.ResultSet;
33
import java.sql.ResultSetMetaData;
34
import java.sql.SQLException;
142 ilm 35
import java.sql.SQLNonTransientException;
36
import java.sql.SQLTransientException;
73 ilm 37
import java.sql.Savepoint;
17 ilm 38
import java.sql.Statement;
39
import java.util.Arrays;
40
import java.util.Collections;
41
import java.util.HashMap;
42
import java.util.HashSet;
43
import java.util.Hashtable;
44
import java.util.List;
45
import java.util.Map;
46
import java.util.NoSuchElementException;
47
import java.util.Set;
48
import java.util.WeakHashMap;
49
import java.util.concurrent.ExecutorService;
50
import java.util.concurrent.LinkedBlockingQueue;
51
import java.util.concurrent.ThreadPoolExecutor;
52
import java.util.concurrent.TimeUnit;
174 ilm 53
import java.util.concurrent.atomic.AtomicLong;
54
import java.util.concurrent.atomic.AtomicReference;
17 ilm 55
import java.util.logging.Level;
56
 
73 ilm 57
import org.apache.commons.dbcp.AbandonedConfig;
17 ilm 58
import org.apache.commons.dbcp.BasicDataSource;
73 ilm 59
import org.apache.commons.dbcp.ConnectionFactory;
60
import org.apache.commons.dbcp.PoolableConnection;
61
import org.apache.commons.dbcp.PoolableConnectionFactory;
62
import org.apache.commons.dbcp.PoolingConnection;
17 ilm 63
import org.apache.commons.dbcp.PoolingDataSource;
64
import org.apache.commons.dbcp.SQLNestedException;
65
import org.apache.commons.dbutils.BasicRowProcessor;
66
import org.apache.commons.dbutils.ResultSetHandler;
67
import org.apache.commons.dbutils.RowProcessor;
68
import org.apache.commons.dbutils.handlers.ArrayHandler;
69
import org.apache.commons.dbutils.handlers.ArrayListHandler;
70
import org.apache.commons.dbutils.handlers.MapHandler;
71
import org.apache.commons.dbutils.handlers.MapListHandler;
72
import org.apache.commons.dbutils.handlers.ScalarHandler;
73 ilm 73
import org.apache.commons.pool.KeyedObjectPool;
74
import org.apache.commons.pool.KeyedObjectPoolFactory;
75
import org.apache.commons.pool.ObjectPool;
17 ilm 76
import org.apache.commons.pool.impl.GenericObjectPool;
142 ilm 77
import org.h2.constant.ErrorCode;
17 ilm 78
import org.postgresql.util.GT;
79
import org.postgresql.util.PSQLException;
80
 
132 ilm 81
import net.jcip.annotations.GuardedBy;
82
import net.jcip.annotations.ThreadSafe;
83
 
17 ilm 84
/**
85
 * Une source de donnée SQL.
86
 *
87
 * @author ILM Informatique 10 juin 2004
88
 */
61 ilm 89
@ThreadSafe
17 ilm 90
public final class SQLDataSource extends BasicDataSource implements Cloneable {
91
 
92
    // MAYBE add a cache, but ATTN synchronized : one connection per thread, but only one shared DS
93
 
94
    /** A map of supported database systems and associated drivers. */
95
    static public final Map<SQLSystem, String> DRIVERS;
96
    static {
97
        DRIVERS = new HashMap<SQLSystem, String>();
98
        DRIVERS.put(SQLSystem.MYSQL, "com.mysql.jdbc.Driver");
99
        DRIVERS.put(SQLSystem.POSTGRESQL, "org.postgresql.Driver");
100
        DRIVERS.put(SQLSystem.DERBY, "org.apache.derby.jdbc.ClientDriver");
101
        DRIVERS.put(SQLSystem.H2, "org.h2.Driver");
102
        DRIVERS.put(SQLSystem.MSSQL, "com.microsoft.sqlserver.jdbc.SQLServerDriver");
103
    }
104
 
105
    // timeouts in seconds
106
    static public final int loginTimeOut = 15;
107
    static public final int socketTimeOut = 8 * 60;
108
 
80 ilm 109
    // in milliseconds
110
    static public int QUERY_TUNING = 0;
111
 
17 ilm 112
    static public interface IgnoringRowProcessor extends RowProcessor {
113
 
114
        @Override
115
        public Map<String, Object> toMap(ResultSet rs) throws SQLException;
116
 
117
        /**
118
         * Convert the passed result set to a map, ignoring some columns.
119
         *
120
         * @param rs the result set.
121
         * @param toIgnore which columns' label to ignore.
122
         * @return a map with all columns of <code>rs</code> except <code>toIgnore</code>.
123
         * @throws SQLException if an error occurs while reading <code>rs</code>.
124
         */
125
        public Map<String, Object> toMap(ResultSet rs, Set<String> toIgnore) throws SQLException;
126
    }
127
 
128
    // ignoring case-sensitive processor
129
    static private class IgnoringCSRowProcessor extends BasicRowProcessor implements IgnoringRowProcessor {
130
        @Override
131
        public Map<String, Object> toMap(ResultSet rs) throws SQLException {
132
            return toMap(rs, Collections.<String> emptySet());
133
        }
134
 
135
        // on ne veut pas de CaseInsensitiveMap
136
        @Override
137
        public Map<String, Object> toMap(ResultSet rs, Set<String> toIgnore) throws SQLException {
138
            final Map<String, Object> result = new HashMap<String, Object>();
139
            final ResultSetMetaData rsmd = rs.getMetaData();
140
            final int cols = rsmd.getColumnCount();
141
            for (int i = 1; i <= cols; i++) {
142
                // ATTN use label not base name (eg "des" in "SELECT DESIGNATION as des FROM ...")
143
                final String label = rsmd.getColumnLabel(i);
144
                if (!toIgnore.contains(label))
145
                    result.put(label, rs.getObject(i));
146
            }
147
            return result;
148
        }
149
    }
150
 
151
    static public final IgnoringRowProcessor ROW_PROC = new IgnoringCSRowProcessor();
152
    // all thread safe
153
    public static final ColumnListHandler COLUMN_LIST_HANDLER = new ColumnListHandler();
154
    public static final ArrayListHandler ARRAY_LIST_HANDLER = new ArrayListHandler();
142 ilm 155
    public static final ListListHandlerGeneric<Object> LIST_LIST_HANDLER = ListListHandlerGeneric.create(Object.class, null);
17 ilm 156
    public static final ArrayHandler ARRAY_HANDLER = new ArrayHandler();
157
    public static final ScalarHandler SCALAR_HANDLER = new ScalarHandler();
158
    public static final MapListHandler MAP_LIST_HANDLER = new MapListHandler(ROW_PROC);
159
    public static final MapHandler MAP_HANDLER = new MapHandler(ROW_PROC);
160
 
61 ilm 161
    // Cache, linked to cacheEnable and tables
162
    @GuardedBy("this")
163
    private SQLCache<List<?>, Object> cache;
164
    @GuardedBy("this")
17 ilm 165
    private boolean cacheEnabled;
80 ilm 166
    private final PropertyChangeListener descL;
17 ilm 167
    // tables that can be used in queries (and thus can impact the cache)
61 ilm 168
    @GuardedBy("this")
169
    private Set<SQLTable> tables;
17 ilm 170
 
171
    private static int count = 0; // compteur de requetes
172
 
80 ilm 173
    private final DBSystemRoot sysRoot;
93 ilm 174
    private ConnectionFactory connectionFactory;
61 ilm 175
    // no need to synchronize multiple call to this attribute since we only access the
176
    // Thread.currentThread() key
177
    @GuardedBy("handlers")
17 ilm 178
    private final Map<Thread, HandlersStack> handlers;
179
 
61 ilm 180
    @GuardedBy("this")
17 ilm 181
    private ExecutorService exec = null;
182
 
65 ilm 183
    private final Object setInitialShemaLock = new String("initialShemaWriteLock");
61 ilm 184
    // linked to initialSchema and uptodate
185
    @GuardedBy("this")
17 ilm 186
    private boolean initialShemaSet;
61 ilm 187
    @GuardedBy("this")
17 ilm 188
    private String initialShema;
61 ilm 189
    // which Connection have the right default schema
190
    @GuardedBy("this")
73 ilm 191
    private final Map<Connection, Object> schemaUptodate;
192
    // which Connection aren't invalidated
193
    @GuardedBy("this")
17 ilm 194
    private final Map<Connection, Object> uptodate;
195
 
61 ilm 196
    private volatile int retryWait;
63 ilm 197
    @GuardedBy("this")
198
    private boolean blockWhenExhausted;
67 ilm 199
    @GuardedBy("this")
200
    private long softMinEvictableIdleTimeMillis;
61 ilm 201
 
73 ilm 202
    @GuardedBy("this")
203
    private int txIsolation;
204
    @GuardedBy("this")
205
    private Integer dbTxIsolation;
206
    @GuardedBy("this")
207
    private boolean checkOnceDBTxIsolation;
208
 
142 ilm 209
    private final Object testLock = new String("testLock");
17 ilm 210
 
80 ilm 211
    public SQLDataSource(DBSystemRoot sysRoot, String base, String login, String pass) {
212
        this(sysRoot, sysRoot.getServer().getURL(base), login, pass, Collections.<SQLTable> emptySet());
17 ilm 213
    }
214
 
80 ilm 215
    private SQLDataSource(DBSystemRoot sysRoot, String url, String login, String pass, Set<SQLTable> tables) {
216
        this(sysRoot);
17 ilm 217
 
80 ilm 218
        final SQLSystem system = getSystem();
17 ilm 219
        if (!DRIVERS.containsKey(system))
220
            throw new IllegalArgumentException("unknown database system: " + system);
221
 
222
        this.setDriverClassName(DRIVERS.get(system));
223
        this.setUrl("jdbc:" + system.getJDBCName() + ":" + url);
224
 
225
        this.setUsername(login);
226
        this.setPassword(pass);
227
        this.setTables(tables);
228
 
80 ilm 229
        if (system == SQLSystem.MYSQL) {
17 ilm 230
            this.addConnectionProperty("transformedBitIsBoolean", "true");
73 ilm 231
            // by default allowMultiQueries, since it's more convenient (i.e. pass String around
232
            // instead of List<String>) and faster (less trips to the server, allow
233
            // SQLUtils.executeMultiple())
234
            this.addConnectionProperty("allowMultiQueries", "true");
83 ilm 235
        } else if (system == SQLSystem.MSSQL) {
236
            // Otherwise we get SQLState S0002 instead of 42S02 (needed at least in
237
            // SQLBase.getFwkMetadata())
238
            // http://msdn.microsoft.com/fr-fr/library/ms712451.aspx
239
            // http://technet.microsoft.com/en-us/library/ms378988(v=sql.105).aspx
240
            this.addConnectionProperty("xopenStates", "true");
241
            // see
242
            // https://connect.microsoft.com/SQLServer/feedback/details/295907/resultsetmetadata-gettablename-returns-null-or-inconsistent-results
243
            // http://social.msdn.microsoft.com/Forums/sqlserver/en-US/55e8cbb2-b11c-446e-93ab-dc30658caf99/resultsetmetadatagettablename-returns-instead-of-table-name?forum=sqldataaccess
244
            // 1. The statement that the resultset belongs to was created with
245
            // TYPE_SCROLL_INSENSITIVE or TYPE_SCROLL_SENSITIVE : The full table or view name will
246
            // be returned
247
            // 2. The statement that the resultset belongs to was created without specifying the
248
            // cursor type, or the cursor type is TYPE_FORWARD_ONLY : The full table or view name
249
            // will be returned if the column is a text, ntext, or image, else empty string.
250
            this.addConnectionProperty("selectMethod", "cursor");
17 ilm 251
        }
252
        this.setLoginTimeout(loginTimeOut);
253
        this.setSocketTimeout(socketTimeOut);
93 ilm 254
        this.setTCPKeepAlive(true);
142 ilm 255
        this.setRetryWait(7000);
17 ilm 256
        // ATTN DO NOT call execute() or any method that might create a connection
257
        // since at this point dsInit() has not been called and thus connection properties might be
258
        // missing (eg allowMultiQueries). And the faulty connection will stay in the pool.
259
    }
260
 
261
    @Override
262
    public final void setLoginTimeout(int timeout) {
80 ilm 263
        if (this.getSystem() == SQLSystem.MYSQL) {
17 ilm 264
            this.addConnectionProperty("connectTimeout", timeout + "000");
83 ilm 265
        } else if (this.getSystem() == SQLSystem.POSTGRESQL || this.getSystem() == SQLSystem.MSSQL) {
17 ilm 266
            this.addConnectionProperty("loginTimeout", timeout + "");
93 ilm 267
        } else {
268
            Log.get().warning("Ignoring login timeout for " + this);
17 ilm 269
        }
270
    }
271
 
272
    public final void setSocketTimeout(int timeout) {
80 ilm 273
        if (this.getSystem() == SQLSystem.MYSQL) {
17 ilm 274
            this.addConnectionProperty("socketTimeout", timeout + "000");
80 ilm 275
        } else if (this.getSystem() == SQLSystem.H2) {
142 ilm 276
            // org.h2.util.NetUtils.createSocket() doesn't use setSoTimeout(), so this is the next
277
            // best thing. But it isn't checked everywhere, see DbSettings.maxQueryTimeout
278
            this.addConnectionProperty("MAX_QUERY_TIMEOUT", timeout + "000");
80 ilm 279
        } else if (this.getSystem() == SQLSystem.POSTGRESQL) {
17 ilm 280
            this.addConnectionProperty("socketTimeout", timeout + "");
93 ilm 281
        } else {
282
            Log.get().log(getLogLevelForIgnoredTCPParam(), "Ignoring socket timeout for " + this);
17 ilm 283
        }
284
    }
285
 
93 ilm 286
    // if TCP isn't used or is used to connect to localhost, TCP should be quite robust
287
    private final Level getLogLevelForIgnoredTCPParam() {
288
        return SQLSyntax.get(this.sysRoot).isServerLocalhost(this.sysRoot.getServer()) ? Level.CONFIG : Level.WARNING;
289
    }
290
 
291
    public final void setTCPKeepAlive(final boolean b) {
292
        if (this.getSystem() == SQLSystem.POSTGRESQL || this.getSystem() == SQLSystem.MYSQL) {
293
            this.addConnectionProperty("tcpKeepAlive", String.valueOf(b));
294
        } else {
295
            Log.get().log(getLogLevelForIgnoredTCPParam(), "Ignoring TCP keep alive for " + this);
296
        }
297
    }
298
 
299
    /**
300
     * Set the number of milliseconds to wait before retrying if a connection fails to establish or
301
     * if a query fails to execute.
302
     *
303
     * @param retryWait the number of milliseconds to wait, negative to disable retrying.
304
     */
17 ilm 305
    public final void setRetryWait(int retryWait) {
306
        this.retryWait = retryWait;
307
    }
308
 
61 ilm 309
    synchronized void setTables(Set<SQLTable> tables) {
17 ilm 310
        // don't change the cache if we're only adding tables
311
        final boolean update = this.cache == null || !tables.containsAll(this.tables);
61 ilm 312
        this.tables = Collections.unmodifiableSet(new HashSet<SQLTable>(tables));
17 ilm 313
        if (update)
314
            updateCache();
315
    }
316
 
142 ilm 317
    private synchronized Set<SQLTable> getTables() {
318
        return this.tables;
319
    }
320
 
61 ilm 321
    private synchronized void updateCache() {
17 ilm 322
        if (this.cache != null)
132 ilm 323
            this.cache.getSupp().die();
324
        this.cache = createCache(null);
73 ilm 325
        for (final HandlersStack s : this.handlers.values()) {
326
            s.updateCache();
327
        }
328
    }
329
 
132 ilm 330
    // the cache for committed data
331
    synchronized final SQLCache<List<?>, Object> getCommittedCache() {
332
        return this.cache;
333
    }
334
 
335
    final SQLCache<List<?>, Object> getCache() {
336
        // transactions are isolated from one another, so their caches should be too
337
        final HandlersStack stack = getHandlersStack();
338
        if (stack != null && stack.hasTransaction())
339
            return stack.getCache();
340
        else
341
            return this.getCommittedCache();
342
    }
343
 
344
    synchronized SQLCache<List<?>, Object> createCache(final TransactionPoint o) {
73 ilm 345
        final SQLCache<List<?>, Object> res;
132 ilm 346
        if (this.isCacheEnabled() && this.tables.size() > 0) {
73 ilm 347
            // the general cache should wait for transactions to end, but the cache of transactions
348
            // must not.
132 ilm 349
            final boolean committedCache = o == null;
350
            final Object desc = committedCache ? this : o;
351
            final ICacheSupport<SQLData> cacheSupp = committedCache ? null : this.cache.getSupp();
352
            res = new SQLCache<List<?>, Object>(cacheSupp, 30, 30, "results of " + desc.toString(), o) {
353
                @Override
354
                protected String getCacheSuppName(String cacheName) {
355
                    assert committedCache : "Creating extra ICacheSupport";
356
                    return SQLDataSource.this.toString();
357
                }
358
            };
359
        } else {
73 ilm 360
            res = null;
132 ilm 361
        }
73 ilm 362
        return res;
17 ilm 363
    }
364
 
365
    /**
366
     * Enable or disable the cache. ATTN if you enable the cache you must
367
     * {@link SQLTable#fire(SQLTableEvent) fire} table events, or use a class that does like
368
     * {@link SQLRowValues}.
369
     *
370
     * @param b <code>true</code> to enable the cache.
371
     */
61 ilm 372
    public final synchronized void setCacheEnabled(boolean b) {
17 ilm 373
        if (this.cacheEnabled != b) {
374
            this.cacheEnabled = b;
375
            updateCache();
376
        }
377
    }
378
 
73 ilm 379
    public final synchronized boolean isCacheEnabled() {
380
        return this.cacheEnabled;
381
    }
382
 
17 ilm 383
    /* pour le clonage */
80 ilm 384
    private SQLDataSource(DBSystemRoot sysRoot) {
385
        this.sysRoot = sysRoot;
17 ilm 386
        // on a besoin d'une implementation synchronisée
61 ilm 387
        this.handlers = new Hashtable<Thread, HandlersStack>();
17 ilm 388
        // weak, since this is only a hint to avoid initializing the connection
389
        // on each borrowal
73 ilm 390
        this.schemaUptodate = new WeakHashMap<Connection, Object>();
17 ilm 391
        this.uptodate = new WeakHashMap<Connection, Object>();
392
        this.initialShemaSet = false;
393
        this.initialShema = null;
394
 
93 ilm 395
        // used by #getNewConnection() when there's an exception and by #validateDBConnectivity()
17 ilm 396
        this.setValidationQuery("SELECT 1");
93 ilm 397
        // We must set a socket timeout high enough for large queries but the validation query
398
        // should return almost instantly. Not too low since overloaded links may have very high
399
        // latencies.
400
        this.setValidationQueryTimeout(6);
401
        // don't test on borrow, this would double the queries.
17 ilm 402
        this.setTestOnBorrow(false);
93 ilm 403
        // don't test on return, the connection was just used and this would double the queries.
404
        this.setTestOnReturn(false);
405
        // for now don't test on idle as our evictor is run quite often to promptly close unneeded
406
        // connections. If it is slowed down, we risk overloading the server or just hit its maximum
407
        // connection count. MAYBE enable when we upgrade to DBCP 2, since it depends on POOL 2
408
        // which has an EvictionPolicy, so for example we could test connections after a smaller
409
        // amount of time than minEvictableIdleTimeMillis.
410
        this.setTestWhileIdle(false);
17 ilm 411
 
412
        this.setInitialSize(3);
93 ilm 413
        // neither DOS the server...
414
        this.setBlockWhenExhausted(true);
415
        this.setMaxActive(12);
416
        // ... nor us
417
        this.setMaxWait(5000);
17 ilm 418
        // creating connections is quite costly so make sure we always have a couple free
419
        this.setMinIdle(2);
420
        // but not too much as it can lock out other users (the server has a max connection count)
93 ilm 421
        this.setMaxIdle(10);
67 ilm 422
        // check 5 connections every 4 seconds
423
        this.setTimeBetweenEvictionRunsMillis(4000);
424
        this.setNumTestsPerEvictionRun(5);
425
        // kill extra (above minIdle) connections after 40s
426
        this.setSoftMinEvictableIdleTimeMillis(TimeUnit.SECONDS.toMillis(40));
427
        // kill idle connections after 30 minutes (even if it means re-creating some new ones
93 ilm 428
        // immediately afterwards to ensure minIdle connections). For now it's a poor man's solution
429
        // for lacking testWhileIdle. After that time NAT tables expires, the path to the server
430
        // might be broken...
67 ilm 431
        this.setMinEvictableIdleTimeMillis(TimeUnit.MINUTES.toMillis(30));
432
 
73 ilm 433
        // the default of many systems
434
        this.txIsolation = Connection.TRANSACTION_READ_COMMITTED;
435
        // by definition unknown without a connection
436
        this.dbTxIsolation = null;
437
        // it's rare that DB configuration changes, and it's expensive to add a trip to the server
438
        // for each new connection
439
        this.checkOnceDBTxIsolation = true;
440
 
17 ilm 441
        // see #createDataSource() for properties not supported by this class
61 ilm 442
        this.tables = Collections.emptySet();
80 ilm 443
        this.descL = new PropertyChangeListener() {
444
            @Override
445
            public void propertyChange(PropertyChangeEvent evt) {
446
                if (evt.getPropertyName().equals("descendants")) {
447
                    // the dataSource must always have all tables, to listen to them for its cache
448
                    setTables(((DBSystemRoot) evt.getSource()).getDescs(SQLTable.class));
449
                }
450
            }
451
        };
452
        this.sysRoot.addListener(this.descL);
17 ilm 453
        this.cache = null;
454
        this.cacheEnabled = false;
455
    }
456
 
457
    /**
458
     * Exécute la requête et retourne le résultat sous forme de liste de map. Si la requete va
459
     * retourner beaucoup de lignes, il est peut-être préférable d'utiliser un ResultSetHandler.
460
     *
461
     * @param query le requête à exécuter.
462
     * @return le résultat de la requête.
463
     * @see MapListHandler
464
     * @see #execute(String, ResultSetHandler)
465
     */
466
    public List execute(String query) {
467
        return (List) this.execute(query, MAP_LIST_HANDLER);
468
    }
469
 
470
    /**
471
     * Exécute la requête et retourne la première colonne uniquement.
472
     *
473
     * @param query le requête à exécuter.
474
     * @return le résultat de la requête.
475
     * @see ColumnListHandler
476
     * @see #execute(String, ResultSetHandler)
477
     */
478
    public List executeCol(String query) {
479
        return (List) this.execute(query, COLUMN_LIST_HANDLER);
480
    }
481
 
482
    /**
483
     * Exécute la requête et retourne le résultat sous forme de liste de tableau. Si la requete va
484
     * retourner beaucoup de lignes, il est peut-être préférable d'utiliser un ResultSetHandler.
485
     *
486
     * @param query le requête à exécuter.
487
     * @return le résultat de la requête.
488
     * @see ArrayListHandler
489
     * @see #execute(String, ResultSetHandler)
490
     */
491
    public List executeA(String query) {
492
        return (List) this.execute(query, ARRAY_LIST_HANDLER);
493
    }
494
 
495
    /**
496
     * Exécute la requête et retourne la première ligne du résultat sous forme de map.
497
     *
498
     * @param query le requête à exécuter.
499
     * @return le résultat de la requête.
500
     * @see MapHandler
501
     * @see #execute(String)
502
     */
142 ilm 503
    public Map<String, Object> execute1(String query) {
504
        return (Map<String, Object>) this.execute(query, MAP_HANDLER);
17 ilm 505
    }
506
 
507
    /**
508
     * Exécute la requête et retourne la première ligne du résultat sous forme de tableau.
509
     *
510
     * @param query le requête à exécuter.
511
     * @return le résultat de la requête.
512
     * @see ArrayHandler
513
     * @see #executeA(String)
514
     */
515
    public Object[] executeA1(String query) {
516
        return (Object[]) this.execute(query, ARRAY_HANDLER);
517
    }
518
 
519
    /**
520
     * Exécute la requête et retourne la valeur de la premiere colonne de la premiere ligne.
521
     *
522
     * @param query le requête à exécuter.
523
     * @return le résultat de la requête.
524
     */
525
    public Object executeScalar(String query) {
526
        return this.execute(query, SCALAR_HANDLER);
527
    }
528
 
529
    /**
530
     * Exécute la requête et passe le résultat au ResultSetHandler.
531
     *
532
     * @param query le requête à exécuter.
533
     * @param rsh le handler à utiliser, ou <code>null</code>.
534
     * @return le résultat du handler, <code>null</code> si rsh est <code>null</code>.
535
     * @see #execute(String)
536
     */
537
    public Object execute(String query, ResultSetHandler rsh) {
538
        return this.execute(query, rsh, null);
539
    }
540
 
541
    /**
542
     * Execute <code>query</code> within <code>c</code>, passing the result set to <code>rsh</code>.
543
     *
544
     * @param query the query to perform.
545
     * @param rsh what to do with the result, can be <code>null</code>.
546
     * @param changeState whether <code>query</code> changes the state of a connection.
547
     * @return the result of <code>rsh</code>, <code>null</code> if rsh or the resultSet is
548
     *         <code>null</code>.
549
     * @throws RTInterruptedException if the current thread is interrupted while waiting for the
550
     *         cache or for the database.
551
     */
552
    public final Object execute(final String query, final ResultSetHandler rsh, final boolean changeState) throws RTInterruptedException {
553
        return this.execute(query, rsh, changeState, null);
554
    }
555
 
556
    private Object execute(final String query, final ResultSetHandler rsh, final Connection c) throws RTInterruptedException {
557
        // false since the vast majority of request do NOT change the state
558
        return this.execute(query, rsh, false, c);
559
    }
560
 
132 ilm 561
    final List<Object> getCacheKey(final String query, final ResultSetHandler rsh) {
562
        return query.startsWith("SELECT") ? Arrays.asList(new Object[] { query, rsh }) : null;
563
    }
564
 
17 ilm 565
    /**
566
     * Execute <code>query</code> within <code>c</code>, passing the result set to <code>rsh</code>.
567
     *
568
     * @param query the query to perform.
569
     * @param rsh what to do with the result, can be <code>null</code>.
570
     * @param changeState whether <code>query</code> changes the state of a connection.
571
     * @param passedConn the sql connection to use.
572
     * @return the result of <code>rsh</code>, <code>null</code> if rsh or the resultSet is
573
     *         <code>null</code>.
574
     * @throws RTInterruptedException if the current thread is interrupted while waiting for the
575
     *         cache or for the database.
576
     */
577
    private Object execute(final String query, final ResultSetHandler rsh, final boolean changeState, final Connection passedConn) throws RTInterruptedException {
578
        final long timeMs = System.currentTimeMillis();
579
        final long time = System.nanoTime();
580
        // some systems refuse to execute nothing
581
        if (query.length() == 0) {
65 ilm 582
            SQLRequestLog.log(query, "Pas de requête.", timeMs, time);
17 ilm 583
            return null;
584
        }
585
 
586
        final IResultSetHandler irsh = rsh instanceof IResultSetHandler ? (IResultSetHandler) rsh : null;
132 ilm 587
        final boolean readCache = irsh == null || irsh.readCache();
588
        final boolean canWriteCache = irsh == null || irsh.canWriteCache();
589
        final SQLCache<List<?>, Object> cache = !readCache && !canWriteCache ? null : this.getCache();
590
        final List<Object> key = cache == null ? null : getCacheKey(query, rsh);
591
        final CacheResult<Object> cacheRes;
592
        if (key != null) {
142 ilm 593
            final Set<? extends SQLData> data = irsh == null || irsh.getCacheModifiers() == null ? this.getTables() : irsh.getCacheModifiers();
132 ilm 594
            cacheRes = cache.check(key, readCache, canWriteCache, data);
595
            if (cacheRes.getState() == CacheResult.State.INTERRUPTED)
17 ilm 596
                throw new RTInterruptedException("interrupted while waiting for the cache");
132 ilm 597
            else if (cacheRes.getState() == CacheResult.State.VALID) {
17 ilm 598
                // cache actif
599
                if (State.DEBUG)
600
                    State.INSTANCE.addCacheHit();
65 ilm 601
                SQLRequestLog.log(query, "En cache.", timeMs, time);
132 ilm 602
                return cacheRes.getRes();
17 ilm 603
            }
132 ilm 604
        } else {
605
            cacheRes = null;
17 ilm 606
        }
607
 
608
        Object result = null;
609
        QueryInfo info = null;
65 ilm 610
        final long afterCache = System.nanoTime();
611
        final long afterQueryInfo, afterExecute, afterHandle;
142 ilm 612
        int count = 0;
17 ilm 613
        try {
614
            info = new QueryInfo(query, changeState, passedConn);
615
            try {
65 ilm 616
                afterQueryInfo = System.nanoTime();
17 ilm 617
                final Object[] res = this.executeTwice(info);
618
                final Statement stmt = (Statement) res[0];
619
                ResultSet rs = (ResultSet) res[1];
620
                // TODO 1. rename #execute(String) to #executeN(String)
621
                // and make #execute(String) do #execute(String, null)
622
                // 2. let null rs pass to rsh
623
                // otherwise you write ds.execute("req", new ResultSetHandler() {
624
                // public Object handle(ResultSet rs) throws SQLException {
625
                // return "OK";
626
                // }
627
                // });
628
                // and OK won't be returned if "req" returns a null rs.
65 ilm 629
                afterExecute = System.nanoTime();
17 ilm 630
                if (rsh != null && rs != null) {
631
                    if (this.getSystem() == SQLSystem.DERBY || this.getSystem() == SQLSystem.POSTGRESQL) {
632
                        rs = new SQLResultSet(rs);
633
                    }
634
                    result = rsh.handle(rs);
142 ilm 635
                    count = SQLResultSet.getRowProcessedCount(rs);
17 ilm 636
                }
142 ilm 637
 
65 ilm 638
                afterHandle = System.nanoTime();
17 ilm 639
 
640
                stmt.close();
641
                // if key was added to the cache
642
                if (key != null) {
61 ilm 643
                    synchronized (this) {
132 ilm 644
                        putInCache(cache, irsh, cacheRes, result);
61 ilm 645
                    }
17 ilm 646
                }
647
                info.releaseConnection();
648
            } catch (SQLException exn) {
649
                // don't usually do a getSchema() as it access the db
650
                throw new IllegalStateException("Impossible d'accéder au résultat de " + query + "\n in " + this, exn);
651
            }
652
        } catch (RuntimeException e) {
653
            // for each #check() there must be a #removeRunning()
654
            // let the cache know we ain't gonna tell it the result
132 ilm 655
            if (cacheRes != null)
656
                cache.removeRunning(cacheRes);
17 ilm 657
            if (info != null)
658
                info.releaseConnection(e);
659
            throw e;
660
        }
661
 
142 ilm 662
        SQLRequestLog.log(query, "", info.getConnection(), timeMs, time, afterCache, afterQueryInfo, afterExecute, afterHandle, System.nanoTime(), count);
17 ilm 663
 
664
        return result;
665
    }
666
 
132 ilm 667
    private synchronized void putInCache(final SQLCache<List<?>, Object> cache, final IResultSetHandler irsh, final CacheResult<Object> cacheRes, Object result) {
668
        if (irsh != null && irsh.writeCache() || irsh == null && IResultSetHandler.shouldCache(result)) {
669
            cache.put(cacheRes, result);
670
        } else {
671
            cache.removeRunning(cacheRes);
61 ilm 672
        }
673
    }
674
 
17 ilm 675
    private synchronized final ExecutorService getExec() {
676
        if (this.exec == null) {
677
            // not daemon since we want the connections to be returned
678
            final ThreadFactory factory = new ThreadFactory(SQLDataSource.class.getSimpleName() + " " + this.toString() + " exec n° ", false);
679
            // a rather larger number of threads since all they do is wait severals seconds
680
            this.exec = new ThreadPoolExecutor(0, 32, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), factory);
681
        }
682
        return this.exec;
683
    }
684
 
67 ilm 685
    private final class QueryInfo {
17 ilm 686
        private final String query;
687
        // whether query change the state of our connection
688
        private final boolean changeState;
689
        // can change if private
690
        private Connection c;
691
        // whether we acquired a new connection (and thus can do whatever we want with it)
692
        private final boolean privateConnection;
693
 
694
        QueryInfo(String query, boolean changeState, final Connection passedConn) {
695
            super();
696
            this.query = query;
697
            this.changeState = changeState;
698
 
699
            // if passedConn is provided use it, else we need to find one
700
            boolean acquiredConnection = false;
701
            final Connection foundConn;
702
            if (passedConn != null)
703
                foundConn = passedConn;
704
            else if (!handlingConnection()) {
705
                foundConn = getNewConnection();
706
                acquiredConnection = true;
707
            } else {
708
                final HandlersStack threadHandlers = getHandlersStack();
709
                if (!changeState || threadHandlers.isChangeAllowed()) {
710
                    foundConn = threadHandlers.getConnection();
711
                } else {
712
                    throw new IllegalStateException("the passed query change the connection's state and the current thread has a connection which will thus be changed."
713
                            + " A possible solution is to execute it in the setup() of a ConnectionHandler\n" + query);
714
                }
715
            }
716
 
717
            this.privateConnection = acquiredConnection;
718
            this.c = foundConn;
719
        }
720
 
721
        public final Connection getConnection() {
722
            return this.c;
723
        }
724
 
725
        public final String getQuery() {
726
            return this.query;
727
        }
728
 
729
        void releaseConnection(RuntimeException e) {
730
            // MySQL reste des fois bloqué dans SocketInputStream.socketRead0()
731
            // (le serveur ayant tué la query)
732
            if (e instanceof InterruptedQuery && getSystem() == SQLSystem.MYSQL) {
733
                final ExecutorThread thread = ((InterruptedQuery) e).getThread();
734
 
735
                if (this.privateConnection) {
736
                    if (this.changeState)
737
                        // no need to try to save the connection, it is no longer valid
738
                        this.releaseConnection();
739
                    else {
740
                        // test if the connection is still valid before returning it to the pool
741
                        getExec().execute(new Runnable() {
742
                            public void run() {
743
                                // on attend un peu
744
                                try {
745
                                    thread.join(1500);
746
                                    // pour voir si on meurt
747
                                    if (thread.isAlive()) {
748
                                        Log.get().warning(getFailedCancelMsg());
749
                                        closeConnection(getConnection());
750
                                    } else {
751
                                        // la connexion est ok, on la remet dans le pool
752
                                        returnConnection(getConnection());
753
                                    }
754
                                } catch (InterruptedException e) {
755
                                    // the datasource is closing
756
                                    Log.get().fine("Interrupted while joining " + getQuery());
757
                                    closeConnection(getConnection());
758
                                }
759
                            }
760
                        });
761
                    }
762
                } else {
763
                    // try to save the connection since it is used by others
764
                    try {
765
                        // clear the interrupt status set by InterruptedQuery
766
                        // so that we can wait on thread
767
                        Thread.interrupted();
768
                        thread.join(500);
769
                    } catch (InterruptedException e2) {
770
                        System.err.println("ignore, we are already interrupted");
771
                        e2.printStackTrace();
772
                    }
773
                    // remettre le flag pour les méthodes appelantes.
774
                    Thread.currentThread().interrupt();
775
 
776
                    // connection is still stuck
777
                    if (thread.isAlive()) {
778
                        throw new IllegalStateException(getFailedCancelMsg(), e);
779
                    } else
780
                        this.releaseConnection();
781
                }
782
            } else
783
                this.releaseConnection();
784
        }
785
 
786
        void releaseConnection() {
787
            // have we borrowed a connection, otherwise it is not our responsibility to release it.
788
            if (this.privateConnection) {
789
                if (this.changeState)
790
                    // the connection is no longer in a pristine state so close it
791
                    closeConnection(this.getConnection());
792
                else
793
                    // otherwise we can reuse it
794
                    returnConnection(this.getConnection());
795
            }
796
        }
797
 
798
        private final String getFailedCancelMsg() {
799
            return "cancel of " + System.identityHashCode(getConnection()) + " failed for " + getQuery();
800
        }
801
 
142 ilm 802
        final boolean canObtainNewConnection() {
803
            return this.privateConnection;
804
        }
805
 
17 ilm 806
        // an error has occured, try within another connection if possible
67 ilm 807
        final Connection obtainNewConnection() {
142 ilm 808
            if (!this.canObtainNewConnection()) {
17 ilm 809
                return null;
142 ilm 810
            } else {
17 ilm 811
                // ATTN should be sure that our connection was not already closed,
812
                // see #closeConnection()
813
                closeConnection(this.getConnection());
814
                this.c = borrowConnection(true);
815
                return this.getConnection();
816
            }
817
        }
65 ilm 818
 
819
        @Override
820
        public String toString() {
821
            return this.getClass().getSimpleName() + " private connection: " + this.privateConnection + " query: " + this.getQuery();
822
        }
17 ilm 823
    }
824
 
67 ilm 825
    /**
826
     * Whether the current thread has called {@link #useConnection(ConnectionHandler)}.
827
     *
828
     * @return <code>true</code> if within <code>useConnection()</code> and thus safe to call
829
     *         {@link #getConnection()}.
830
     */
831
    public final boolean handlingConnection() {
17 ilm 832
        return this.handlers.containsKey(Thread.currentThread());
833
    }
834
 
835
    private final HandlersStack getHandlersStack() {
836
        return this.handlers.get(Thread.currentThread());
837
    }
838
 
839
    /**
840
     * Use a single connection to execute <code>handler</code>.
841
     *
842
     * @param <T> type of return.
843
     * @param <X> type of exception.
844
     * @param handler what to do with the connection.
845
     * @return what <code>handler</code> returned.
846
     * @throws SQLException if an exception happens in setup() or restore().
847
     * @throws X if handle() throws an exception.
848
     * @see ConnectionHandler
849
     */
850
    public final <T, X extends Exception> T useConnection(ConnectionHandler<T, X> handler) throws SQLException, X {
93 ilm 851
        return this.useConnection(handler, false);
852
    }
853
 
854
    private final <T, X extends Exception> T useConnection(ConnectionHandler<T, X> handler, final boolean force) throws SQLException, X {
17 ilm 855
        final HandlersStack h;
93 ilm 856
        boolean connOutsidePool = false;
17 ilm 857
        if (!this.handlingConnection()) {
93 ilm 858
            Connection conn;
859
            try {
860
                conn = this.getNewConnection();
861
            } catch (NoSuchElementException e) {
862
                if (force) {
863
                    conn = this.connectionFactory.createConnection();
864
                    connOutsidePool = true;
865
                } else {
866
                    throw e;
867
                }
868
            }
869
            h = new HandlersStack(this, conn, handler);
132 ilm 870
            this.handlers.put(h.getThread(), h);
17 ilm 871
        } else if (handler.canRestoreState()) {
872
            h = this.getHandlersStack().push(handler);
873
        } else
874
            throw new IllegalStateException("this thread has already called useConnection() and thus expect its state, but the passed handler cannot restore state: " + handler);
875
 
876
        Connection conn = null;
132 ilm 877
        // before or after compute, RuntimeException or SQLException
878
        Exception beforeExn = null, afterExn = null;
879
        // X or SQLException
880
        Exception computeExn = null;
17 ilm 881
        try {
882
            conn = h.getConnection();
883
            h.setChangeAllowed(true);
884
            handler.setup(conn);
885
            h.setChangeAllowed(false);
132 ilm 886
            try {
887
                handler.compute(this);
888
            } catch (Exception e) {
889
                computeExn = e;
890
            }
17 ilm 891
        } catch (Exception e) {
892
            h.setChangeAllowed(false);
132 ilm 893
            beforeExn = e;
17 ilm 894
        }
895
 
896
        // in all cases (thanks to the above catch), try to restore the state
897
        // if conn is null setup() was never called
898
        boolean pristineState = conn == null;
132 ilm 899
        // don't bother trying to restore state if the connection has been invalidated (by a
900
        // recursive call)
901
        if (!pristineState && h.hasValidConnection() && handler.canRestoreState()) {
17 ilm 902
            h.setChangeAllowed(true);
903
            try {
904
                handler.restoreState(conn);
905
                pristineState = true;
906
            } catch (Exception e) {
132 ilm 907
                afterExn = e;
17 ilm 908
            }
909
            h.setChangeAllowed(false);
910
        }
911
 
912
        // ATTN conn can be null (return/closeConnection() accept it)
913
        if (h.pop()) {
914
            // remove if this thread has no handlers left
915
            this.handlers.remove(Thread.currentThread());
93 ilm 916
            if (connOutsidePool) {
917
                conn.close();
132 ilm 918
            } else if (pristineState) {
919
                this.returnConnection(h.getConnection());
920
            } else {
921
                this.closeConnection(h.invalidConnection());
922
            }
17 ilm 923
        } else {
93 ilm 924
            assert !connOutsidePool;
17 ilm 925
            // connection is still used
926
            if (!pristineState) {
132 ilm 927
                this.closeConnection(h.invalidConnection());
17 ilm 928
            }
929
            // else the top handler will release the connection
930
        }
132 ilm 931
        if (beforeExn != null) {
932
            assert computeExn == null : "Compute shouldn't be attempted if setup fails : " + beforeExn + " " + computeExn;
933
            if (afterExn != null) {
934
                throw new SQLException("could not restore state after failed setup : " + ExceptionUtils.getStackTrace(afterExn), beforeExn);
935
            } else {
936
                throw ExceptionUtils.throwExn(beforeExn, SQLException.class, RuntimeException.class);
937
            }
938
        } else if (afterExn != null) {
939
            if (computeExn != null) {
940
                throw new SQLException("could not restore state after successful setup : " + ExceptionUtils.getStackTrace(afterExn), computeExn);
941
            } else {
942
                throw ExceptionUtils.throwExn(afterExn, SQLException.class, RuntimeException.class);
943
            }
944
        } else {
17 ilm 945
            return handler.get();
132 ilm 946
        }
17 ilm 947
    }
948
 
949
    // this method create a Statement, don't forget to close it when you're done
950
    private Object[] executeTwice(QueryInfo queryInfo) throws SQLException {
951
        final String query = queryInfo.getQuery();
952
        Object[] res;
953
        try {
954
            res = executeOnce(query, queryInfo.getConnection());
955
        } catch (SQLException exn) {
956
            if (State.DEBUG)
957
                State.INSTANCE.addFailedRequest(query);
142 ilm 958
            // only retry for transient errors
959
            final boolean retry;
960
            if (exn instanceof SQLTransientException) {
961
                retry = true;
962
            } else if (exn instanceof SQLNonTransientException) {
963
                retry = false;
964
            } else if (getSystem() == SQLSystem.H2) {
965
                // 1. server was killed, maybe it will be restarted
966
                // 2. client network interface was brought down, maybe it will be brought up again
967
                retry = exn.getErrorCode() == ErrorCode.CONNECTION_BROKEN_1;
968
            } else if (getSystem() == SQLSystem.POSTGRESQL) {
969
                // Class 08 — Connection Exception (e.g. SocketException)
970
                // Class 57 — Operator Intervention (e.g. server shutdown)
971
                retry = exn.getSQLState().startsWith("08") || exn.getSQLState().startsWith("57");
972
            } else {
973
                retry = getSystem() == SQLSystem.MYSQL;
974
            }
17 ilm 975
            // maybe this was a network problem, so wait a little
93 ilm 976
            final int retryWait = this.retryWait;
142 ilm 977
            if (!retry || retryWait < 0 || !queryInfo.canObtainNewConnection())
93 ilm 978
                throw exn;
17 ilm 979
            try {
142 ilm 980
                Thread.sleep(retryWait);
17 ilm 981
            } catch (InterruptedException e) {
982
                throw new RTInterruptedException(e.getMessage() + " : " + query, exn);
983
            }
984
            // and try to obtain a new connection
985
            try {
986
                final Connection otherConn = queryInfo.obtainNewConnection();
142 ilm 987
                res = executeOnce(query, otherConn);
17 ilm 988
            } catch (Exception e) {
989
                if (e == exn)
990
                    throw exn;
991
                else
992
                    throw new SQLException("second exec failed: " + e.getLocalizedMessage(), exn);
993
            }
67 ilm 994
            // only log if the second succeeds (otherwise it's thrown)
995
            Log.get().log(Level.INFO, "executeOnce() failed for " + queryInfo, exn);
17 ilm 996
        }
997
        return res;
998
    }
999
 
174 ilm 1000
    private static final Tuple2<Long, int[]> NO_QUERIES_RES = Tuple2.create(0l, new int[0]);
1001
 
93 ilm 1002
    /**
174 ilm 1003
     * Execute multiple queries in batch.
1004
     *
1005
     * @param queries what to execute.
1006
     * @param atomic <code>true</code> if all queries should be executed in a transaction.
1007
     * @return the total update count (&lt; 0 if unknown), followed by the individual update counts.
1008
     * @throws SQLException if an error occurs.
1009
     * @see Statement#executeBatch()
1010
     */
1011
    public final Tuple2<Long, int[]> executeBatch(final List<String> queries, final boolean atomic) throws SQLException {
1012
        if (queries.isEmpty())
1013
            return NO_QUERIES_RES;
1014
        final long timeMs = System.currentTimeMillis();
1015
        final long time = System.nanoTime();
1016
        final long afterCache = time;
1017
        final AtomicLong afterQueryInfo = new AtomicLong();
1018
        final AtomicLong afterExecute = new AtomicLong();
1019
        final AtomicReference<Connection> conn = new AtomicReference<>();
1020
        final ConnectionHandlerNoSetup<int[], SQLException> handler = new ConnectionHandlerNoSetup<int[], SQLException>() {
1021
            @Override
1022
            public int[] handle(SQLDataSource ds) throws SQLException {
1023
                afterQueryInfo.set(System.nanoTime());
1024
                conn.set(ds.getConnection());
1025
                final int[] res;
1026
                try (final Statement stmt = conn.get().createStatement()) {
1027
                    for (final String s : queries) {
1028
                        stmt.addBatch(s);
1029
                    }
1030
                    if (Thread.currentThread().isInterrupted())
1031
                        throw new RTInterruptedException("Interrupted before executing : " + queries);
1032
                    res = stmt.executeBatch();
1033
                }
1034
                afterExecute.set(System.nanoTime());
1035
                return res;
1036
            }
1037
        };
1038
        final int[] res = atomic ? SQLUtils.executeAtomic(this, handler) : this.useConnection(handler);
1039
        long totalCount = 0;
1040
        int i = 0;
1041
        for (final int count : res) {
1042
            if (count == Statement.SUCCESS_NO_INFO) {
1043
                totalCount = -1;
1044
                break;
1045
            } else {
1046
                if (count < 0)
1047
                    throw new SQLException("Invalid count (" + count + ") for query " + i + " : " + queries.get(i));
1048
                totalCount += count;
1049
            }
1050
            i++;
1051
        }
1052
        if (SQLRequestLog.isEnabled()) {
1053
            final long afterHandle = System.nanoTime();
1054
            SQLRequestLog.log(queries.toString(), "executeBatch", conn.get(), timeMs, time, afterCache, afterQueryInfo.get(), afterExecute.get(), afterHandle, afterHandle, (int) totalCount);
1055
        }
1056
        return Tuple2.create(totalCount, res);
1057
    }
1058
 
1059
    /**
93 ilm 1060
     * Try to execute a {@link #getValidationQuery() simple query} on the database server. This
1061
     * method even works when the pool is exhausted.
1062
     *
1063
     * @throws SQLException if the query couldn't be executed.
1064
     */
1065
    public final void validateDBConnectivity() throws SQLException {
1066
        this.validateDBConnectivity(this.getValidationQueryTimeout());
1067
    }
1068
 
1069
    public final void validateDBConnectivity(final int timeout) throws SQLException {
1070
        this.useConnection(new ConnectionHandlerNoSetup<Object, SQLException>() {
1071
            @Override
1072
            public Object handle(SQLDataSource ds) throws SQLException {
1073
                final Statement stmt = ds.getConnection().createStatement();
1074
                try {
1075
                    stmt.setQueryTimeout(timeout);
1076
                    final ResultSet rs = stmt.executeQuery(ds.getValidationQuery());
1077
                    if (!rs.next())
1078
                        throw new SQLException("No row returned");
1079
                    rs.close();
1080
                } finally {
1081
                    stmt.close();
1082
                }
1083
                return null;
1084
            }
1085
        }, true);
1086
    }
1087
 
17 ilm 1088
    private Object[] executeOnce(String query, Connection c) throws SQLException {
1089
        final Statement stmt = c.createStatement();
1090
        final ResultSet rs = execute(query, stmt);
1091
        return new Object[] { stmt, rs };
1092
    }
1093
 
1094
    /**
1095
     * Exécute la requête et retourne le résultat. Attention le resultSet peut cesser d'être valide
1096
     * a tout moment, de plus cette méthode ne ferme pas le statement qu'elle crée, la méthode
1097
     * préférée est execute()
1098
     *
1099
     * @param query le requête à exécuter.
1100
     * @return le résultat de la requête.
1101
     * @deprecated replaced by execute().
1102
     * @see #execute(String)
1103
     */
1104
    public ResultSet executeRaw(String query) {
1105
        try {
1106
            return execute(query, this.getStatement());
1107
        } catch (SQLException e) {
1108
            try {
1109
                return execute(query, this.getStatement());
1110
            } catch (SQLException ex) {
1111
                ExceptionHandler.handle("Impossible d'executer la query: " + query, ex);
1112
                return null;
1113
            }
1114
        }
1115
    }
1116
 
1117
    /**
1118
     * Retourne un nouveau statement. Attention, la fermeture est à la charge de l'appelant.
1119
     *
1120
     * @return un nouveau statement.
1121
     * @throws SQLException if an error occurs.
1122
     */
1123
    private Statement getStatement() throws SQLException {
1124
        return this.getConnection().createStatement();
1125
    }
1126
 
1127
    /**
1128
     * Execute la requete avec le statement passé. Attention cette méthode ne peut fermer le
1129
     * statement car elle retourne directement le resultSet.
1130
     *
1131
     * @param query le requête à exécuter.
1132
     * @param stmt le statement.
1133
     * @return le résultat de la requête, should never be null according to the spec but Derby don't
1134
     *         care.
1135
     * @throws SQLException si erreur lors de l'exécution de la requête.
1136
     */
1137
    private ResultSet execute(String query, Statement stmt) throws SQLException, RTInterruptedException {
1138
        // System.err.println("\n" + count + "*** " + query + "\n");
1139
 
1140
        if (State.DEBUG)
1141
            State.INSTANCE.beginRequest(query);
1142
 
1143
        // test before calling JDBC methods and creating threads
80 ilm 1144
        boolean interrupted = false;
1145
        if (QUERY_TUNING > 0) {
1146
            try {
1147
                Thread.sleep(QUERY_TUNING);
1148
            } catch (InterruptedException e1) {
1149
                interrupted = true;
1150
            }
1151
        } else {
1152
            interrupted = Thread.currentThread().isInterrupted();
1153
        }
1154
        if (interrupted) {
17 ilm 1155
            throw new RTInterruptedException("request interrupted : " + query);
1156
        }
1157
 
1158
        final long t1 = System.currentTimeMillis();
1159
        ResultSet rs = null;
1160
        try {
1161
            // MAYBE un truc un peu plus formel
80 ilm 1162
            if (query.startsWith("INSERT") || query.startsWith("UPDATE") || query.startsWith("DELETE") || query.startsWith("CREATE") || query.startsWith("ALTER") || query.startsWith("DROP")
1163
                    || query.startsWith("SET")) {
83 ilm 1164
                // MS SQL doesn't support UPDATE
1165
                final boolean returnGenK = query.startsWith("INSERT") && stmt.getConnection().getMetaData().supportsGetGeneratedKeys();
17 ilm 1166
                stmt.executeUpdate(query, returnGenK ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
1167
                rs = returnGenK ? stmt.getGeneratedKeys() : null;
1168
            } else {
1169
                // TODO en faire qu'un seul par Connection
1170
                final ExecutorThread thr = new ExecutorThread(stmt, query);
1171
                // on lance l'exécution
1172
                thr.start();
1173
                // et on attend soit qu'elle finisse soit qu'on soit interrompu
1174
                try {
1175
                    rs = thr.getRs();
93 ilm 1176
                } catch (SQLException e) {
1177
                    if (getSystem() == SQLSystem.MYSQL && e.getErrorCode() == 1317) {
1178
                        thr.stopQuery();
1179
                        throw new InterruptedQuery("request interrupted : " + query, e, thr);
1180
                    } else {
1181
                        throw e;
1182
                    }
17 ilm 1183
                } catch (InterruptedException e) {
1184
                    thr.stopQuery();
1185
                    throw new InterruptedQuery("request interrupted : " + query, e, thr);
1186
                }
1187
            }
1188
        } finally {
1189
            if (State.DEBUG)
1190
                State.INSTANCE.endRequest(query);
1191
        }
1192
        long t2 = System.currentTimeMillis();
1193
        // obviously very long queries tend to last longer, that's normal so don't warn
1194
        if (t2 - t1 > 1000 && query.length() < 1000) {
1195
            System.err.println("Warning:" + (t2 - t1) + "ms pour :" + query);
1196
        }
1197
 
1198
        count++;
1199
        return rs;
1200
    }
1201
 
1202
    private final class InterruptedQuery extends RTInterruptedException {
1203
 
1204
        private final ExecutorThread thread;
1205
 
1206
        InterruptedQuery(String message, Throwable cause, ExecutorThread thr) {
1207
            super(message, cause);
1208
            this.thread = thr;
1209
        }
1210
 
1211
        public final ExecutorThread getThread() {
1212
            return this.thread;
1213
        }
1214
    }
1215
 
1216
    private static int executorSerial = 0;
1217
 
1218
    private final class ExecutorThread extends Thread {
1219
 
1220
        private final Statement stmt;
1221
        private final String query;
1222
 
1223
        private ResultSet rs;
1224
        private Exception exn;
1225
        private boolean canceled;
1226
 
1227
        public ExecutorThread(Statement stmt, String query) {
1228
            super(executorSerial++ + " ExecutorThread on " + query);
1229
            this.stmt = stmt;
1230
            this.query = query;
1231
            this.canceled = false;
1232
        }
1233
 
1234
        public void run() {
1235
            synchronized (this) {
1236
                if (this.canceled)
1237
                    return;
1238
            }
1239
 
1240
            ResultSet rs = null;
1241
            try {
1242
                // do not use executeQuery since this.query might contain several statements
1243
                this.stmt.execute(this.query);
1244
                synchronized (this) {
1245
                    if (this.canceled)
1246
                        return;
1247
                }
1248
                rs = this.stmt.getResultSet();
1249
            } catch (Exception e) {
1250
                // can only be SQLException or RuntimeException
1251
                // eg MySQLStatementCancelledException if stopQuery() was called
1252
                this.exn = e;
1253
            }
1254
            this.rs = rs;
1255
        }
1256
 
1257
        public void stopQuery() throws SQLException {
93 ilm 1258
            if (!this.stmt.isClosed())
1259
                this.stmt.cancel();
17 ilm 1260
            synchronized (this) {
1261
                this.canceled = true;
1262
            }
1263
        }
1264
 
1265
        public ResultSet getRs() throws SQLException, InterruptedException {
1266
            this.join();
1267
            // pas besoin de synchronized puisque seule notre thread ecrit les var
1268
            // et qu'elle est maintenant terminée
1269
            if (this.exn != null) {
1270
                if (this.exn instanceof SQLException)
1271
                    throw (SQLException) this.exn;
1272
                else
1273
                    throw (RuntimeException) this.exn;
1274
            }
1275
            return this.rs;
1276
        }
1277
    }
1278
 
1279
    /**
1280
     * All connections obtained with {@link #getConnection()} will be closed immediately, but
1281
     * threads in {@link #useConnection(ConnectionHandler)} will get to keep them. After the last
1282
     * thread returns from {@link #useConnection(ConnectionHandler)} there won't be any connection
73 ilm 1283
     * left open. This instance will be permanently closed, it cannot be reused later.
17 ilm 1284
     *
1285
     * @throws SQLException if a database error occurs
1286
     */
1287
    public synchronized void close() throws SQLException {
80 ilm 1288
        this.sysRoot.rmListener(this.descL);
73 ilm 1289
        @SuppressWarnings("rawtypes")
17 ilm 1290
        final GenericObjectPool pool = this.connectionPool;
1291
        super.close();
1292
        // super close and unset our pool, but we need to keep it
1293
        // to allow used connections to be closed, see #closeConnection(Connection)
1294
        this.connectionPool = pool;
63 ilm 1295
 
17 ilm 1296
        // interrupt to force waiting threads to close their connections
1297
        if (this.exec != null) {
1298
            this.exec.shutdownNow();
1299
            this.exec = null;
1300
        }
1301
 
1302
        // uptodate was cleared by closeConnection()
1303
        // the handlers will clear themselves
1304
        // the cache is expected to be cleared (when all connections are closed)
1305
        if (this.getBorrowedConnectionCount() == 0)
1306
            noConnectionIsOpen();
1307
        // ATTN keep tables to be able to reopen
1308
    }
1309
 
61 ilm 1310
    private synchronized void noConnectionIsOpen() {
80 ilm 1311
        assert this.connectionPool == null || (this.connectionPool.getNumIdle() + this.getBorrowedConnectionCount()) == 0;
17 ilm 1312
        if (this.cache != null)
132 ilm 1313
            this.cache.getSupp().die();
17 ilm 1314
    }
1315
 
1316
    /**
63 ilm 1317
     * Retourne la connection à cette source de donnée.
1318
     *
1319
     * @return la connection à cette source de donnée.
1320
     * @throws IllegalStateException if not called from within useConnection().
1321
     * @see #useConnection(ConnectionHandler)
67 ilm 1322
     * @see #handlingConnection()
17 ilm 1323
     */
63 ilm 1324
    public final Connection getConnection() {
73 ilm 1325
        final HandlersStack res = this.getHandlersStack();
63 ilm 1326
        if (res == null)
1327
            throw new IllegalStateException("useConnection() wasn't called");
1328
        return res.getConnection();
17 ilm 1329
    }
1330
 
73 ilm 1331
    public final TransactionPoint getTransactionPoint() {
1332
        final HandlersStack handlersStack = this.getHandlersStack();
1333
        if (handlersStack == null)
1334
            return null;
1335
        return handlersStack.getLastTxPoint();
1336
    }
1337
 
17 ilm 1338
    /**
63 ilm 1339
     * Retourne une connection à cette source de donnée (generally
93 ilm 1340
     * {@link #useConnection(ConnectionHandler)} should be used). If a connection in the pool fails
1341
     * to {@link #initConnection(Connection) initialize} or if the pool is empty and a new
1342
     * connection fails to get created, this method will try to borrow a connection from the pool a
1343
     * second time.
17 ilm 1344
     * <p>
63 ilm 1345
     * Note : you <b>must</b> return this connection (e.g. use try/finally).
17 ilm 1346
     * <p>
1347
     *
1348
     * @return une connection à cette source de donnée.
93 ilm 1349
     * @throws NoSuchElementException after {@link #getMaxWait()} milliseconds if the pool is
1350
     *         exhausted and {@link #blocksWhenExhausted()}.
63 ilm 1351
     * @see #returnConnection(Connection)
1352
     * @see #closeConnection(Connection)
17 ilm 1353
     */
93 ilm 1354
    protected final Connection getNewConnection() throws NoSuchElementException {
17 ilm 1355
        try {
1356
            return this.borrowConnection(false);
1357
        } catch (RTInterruptedException e) {
1358
            throw e;
1359
        } catch (Exception e) {
93 ilm 1360
            if (e instanceof NoSuchElementException) {
1361
                // no need to try to test all others connections, the pool is just exhausted
1362
                throw (NoSuchElementException) e;
1363
            } else {
1364
                return this.borrowConnection(true);
1365
            }
17 ilm 1366
        }
1367
    }
1368
 
1369
    /**
1370
     * Borrow a new connection from the pool, optionally purging invalid connections with the
1371
     * validation query.
1372
     *
1373
     * @param test if <code>true</code> then testOnBorrow will be set.
1374
     * @return the new connection.
93 ilm 1375
     * @throws NoSuchElementException after {@link #getMaxWait()} milliseconds if the pool is
1376
     *         exhausted and {@link #blocksWhenExhausted()}.
17 ilm 1377
     */
93 ilm 1378
    private final Connection borrowConnection(final boolean test) throws NoSuchElementException {
17 ilm 1379
        if (test) {
142 ilm 1380
            synchronized (this.testLock) {
1381
                // invalidate all bad connections
1382
                setTestOnBorrow(true);
1383
                try {
1384
                    return this._borrowConnection(test);
1385
                } finally {
1386
                    setTestOnBorrow(false);
1387
                }
1388
            }
1389
        } else {
1390
            return this._borrowConnection(test);
17 ilm 1391
        }
142 ilm 1392
    }
1393
 
1394
    private final Connection _borrowConnection(final boolean test) throws NoSuchElementException {
1395
        // when we call borrowConnection() with test, it's because there was an error so this
1396
        // call is already a second try, thus getRawConnection() shouldn't try a third time.
1397
        final Connection res = this.getRawConnection(!test);
17 ilm 1398
        try {
142 ilm 1399
            initConnection(res);
1400
            return res;
1401
        } catch (RuntimeException e) {
1402
            this.closeConnection(res);
1403
            throw e;
17 ilm 1404
        }
1405
    }
1406
 
1407
    // initialize the passed connection if needed
1408
    protected final void initConnection(final Connection res) {
1409
        boolean setSchema = false;
1410
        String schemaToSet = null;
1411
        synchronized (this) {
73 ilm 1412
            if (!this.schemaUptodate.containsKey(res)) {
17 ilm 1413
                if (this.initialShemaSet) {
1414
                    setSchema = true;
1415
                    schemaToSet = this.initialShema;
1416
                }
1417
                // safe to put before setSchema() since res cannot be passed to
1418
                // release/closeConnection()
73 ilm 1419
                this.schemaUptodate.put(res, null);
17 ilm 1420
            }
73 ilm 1421
            // a connection from the pool is up to date since we close all idle connections in
1422
            // invalidateAllConnections() and borrowed ones are closed before they return to the
1423
            // pool
1424
            this.uptodate.put(res, null);
17 ilm 1425
        }
1426
        // warmup the connection (executing a bogus simple query, like "SELECT 1") could help but in
1427
        // general doesn't since we often do getDS().execute() and thus our warm up thread will run
1428
        // after the execute(), making it useless.
1429
        if (setSchema)
1430
            this.setSchema(schemaToSet, res);
1431
    }
1432
 
1433
    private static final String pgInterrupted = GT.tr("Interrupted while attempting to connect.");
1434
 
93 ilm 1435
    private void getRawConnectionThrow(final Exception e1, final Exception e2) throws NoSuchElementException {
1436
        if (e1.getCause() instanceof NoSuchElementException)
1437
            throw (NoSuchElementException) e1.getCause();
1438
        else if (e2 == null)
1439
            throw new IllegalStateException("Impossible d'obtenir une connexion sur " + this, e1);
1440
        else
1441
            throw new IllegalStateException("Impossible d'obtenir une connexion sur " + this + "après 2 essais\nexception 2 :" + e2.getLocalizedMessage(), e1);
1442
    }
1443
 
1444
    private Connection getRawConnection(final boolean retry) throws NoSuchElementException {
132 ilm 1445
        assert !Thread.holdsLock(
1446
                this) : "super.getConnection() might block (see setWhenExhaustedAction()), and since return/closeConnection() need this lock, this method cannot wait while holding the lock";
17 ilm 1447
        Connection result = null;
1448
        try {
1449
            result = super.getConnection();
93 ilm 1450
        } catch (Exception e1) {
17 ilm 1451
            // try to know if interrupt, TODO cleanup : patch pg Driver.java to fill the cause
1452
            if (e1.getCause() instanceof InterruptedException || (e1 instanceof PSQLException && e1.getMessage().equals(pgInterrupted))) {
1453
                throw new RTInterruptedException(e1);
1454
            }
93 ilm 1455
            final int retryWait = retry ? this.retryWait : -1;
142 ilm 1456
            if (retryWait < 0 || e1 instanceof SQLNonTransientException)
93 ilm 1457
                getRawConnectionThrow(e1, null);
17 ilm 1458
            try {
1459
                // on attend un petit peu
93 ilm 1460
                Thread.sleep(retryWait);
17 ilm 1461
                // avant de réessayer
1462
                result = super.getConnection();
1463
            } catch (InterruptedException e) {
1464
                throw new RTInterruptedException("interrupted while waiting for a second try", e);
1465
            } catch (Exception e) {
93 ilm 1466
                getRawConnectionThrow(e1, e);
17 ilm 1467
            }
1468
        }
1469
        if (State.DEBUG)
1470
            State.INSTANCE.connectionCreated();
1471
        return result;
1472
    }
1473
 
1474
    public final int getBorrowedConnectionCount() {
80 ilm 1475
        return this.connectionPool == null ? 0 : this.connectionPool.getNumActive();
17 ilm 1476
    }
1477
 
63 ilm 1478
    public synchronized boolean blocksWhenExhausted() {
1479
        return this.blockWhenExhausted;
17 ilm 1480
    }
1481
 
63 ilm 1482
    public synchronized void setBlockWhenExhausted(boolean block) {
1483
        this.blockWhenExhausted = block;
1484
        if (this.connectionPool != null) {
1485
            this.connectionPool.setWhenExhaustedAction(block ? GenericObjectPool.WHEN_EXHAUSTED_BLOCK : GenericObjectPool.WHEN_EXHAUSTED_GROW);
1486
        }
1487
    }
1488
 
67 ilm 1489
    public synchronized final long getSoftMinEvictableIdleTimeMillis() {
1490
        return this.softMinEvictableIdleTimeMillis;
1491
    }
1492
 
1493
    public synchronized final void setSoftMinEvictableIdleTimeMillis(long millis) {
1494
        this.softMinEvictableIdleTimeMillis = millis;
1495
        if (this.connectionPool != null) {
1496
            this.connectionPool.setSoftMinEvictableIdleTimeMillis(millis);
1497
        }
1498
    }
1499
 
73 ilm 1500
    /**
1501
     * Whether the database defaut transaction isolation is check only once for this instance. If
1502
     * <code>false</code>, every new connection will have its
1503
     * {@link Connection#setTransactionIsolation(int) isolation set}. If <code>true</code> the
1504
     * isolation will only be set if the {@link #setInitialTransactionIsolation(int) requested one}
1505
     * differs from the DB one. In other words, if you want to optimize DB access, the DB
1506
     * configuration must match the datasource configuration.
1507
     *
1508
     * @param checkOnce <code>true</code> to check only once the DB transaction isolation.
1509
     */
1510
    public synchronized final void setTransactionIsolationCheckedOnce(final boolean checkOnce) {
1511
        this.checkOnceDBTxIsolation = checkOnce;
1512
        this.dbTxIsolation = null;
1513
    }
1514
 
1515
    public synchronized final boolean isTransactionIsolationCheckedOnce() {
1516
        return this.checkOnceDBTxIsolation;
1517
    }
1518
 
1519
    // don't use setDefaultTransactionIsolation() in super since it makes extra requests each time a
1520
    // connection is borrowed
1521
    public final void setInitialTransactionIsolation(int level) {
1522
        if (level != Connection.TRANSACTION_READ_UNCOMMITTED && level != Connection.TRANSACTION_READ_COMMITTED && level != Connection.TRANSACTION_REPEATABLE_READ
1523
                && level != Connection.TRANSACTION_SERIALIZABLE)
1524
            throw new IllegalArgumentException("Invalid value :" + level);
1525
        synchronized (this) {
1526
            if (this.txIsolation != level) {
1527
                this.txIsolation = level;
1528
                // perhaps do like setInitialSchema() : i.e. call setTransactionIsolation() on
1529
                // existing connections
1530
                this.invalidateAllConnections(false);
1531
            }
1532
        }
1533
    }
1534
 
1535
    public synchronized final int getInitialTransactionIsolation() {
1536
        return this.txIsolation;
1537
    }
1538
 
1539
    public synchronized final Integer getDBTransactionIsolation() {
1540
        return this.dbTxIsolation;
1541
    }
1542
 
1543
    final synchronized void setTransactionIsolation(Connection conn) throws SQLException {
1544
        if (this.dbTxIsolation == null) {
1545
            this.dbTxIsolation = conn.getTransactionIsolation();
1546
            assert this.dbTxIsolation != null;
1547
        }
1548
        // no need to try to change the level if the DB doesn't support transactions
1549
        if (this.dbTxIsolation != Connection.TRANSACTION_NONE && (!this.checkOnceDBTxIsolation || this.dbTxIsolation != this.txIsolation)) {
1550
            // if not check once, it's the desired action, so don't log
1551
            if (this.checkOnceDBTxIsolation)
1552
                Log.get().config("Setting transaction isolation to " + this.txIsolation);
1553
            conn.setTransactionIsolation(this.txIsolation);
1554
        }
1555
    }
1556
 
1557
    // allow to know transaction states
1558
    private final class TransactionPoolableConnection extends PoolableConnection {
1559
        // perhaps call getAutoCommit() once to have the initial value
1560
        @GuardedBy("this")
1561
        private boolean autoCommit = true;
1562
 
1563
        private TransactionPoolableConnection(Connection conn, @SuppressWarnings("rawtypes") ObjectPool pool, AbandonedConfig config) {
1564
            super(conn, pool, config);
1565
        }
1566
 
1567
        private HandlersStack getNonNullHandlersStack() throws SQLException {
1568
            final HandlersStack res = getHandlersStack();
1569
            if (res == null)
1570
                throw new SQLException("Unsafe transaction, call useConnection() or SQLUtils.executeAtomic()");
1571
            return res;
1572
        }
1573
 
1574
        @Override
1575
        public synchronized void setAutoCommit(boolean autoCommit) throws SQLException {
1576
            if (this.autoCommit != autoCommit) {
1577
                // don't call setAutoCommit() if no stack
1578
                final HandlersStack handlersStack = getNonNullHandlersStack();
1579
                super.setAutoCommit(autoCommit);
1580
                this.autoCommit = autoCommit;
1581
                if (this.autoCommit)
1582
                    // some delegates of the super implementation might have already called our
1583
                    // commit(), but in this case, the following commit will be a no-op
132 ilm 1584
                    handlersStack.commit(null);
73 ilm 1585
                else
1586
                    handlersStack.addTxPoint(new TransactionPoint(this));
1587
            }
1588
        }
1589
 
1590
        @Override
1591
        public synchronized void commit() throws SQLException {
1592
            super.commit();
132 ilm 1593
            assert !this.autoCommit;
73 ilm 1594
            final HandlersStack handlersStack = getNonNullHandlersStack();
132 ilm 1595
            handlersStack.commit(new TransactionPoint(this));
73 ilm 1596
        }
1597
 
1598
        @Override
1599
        public synchronized void rollback() throws SQLException {
1600
            super.rollback();
132 ilm 1601
            assert !this.autoCommit;
73 ilm 1602
            final HandlersStack handlersStack = getNonNullHandlersStack();
132 ilm 1603
            handlersStack.rollback(new TransactionPoint(this));
73 ilm 1604
        }
1605
 
1606
        @Override
1607
        public synchronized Savepoint setSavepoint() throws SQLException {
1608
            // don't call setSavepoint() if no stack
1609
            final HandlersStack handlersStack = getNonNullHandlersStack();
1610
            final Savepoint res = super.setSavepoint();
83 ilm 1611
            // MySQL always create named save points
1612
            handlersStack.addTxPoint(new TransactionPoint(this, res, getSystem() == SQLSystem.MYSQL));
73 ilm 1613
            return res;
1614
        }
1615
 
1616
        @Override
1617
        public synchronized Savepoint setSavepoint(String name) throws SQLException {
1618
            // don't call setSavepoint() if no stack
1619
            final HandlersStack handlersStack = getNonNullHandlersStack();
1620
            final Savepoint res = super.setSavepoint(name);
1621
            handlersStack.addTxPoint(new TransactionPoint(this, res, true));
1622
            return res;
1623
        }
1624
 
1625
        @Override
1626
        public synchronized void rollback(Savepoint savepoint) throws SQLException {
1627
            super.rollback(savepoint);
1628
            getNonNullHandlersStack().rollback(savepoint);
1629
        }
1630
 
1631
        @Override
1632
        public synchronized void releaseSavepoint(Savepoint savepoint) throws SQLException {
1633
            super.releaseSavepoint(savepoint);
132 ilm 1634
            getNonNullHandlersStack().releaseSavepoint(savepoint);
73 ilm 1635
        }
1636
    }
1637
 
17 ilm 1638
    @Override
73 ilm 1639
    protected void createPoolableConnectionFactory(ConnectionFactory driverConnectionFactory, @SuppressWarnings("rawtypes") KeyedObjectPoolFactory statementPoolFactory, AbandonedConfig configuration)
1640
            throws SQLException {
1641
        PoolableConnectionFactory connectionFactory = null;
1642
        try {
1643
            connectionFactory = new PoolableConnectionFactory(driverConnectionFactory, this.connectionPool, statementPoolFactory, this.validationQuery, this.validationQueryTimeout,
1644
                    this.connectionInitSqls, this.defaultReadOnly, this.defaultAutoCommit, this.defaultTransactionIsolation, this.defaultCatalog, configuration) {
17 ilm 1645
                @Override
73 ilm 1646
                public Object makeObject() throws Exception {
1647
                    Connection conn = this._connFactory.createConnection();
1648
                    if (conn == null) {
1649
                        throw new IllegalStateException("Connection factory returned null from createConnection");
17 ilm 1650
                    }
73 ilm 1651
                    initializeConnection(conn);
1652
                    setTransactionIsolation(conn);
1653
                    if (null != this._stmtPoolFactory) {
1654
                        @SuppressWarnings("rawtypes")
1655
                        KeyedObjectPool stmtpool = this._stmtPoolFactory.createPool();
1656
                        conn = new PoolingConnection(conn, stmtpool);
1657
                        stmtpool.setFactory((PoolingConnection) conn);
1658
                    }
1659
                    return new TransactionPoolableConnection(conn, this._pool, this._config);
17 ilm 1660
                }
1661
            };
73 ilm 1662
            validateConnectionFactory(connectionFactory);
1663
        } catch (RuntimeException e) {
1664
            throw e;
81 ilm 1665
        } catch (SQLException e) {
1666
            // only wrap if necessary (calling code can use SQLState)
1667
            throw e;
73 ilm 1668
        } catch (Exception e) {
1669
            throw new SQLException("Cannot create PoolableConnectionFactory", e);
17 ilm 1670
        }
1671
    }
1672
 
73 ilm 1673
    @Override
1674
    protected void createConnectionPool() {
1675
        super.createConnectionPool();
1676
        // methods not defined in superclass and thus not called in super
1677
        this.connectionPool.setLifo(true);
1678
        this.setBlockWhenExhausted(this.blockWhenExhausted);
1679
        this.connectionPool.setSoftMinEvictableIdleTimeMillis(this.softMinEvictableIdleTimeMillis);
1680
    }
1681
 
1682
    @Override
93 ilm 1683
    protected ConnectionFactory createConnectionFactory() throws SQLException {
1684
        final ConnectionFactory res = super.createConnectionFactory();
1685
        this.connectionFactory = res;
1686
        return res;
1687
    }
1688
 
1689
    @Override
73 ilm 1690
    protected void createDataSourceInstance() throws SQLException {
1691
        // PoolingDataSource returns a PoolGuardConnectionWrapper that complicates a lot of
1692
        // things for nothing, so overload to simply return an object of the pool
1693
        this.dataSource = new PoolingDataSource(this.connectionPool) {
1694
 
1695
            // we'll migrate to plain SQLException when our superclass does
1696
            @SuppressWarnings("deprecation")
1697
            @Override
1698
            public Connection getConnection() throws SQLException {
1699
                try {
1700
                    return (Connection) this._pool.borrowObject();
1701
                } catch (SQLException e) {
1702
                    throw e;
1703
                } catch (NoSuchElementException e) {
1704
                    throw new SQLNestedException("Cannot get a connection, pool exhausted", e);
1705
                } catch (RuntimeException e) {
1706
                    throw e;
1707
                } catch (Exception e) {
1708
                    throw new SQLNestedException("Cannot get a connection, general error", e);
1709
                }
1710
            }
1711
 
1712
            @Override
1713
            public Connection getConnection(String username, String password) throws SQLException {
1714
                throw new UnsupportedOperationException();
1715
            }
1716
        };
1717
    }
1718
 
17 ilm 1719
    /**
1720
     * To return a connection to the pool.
1721
     *
1722
     * @param con a connection obtained with getRawConnection(), can be <code>null</code>.
1723
     */
1724
    protected void returnConnection(final Connection con) {
1725
        if (con != null) {
1726
            // if !this.initialShemaSet the out of date cannot be brought up to date
1727
            final boolean unrecoverableOutOfDate;
1728
            synchronized (this) {
73 ilm 1729
                unrecoverableOutOfDate = !this.uptodate.containsKey(con) || !this.initialShemaSet && !this.schemaUptodate.containsKey(con);
17 ilm 1730
            }
1731
            if (isClosed() || unrecoverableOutOfDate)
1732
                // if closed : don't fill the pool (which will have thrown an exception anyway)
1733
                // if we shouldn't set the schema, we must close all previous connections
1734
                // so that we get new ones from the db with the current setting
1735
                this.closeConnection(con);
1736
            else {
1737
                try {
1738
                    // our connectionPool use PoolableConnectionFactory which creates
1739
                    // PoolableConnection whose close() actually does a returnObject()
1740
                    con.close();
1741
                } catch (Exception e) {
1742
                    /* tant pis */
65 ilm 1743
                    Log.get().log(Level.FINE, "Could not return " + con, e);
17 ilm 1744
                }
1745
                if (State.DEBUG)
1746
                    State.INSTANCE.connectionRemoved();
1747
            }
1748
        }
1749
    }
1750
 
1751
    /**
1752
     * To actually close a connection to the db (and remove it from the pool).
1753
     *
1754
     * @param con a connection obtained with getRawConnection(), can be <code>null</code>.
1755
     */
1756
    protected void closeConnection(final Connection con) {
1757
        // Neither BasicDataSource nor PoolingDataSource provide a closeConnection()
1758
        // so we implement one here
1759
        if (con != null) {
1760
            synchronized (this) {
1761
                this.uptodate.remove(con);
73 ilm 1762
                this.schemaUptodate.remove(con);
17 ilm 1763
            }
1764
            try {
1765
                // ATTN this always does _numActive--, so we can't call it multiple times
1766
                // with the same object
1767
                this.connectionPool.invalidateObject(con);
1768
            } catch (Exception e) {
1769
                /* tant pis */
65 ilm 1770
                Log.get().log(Level.FINE, "Could not close " + con, e);
17 ilm 1771
            }
1772
            // the last connection is being returned
1773
            if (this.isClosed() && this.getBorrowedConnectionCount() == 0) {
1774
                noConnectionIsOpen();
1775
            }
1776
        }
1777
    }
1778
 
1779
    /**
73 ilm 1780
     * Invalidates all open connections. This immediately closes idle connections. Borrowed ones are
1781
     * marked as invalid, so that they are closed on return. In other words, after this method
1782
     * returns, no existing connection will be provided.
1783
     */
1784
    public final void invalidateAllConnections() {
1785
        this.invalidateAllConnections(false);
1786
    }
1787
 
1788
    public final void invalidateAllConnections(final boolean preventIdleConnections) {
1789
        // usefull since Evictor of GenericObjectPool might call ensureMinIdle()
1790
        if (preventIdleConnections) {
1791
            this.setMinIdle(0);
1792
            this.setMaxIdle(0);
1793
        }
1794
        synchronized (this) {
1795
            // otherwise nothing to invalidate
1796
            if (this.connectionPool != null) {
1797
                // closes all idle connections
1798
                this.connectionPool.clear();
1799
                // borrowed connections will be closed on return
1800
                this.uptodate.clear();
1801
            }
1802
        }
1803
    }
1804
 
1805
    /**
17 ilm 1806
     * From now on, every new connection will have its default schema set to schemaName.
1807
     *
1808
     * @param schemaName the name of the initial default schema, <code>null</code> to remove any
1809
     *        default schema.
1810
     */
1811
    public void setInitialSchema(String schemaName) {
80 ilm 1812
        if (schemaName != null || this.getSystem().isClearingPathSupported()) {
17 ilm 1813
            this.setInitialSchema(true, schemaName);
80 ilm 1814
        } else if (this.getSystem().isDBPathEmpty()) {
17 ilm 1815
            this.unsetInitialSchema();
1816
        } else
1817
            throw new IllegalArgumentException(this + " cannot have no default schema");
1818
    }
1819
 
65 ilm 1820
    /**
1821
     * From now on, connections won't have their default schema set by this. Of course the SQL
1822
     * server might have set one.
1823
     */
17 ilm 1824
    public void unsetInitialSchema() {
1825
        this.setInitialSchema(false, null);
1826
    }
1827
 
1828
    private final void setInitialSchema(final boolean set, final String schemaName) {
65 ilm 1829
        synchronized (this.setInitialShemaLock) {
1830
            synchronized (this) {
1831
                // even if schemaName no longer exists, and thus the following test would fail, the
1832
                // next initConnection() will correctly fail
1833
                if (this.initialShemaSet == set && CompareUtils.equals(this.initialShema, schemaName))
1834
                    return;
1835
            }
1836
            final Connection newConn;
1837
            if (set) {
1838
                // test if schemaName is valid
1839
                newConn = this.getNewConnection();
1840
                try {
1841
                    this.setSchema(schemaName, newConn);
1842
                } catch (RuntimeException e) {
1843
                    this.closeConnection(newConn);
1844
                    throw e;
1845
                }
1846
                // don't return connection right now otherwise it might be deemed unrecoverable
1847
            } else {
1848
                newConn = null;
1849
            }
1850
            synchronized (this) {
1851
                this.initialShemaSet = set;
1852
                this.initialShema = schemaName;
73 ilm 1853
                this.schemaUptodate.clear();
65 ilm 1854
                if (!set)
1855
                    // by definition we don't want to modify the connection,
1856
                    // so empty the pool, that way new connections will be created
1857
                    // the borrowed ones will be closed when returned
1858
                    this.connectionPool.clear();
1859
                else
73 ilm 1860
                    this.schemaUptodate.put(newConn, null);
65 ilm 1861
            }
1862
            this.returnConnection(newConn);
17 ilm 1863
        }
1864
    }
1865
 
1866
    public synchronized final String getInitialSchema() {
1867
        return this.initialShema;
1868
    }
1869
 
1870
    /**
1871
     * Set the default schema of the current thread's connection. NOTE: pointless if not in
1872
     * {@link #useConnection(ConnectionHandler)} since otherwise a connection will be borrowed then
1873
     * closed.
1874
     *
1875
     * @param schemaName the name of the new default schema.
1876
     */
1877
    public void setSchema(String schemaName) {
1878
        this.setSchema(schemaName, null);
1879
    }
1880
 
1881
    private void setSchema(String schemaName, Connection c) {
1882
        final String q;
1883
        if (this.getSystem() == SQLSystem.MYSQL) {
1884
            if (schemaName == null) {
1885
                if (this.getSchema(c) != null)
1886
                    throw new IllegalArgumentException("cannot unset DATABASE in MySQL");
1887
                else
1888
                    // nothing to do
1889
                    q = null;
1890
            } else
1891
                q = "USE " + schemaName;
73 ilm 1892
        } else if (this.getSystem() == SQLSystem.DERBY) {
17 ilm 1893
            q = "SET SCHEMA " + SQLBase.quoteIdentifier(schemaName);
73 ilm 1894
        } else if (this.getSystem() == SQLSystem.H2) {
1895
            q = "SET SCHEMA " + SQLBase.quoteIdentifier(schemaName);
17 ilm 1896
            // TODO use the line below, but for now it is only used after schema()
1897
            // plus there's no function to read it back
1898
            // q = "set SCHEMA_SEARCH_PATH " + SQLBase.quoteIdentifier(schemaName == null ? "" :
1899
            // schemaName);
1900
        } else if (this.getSystem() == SQLSystem.POSTGRESQL) {
1901
            if (schemaName == null) {
73 ilm 1902
                // SET cannot empty the path
1903
                q = "select set_config('search_path', '', false)";
17 ilm 1904
            } else {
73 ilm 1905
                q = "set session search_path to " + SQLBase.quoteIdentifier(schemaName);
17 ilm 1906
            }
1907
        } else if (this.getSystem() == SQLSystem.MSSQL) {
83 ilm 1908
            if (schemaName == null) {
17 ilm 1909
                throw new IllegalArgumentException("cannot unset default schema in " + this.getSystem());
83 ilm 1910
            } else {
1911
                // ATTN MSSQL apparently hang until the connection that created the schema commits
1912
                q = "ALTER USER " + SQLBase.quoteIdentifier(getUsername()) + " with default_schema = " + SQLBase.quoteIdentifier(schemaName);
1913
            }
73 ilm 1914
        } else {
17 ilm 1915
            throw new UnsupportedOperationException();
73 ilm 1916
        }
17 ilm 1917
 
1918
        if (q != null)
1919
            this.execute(q, null, true, c);
1920
    }
1921
 
1922
    public final String getSchema() {
1923
        return this.getSchema(null);
1924
    }
1925
 
1926
    private String getSchema(Connection c) {
1927
        final String q;
1928
        if (this.getSystem() == SQLSystem.MYSQL)
1929
            q = "select DATABASE(); ";
1930
        else if (this.getSystem() == SQLSystem.DERBY)
1931
            q = "select CURRENT SCHEMA;";
1932
        else if (this.getSystem() == SQLSystem.POSTGRESQL) {
1933
            q = "select (current_schemas(false))[1];";
1934
        } else if (this.getSystem() == SQLSystem.H2) {
1935
            q = "select SCHEMA();";
1936
        } else if (this.getSystem() == SQLSystem.MSSQL) {
1937
            q = "select SCHEMA_NAME();";
1938
        } else
1939
            throw new UnsupportedOperationException();
1940
 
1941
        return (String) this.execute(q, SCALAR_HANDLER, c);
1942
    }
1943
 
132 ilm 1944
    @Override
17 ilm 1945
    public String toString() {
1946
        return this.getUrl();
1947
    }
1948
 
83 ilm 1949
    public final SQLSystem getSystem() {
80 ilm 1950
        return this.sysRoot.getServer().getSQLSystem();
17 ilm 1951
    }
1952
 
1953
    public Object clone() {
80 ilm 1954
        SQLDataSource ds = new SQLDataSource(this.sysRoot);
17 ilm 1955
        ds.setUrl(this.getUrl());
1956
        ds.setUsername(this.getUsername());
1957
        ds.setPassword(this.getPassword());
1958
        ds.setDriverClassName(this.getDriverClassName());
1959
        return ds;
1960
    }
1961
}