OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.utils;
15
 
16
import org.openconcerto.sql.model.ConnectionHandler;
17
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
18
import org.openconcerto.sql.model.DBSystemRoot;
67 ilm 19
import org.openconcerto.sql.model.IResultSetHandler;
17 ilm 20
import org.openconcerto.sql.model.SQLDataSource;
67 ilm 21
import org.openconcerto.sql.model.SQLRequestLog;
142 ilm 22
import org.openconcerto.sql.model.SQLResultSet;
17 ilm 23
import org.openconcerto.sql.model.SQLSystem;
67 ilm 24
import org.openconcerto.utils.RTInterruptedException;
182 ilm 25
import org.openconcerto.utils.cc.ITransformerExn;
17 ilm 26
 
27
import java.sql.Connection;
142 ilm 28
import java.sql.ResultSet;
17 ilm 29
import java.sql.SQLException;
83 ilm 30
import java.sql.Savepoint;
67 ilm 31
import java.sql.Statement;
17 ilm 32
import java.util.ArrayList;
33
import java.util.List;
34
import java.util.regex.Pattern;
35
 
73 ilm 36
import org.apache.commons.dbcp.DelegatingConnection;
67 ilm 37
import org.apache.commons.dbutils.ResultSetHandler;
38
 
73 ilm 39
import com.mysql.jdbc.ConnectionProperties;
40
 
17 ilm 41
public class SQLUtils {
42
 
67 ilm 43
    /**
44
     * Return the first chained exception with a non null SQL state.
45
     *
46
     * @param exn an exception.
47
     * @return the first SQLException with a non-<code>null</code>
48
     *         {@link SQLException#getSQLState()}, <code>null</code> if not found.
49
     */
50
    static public final SQLException findWithSQLState(final Exception exn) {
51
        Throwable e = exn;
52
        while (e != null) {
53
            if (e instanceof SQLException) {
54
                final SQLException sqlExn = (SQLException) e;
55
                if (sqlExn.getSQLState() != null) {
56
                    return sqlExn;
57
                }
58
            }
59
            e = e.getCause();
60
        }
61
        return null;
62
    }
63
 
17 ilm 64
    public interface SQLFactory<T> {
65
 
66
        public T create() throws SQLException;
67
 
68
    }
69
 
70
    /**
71
     * Use a single transaction to execute <code>f</code> : it is either committed or rollbacked.
72
     *
73
     * @param <T> type of factory
74
     * @param ds the datasource where f should be executed.
75
     * @param f the factory to execute.
76
     * @return what f returns.
77
     * @throws SQLException if a pb occurs.
78
     */
79
    public static <T> T executeAtomic(final SQLDataSource ds, final SQLFactory<T> f) throws SQLException {
80
        return executeAtomic(ds, new ConnectionHandlerNoSetup<T, SQLException>() {
81
            @Override
82
            public T handle(SQLDataSource ds) throws SQLException {
83
                return f.create();
84
            }
85
        });
86
    }
87
 
88
    /**
83 ilm 89
     * Use a single transaction to execute <code>h</code> : it is either committed or rolled back.
17 ilm 90
     *
91
     * @param <T> type of return
92
     * @param <X> type of exception of <code>h</code>
83 ilm 93
     * @param ds the data source where h should be executed.
17 ilm 94
     * @param h the code to execute.
95
     * @return what h returns.
83 ilm 96
     * @throws SQLException if a problem occurs.
17 ilm 97
     * @throws X if <code>h</code> throw it.
98
     */
99
    public static <T, X extends Exception> T executeAtomic(final SQLDataSource ds, final ConnectionHandlerNoSetup<T, X> h) throws SQLException, X {
90 ilm 100
        return executeAtomic(ds, h, false);
83 ilm 101
    }
102
 
182 ilm 103
    public static final <T, X extends Exception> T executeAtomic(final SQLDataSource ds, final ITransformerExn<? super SQLDataSource, T, X> run) throws SQLException, X {
104
        return executeAtomic(ds, new ConnectionHandlerNoSetup<T, X>() {
105
            @Override
106
            public T handle(SQLDataSource ds) throws X {
107
                return run.transformChecked(ds);
108
            }
109
        });
110
    }
111
 
83 ilm 112
    /**
113
     * Execute <code>h</code> in a transaction. Only the outer most call to
114
     * <code>executeAtomic()</code> commit or roll back a transaction, for recursive calls if
115
     * <code>continueTx</code> is <code>true</code> then nothing happens, else a save point is
116
     * created and rolled back if an exception occurs (allowing the caller to catch the exception
117
     * without loosing the current transaction).
118
     * <p>
119
     * NOTE : if <code>continueTx</code> is <code>true</code> and an exception is thrown, the
120
     * connection might be aborted. So you should notify the caller, e.g. propagate the exception so
121
     * that he can roll back the transaction.
122
     * </p>
123
     *
124
     * @param <T> type of return
125
     * @param <X> type of exception of <code>h</code>
126
     * @param ds the data source where h should be executed.
127
     * @param h the code to execute.
128
     * @param continueTx only relevant if already in a transaction : if <code>true</code> the
129
     *        handler will just be executed and the connection won't be modified (i.e. the existing
130
     *        transaction will neither be committed nor rolled back) ; if <code>false</code> a save
131
     *        point will be used.
132
     * @return what h returns.
133
     * @throws SQLException if a problem occurs.
134
     * @throws X if <code>h</code> throw it.
135
     */
136
    public static <T, X extends Exception> T executeAtomic(final SQLDataSource ds, final ConnectionHandlerNoSetup<T, X> h, final boolean continueTx) throws SQLException, X {
17 ilm 137
        return ds.useConnection(new ConnectionHandler<T, X>() {
138
 
139
            private Boolean autoCommit = null;
83 ilm 140
            private Savepoint savePoint = null;
17 ilm 141
 
142
            @Override
143
            public boolean canRestoreState() {
144
                return true;
145
            }
146
 
147
            @Override
148
            public void setup(Connection conn) throws SQLException {
149
                this.autoCommit = conn.getAutoCommit();
150
                if (this.autoCommit) {
151
                    conn.setAutoCommit(false);
83 ilm 152
                } else if (!continueTx) {
153
                    this.savePoint = conn.setSavepoint();
17 ilm 154
                }
155
            }
156
 
157
            @Override
158
            public T handle(final SQLDataSource ds) throws X, SQLException {
159
                return h.handle(ds);
160
            }
161
 
162
            @Override
163
            public void restoreState(Connection conn) throws SQLException {
164
                // can be null if getAutoCommit() failed, in that case nothing to do
83 ilm 165
                final boolean hasStoppedAutoCommit = Boolean.TRUE.equals(this.autoCommit);
166
                final boolean hasSavePoint = this.savePoint != null;
167
                // at most one is enough (otherwise change if/else below)
168
                assert !(hasStoppedAutoCommit && hasSavePoint) : "Begun a transaction and created a save point";
169
                if (hasStoppedAutoCommit || hasSavePoint) {
132 ilm 170
                    final boolean hasException = this.hasException();
17 ilm 171
                    try {
132 ilm 172
                        if (hasException) {
173
                            if (hasStoppedAutoCommit) {
174
                                conn.rollback();
175
                                conn.setAutoCommit(true);
176
                            } else {
177
                                conn.rollback(this.savePoint);
178
                            }
179
                        } else {
180
                            if (hasStoppedAutoCommit) {
181
                                conn.setAutoCommit(true);
182
                                // MS SQL cannot release save points
183
                                // http://technet.microsoft.com/en-us/library/ms378791.aspx
184
                            } else if (ds.getSystem() != SQLSystem.MSSQL) {
185
                                conn.releaseSavepoint(this.savePoint);
186
                            }
187
                        }
17 ilm 188
                    } catch (Exception e) {
132 ilm 189
                        final String msg;
190
                        if (hasException)
191
                            msg = "Couldn't " + (hasSavePoint ? "rollback save point" : "rollback");
83 ilm 192
                        else
132 ilm 193
                            msg = "Couldn't " + (hasSavePoint ? "release save point" : "commit");
194
                        throw new SQLException(msg, e);
17 ilm 195
                    }
196
                }
197
            }
198
        });
199
    }
200
 
201
    /**
202
     * If conn is in autocommit, unset it, try to execute f, if an exception is raised rollback
203
     * otherwise commit ; finally set autocommit. Otherwise just execute f as we assume the calling
204
     * method handles transactions.
205
     *
206
     * @param <T> type of factory
207
     *
208
     * @param conn the connection.
209
     * @param f will be executed.
210
     * @return what f returns.
211
     * @throws SQLException if a pb occurs.
212
     */
213
    public static <T> T executeAtomic(final Connection conn, final SQLFactory<T> f) throws SQLException {
214
        // create a transaction if we aren't in any, otherwise do nothing
215
        final boolean autoCommit = conn.getAutoCommit();
216
        final T res;
217
        if (autoCommit) {
218
            conn.setAutoCommit(false);
219
            try {
220
                res = f.create();
221
                conn.commit();
222
            } catch (SQLException e) {
223
                conn.rollback();
224
                throw e;
225
            } catch (RuntimeException e) {
226
                conn.rollback();
227
                throw e;
228
            } finally {
229
                conn.setAutoCommit(true);
230
            }
231
        } else {
232
            res = f.create();
233
        }
234
 
235
        return res;
236
    }
237
 
238
    /**
239
     * Creates a pseudo sequence with an arbitrary type (not just bigint as real sequences). These
240
     * statements create 2 functions : <code>next_<i>seqName</i>()</code> and
241
     * <code>reset_<i>seqName</i>()</code>.
242
     *
243
     * @param seqName the name of the sequence.
244
     * @param sqlType its SQL type, eg "decimal(16,8)".
245
     * @param minVal the starting value, eg "0.123".
246
     * @param inc the increment, eg "3.14".
247
     * @return the SQL statements.
248
     */
249
    public static List<String> createPostgreSQLSeq(String seqName, String sqlType, String minVal, String inc) {
250
        final List<String> res = new ArrayList<String>();
251
        final String genT = seqName + "_generator";
252
        res.add("DROP TABLE if exists " + genT);
253
        res.add("CREATE TABLE " + genT + " ( " + decl(new String[] { "minVal", "inc", "currentVal", "tmpVal" }, sqlType) + ")");
254
 
255
        String body = "UPDATE " + genT + " set tmpVal = currentVal, currentVal = currentVal + inc ;";
256
        body += "SELECT tmpVal from " + genT + ";";
257
        res.addAll(createFunction("next_" + seqName, sqlType, body));
258
 
259
        body = "update " + genT + " set currentVal = minVal ;";
260
        body += "select currentVal from " + genT + ";";
261
        res.addAll(createFunction("reset_" + seqName, sqlType, body));
262
 
263
        res.add("INSERT INTO " + genT + " values(" + minVal + ", (" + inc + ") )");
264
        res.add("SELECT " + "reset_" + seqName + "()");
265
 
266
        return res;
267
    }
268
 
269
    /**
270
     * A list of declaration.
271
     *
272
     * @param cols columns name, eg ["min", "inc"].
273
     * @param type SQL type, eg "int8".
274
     * @return declaration, eg "min int8, inc int8".
275
     */
276
    private static String decl(String[] cols, String type) {
277
        String res = "";
278
        for (String col : cols) {
279
            res += col + " " + type + ",";
280
        }
281
        // remove last ,
282
        return res.substring(0, res.length() - 1);
283
    }
284
 
285
    /**
286
     * Creates an SQL function (dropping it beforehand).
287
     *
288
     * @param name the name of the function.
289
     * @param type the SQL return type.
290
     * @param body the body of the function.
291
     * @return the SQL statements.
292
     */
293
    private static List<String> createFunction(String name, String type, String body) {
294
        final List<String> res = new ArrayList<String>();
295
 
296
        res.add("DROP FUNCTION if exists " + name + "()");
297
        String f = "CREATE FUNCTION " + name + "() RETURNS " + type + " AS $createFunction$ ";
298
        f += body;
299
        f += " $createFunction$ LANGUAGE SQL";
300
        res.add(f);
301
 
302
        return res;
303
    }
304
 
305
    static public final String SPLIT_DELIMITER = "$jdbcDelimiter$";
306
    static private final Pattern splitMySQLQueries = Pattern.compile(";\r?\n");
307
    static public final Pattern SPLIT_PATTERN = Pattern.compile(SPLIT_DELIMITER, Pattern.LITERAL);
308
 
309
    /**
310
     * Split a SQL script so that it can be executed. For MySQL the script is split at ';' for
311
     * others at {@link #SPLIT_DELIMITER}.
312
     *
313
     * @param sql the script to execute.
314
     * @param sysRoot where to execute.
315
     * @throws SQLException if an exception happens.
316
     */
317
    static public void executeScript(final String sql, final DBSystemRoot sysRoot) throws SQLException {
318
        // Bug 1: MySQL jdbc cannot execute what MySQL QueryBrowser can
319
        // ie before 5.1 you could execute a string with multiple CREATE TABLE,
320
        // but in 5.1 each execute must have exactly one query
321
        // Bug 2: MySQL does not have the concept of dollar quoted strings
322
        // so we have to help it and split the query (eg around trigger and functions)
323
        final SQLSystem sys = sysRoot.getServer().getSQLSystem();
324
        final Pattern p = sys == SQLSystem.MYSQL || sys == SQLSystem.MSSQL ? splitMySQLQueries : SPLIT_PATTERN;
325
        executeScript(sql, sysRoot, p);
326
    }
327
 
328
    static public void executeScript(final String sql, final DBSystemRoot sysRoot, final Pattern p) throws SQLException {
329
        try {
330
            for (final String s : p.split(sql)) {
331
                final String trimmed = s.trim();
332
                if (trimmed.length() > 0)
333
                    sysRoot.getDataSource().execute(trimmed, null);
334
            }
335
        } catch (final Exception e) {
336
            throw new SQLException("unable to execute " + sql, e);
337
        }
338
    }
67 ilm 339
 
340
    /**
341
     * Execute all queries at once if possible.
342
     *
343
     * @param sysRoot where to execute.
344
     * @param queries what to execute.
345
     * @param handlers how to process the result sets, items can be <code>null</code>.
346
     * @return the results of the handlers.
347
     * @throws SQLException if an error occur
348
     * @throws RTInterruptedException if the current thread is interrupted.
83 ilm 349
     * @see SQLSystem#isMultipleResultSetsSupported()
67 ilm 350
     */
351
    static public List<?> executeMultiple(final DBSystemRoot sysRoot, final List<String> queries, final List<? extends ResultSetHandler> handlers) throws SQLException, RTInterruptedException {
352
        final int size = handlers.size();
353
        if (queries.size() != size)
354
            throw new IllegalArgumentException("Size mismatch " + queries + " / " + handlers);
355
        final List<Object> results = new ArrayList<Object>(size);
356
 
73 ilm 357
        final SQLSystem system = sysRoot.getServer().getSQLSystem();
358
        if (system.isMultipleResultSetsSupported()) {
67 ilm 359
            final long timeMs = System.currentTimeMillis();
360
            final long time = System.nanoTime();
361
            final long afterCache = time;
362
 
363
            final StringBuilder sb = new StringBuilder(256 * size);
364
            for (final String q : queries) {
365
                sb.append(q);
366
                if (!q.trim().endsWith(";"))
367
                    sb.append(';');
368
                sb.append('\n');
369
            }
370
            final String query = sb.toString();
371
            sysRoot.getDataSource().useConnection(new ConnectionHandlerNoSetup<Object, SQLException>() {
372
                @Override
373
                public Object handle(SQLDataSource ds) throws SQLException {
374
                    final Connection conn = ds.getConnection();
73 ilm 375
 
376
                    if (system == SQLSystem.MYSQL) {
377
                        final ConnectionProperties connectionProperties = (ConnectionProperties) ((DelegatingConnection) conn).getInnermostDelegate();
378
                        if (!connectionProperties.getAllowMultiQueries()) {
379
                            throw new IllegalStateException("Multi queries not allowed and the setting can only be set before connecting");
380
                        }
381
                    }
382
 
67 ilm 383
                    final long afterQueryInfo = System.nanoTime();
384
                    final long afterExecute, afterHandle;
142 ilm 385
                    int count = 0;
174 ilm 386
                    try (final Statement stmt = conn.createStatement()){
67 ilm 387
                        if (Thread.currentThread().isInterrupted())
388
                            throw new RTInterruptedException("Interrupted before executing : " + query);
389
                        stmt.execute(query);
390
                        afterExecute = System.nanoTime();
391
                        for (final ResultSetHandler h : handlers) {
392
                            if (Thread.currentThread().isInterrupted())
393
                                throw new RTInterruptedException("Interrupted while handling results : " + query);
142 ilm 394
                            if (h == null) {
395
                                results.add(null);
396
                            } else {
397
                                final ResultSet resultSet = stmt.getResultSet();
398
                                results.add(h.handle(resultSet));
399
                                count += SQLResultSet.getRowProcessedCount(resultSet);
400
                            }
67 ilm 401
                            stmt.getMoreResults();
402
                        }
403
                        afterHandle = System.nanoTime();
404
                    }
142 ilm 405
                    SQLRequestLog.log(query, "executeMultiple", conn, timeMs, time, afterCache, afterQueryInfo, afterExecute, afterHandle, System.nanoTime(), count);
67 ilm 406
                    return null;
407
                }
408
            });
409
        } else {
410
            // use the same connection to allow some insert/update followed by a select
411
            sysRoot.getDataSource().useConnection(new ConnectionHandlerNoSetup<Object, SQLException>() {
412
                @Override
413
                public Object handle(SQLDataSource ds) throws SQLException {
414
                    for (int i = 0; i < size; i++) {
415
                        final ResultSetHandler rsh = handlers.get(i);
416
                        // since the other if clause cannot support cache and this clause doesn't
417
                        // have any table to fire, don't use cache
418
                        results.add(sysRoot.getDataSource().execute(queries.get(i), rsh == null ? null : new IResultSetHandler(rsh, false)));
419
                    }
420
                    return null;
421
                }
422
            });
423
        }
424
        return results;
425
    }
17 ilm 426
}