1 /*
2 * Copyright (c) 2012-2023, jcabi.com
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met: 1) Redistributions of source code must retain the above
8 * copyright notice, this list of conditions and the following
9 * disclaimer. 2) Redistributions in binary form must reproduce the above
10 * copyright notice, this list of conditions and the following
11 * disclaimer in the documentation and/or other materials provided
12 * with the distribution. 3) Neither the name of the jcabi.com nor
13 * the names of its contributors may be used to endorse or promote
14 * products derived from this software without specific prior written
15 * permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28 * OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 package com.jcabi.jdbc;
31
32 import java.sql.Connection;
33 import java.sql.PreparedStatement;
34 import java.sql.ResultSet;
35 import java.sql.SQLException;
36 import java.util.Collection;
37 import java.util.LinkedList;
38 import java.util.UUID;
39 import java.util.concurrent.atomic.AtomicReference;
40 import javax.sql.DataSource;
41 import lombok.EqualsAndHashCode;
42 import lombok.ToString;
43
44 /**
45 * Universal JDBC wrapper.
46 *
47 * <p>Execute a simple SQL query over a JDBC data source:</p>
48 *
49 * <pre> String name = new JdbcSession(source)
50 * .sql("SELECT name FROM foo WHERE id = ?")
51 * .set(123)
52 * .select(
53 * new Outcome<String>() {
54 * @Override
55 * public String handle(final ResultSet rset) throws SQLException {
56 * rset.next();
57 * return rset.getString(1);
58 * }
59 * }
60 * );</pre>
61 *
62 * <p>There are a number of convenient pre-defined outcomes, like
63 * {@link Outcome#VOID}, {@link Outcome#NOT_EMPTY}, {@link Outcome#UPDATE_COUNT}
64 * {@link SingleOutcome}, etc.</p>
65 *
66 * <p>Methods {@link #insert(Outcome)},
67 * {@link #update(Outcome)},
68 * {@link #execute()}, and
69 * {@link #select(Outcome)} clean the list of arguments pre-set by
70 * {@link #set(Object)}. The class can be used for a complex transaction, when
71 * it's necessary to perform a number of SQL statements in a group. For
72 * example, the following construct will execute two SQL queries, in a single
73 * transaction and will "commit" at the end (or rollback the entire transaction
74 * in case of any error in between):</p>
75 *
76 * <pre> new JdbcSession(source)
77 * .autocommit(false)
78 * .sql("START TRANSACTION")
79 * .execute()
80 * .sql("DELETE FROM foo WHERE id = ?")
81 * .set(444)
82 * .execute()
83 * .set(555)
84 * .execute()
85 * .commit();</pre>
86 *
87 * <p>The following SQL queries will be sent to the database:</p>
88 *
89 * <pre> START TRANSACTION;
90 * DELETE FROM foo WHERE id = 444;
91 * DELETE FROM foo WHERE id = 555;
92 * COMMIT;</pre>
93 *
94 * <p>{@link #autocommit(boolean)} (with {@code false} as an argument)
95 * can be used when it's necessary to execute
96 * a statement and leave the connection open. For example when shutting down
97 * the database through SQL:</p>
98 *
99 * <pre> new JdbcSession(/* H2 Database data source */)
100 * .autocommit(false)
101 * .sql("SHUTDOWN COMPACT")
102 * .execute();</pre>
103 *
104 * <p><b>IMPORTANT:</b></p>
105 *
106 * <p>If you rely on one specific {@link Connection} instance, be careful if
107 * you are using it in more places, especially if more references of this class
108 * use it - one of those references might close the connection if you forget
109 * to call {@link JdbcSession#autocommit(boolean)} with {@code false} as an argument,
110 * for example:</p>
111 *
112 * <pre> Connection connection = [...];
113 * DataSource ds = new StaticSource(connection);
114 * new JdbcSession(ds)
115 * .sql("SQL STATEMENT")
116 * .execute();
117 * new JdbcSession(ds)
118 * .sql("SQL STATEMENT 2")
119 * .execute();</pre>
120
121 * <p>The above example will <b>fail</b> because the first JdbcSession closes
122 * the connection, and the next one tries to work with it closed. In order to
123 * not have this failure, the first session has to call
124 * {@link #autocommit(boolean)} with {@code false} as an argument, like this:</p>
125 *
126 * <pre> Connection connection = [...];
127 * DataSource ds = new StaticSource(connection);
128 * new JdbcSession(ds)
129 * .autocommit(false)
130 * .sql("SQL STATEMENT")
131 * .execute();
132 * new JdbcSession(ds)
133 * .sql("SQL STATEMENT 2")
134 * .execute();</pre>
135 *
136 * <p>This class is thread-safe.</p>
137 *
138 * @since 0.1.8
139 * @todo #51:30min Refactor this class to avoid too much coupling.
140 * For instance, CRUD operations could be performed by another class.
141 * Don't forget to remove the suppressions that become obsolete afterwards.
142 */
143 @ToString
144 @EqualsAndHashCode(of = { "source", "connection", "args", "auto", "query" })
145 @SuppressWarnings({ "PMD.TooManyMethods", "PMD.CloseResource" })
146 public final class JdbcSession {
147
148 /**
149 * JDBC DataSource to get connections from.
150 */
151 private final transient DataSource source;
152
153 /**
154 * Arguments.
155 *
156 * <p>Every time this attribute is modified, we must synchronize, because
157 * a non-thread-safe {@link LinkedList} is assigned to it.</p>
158 */
159 private final transient Collection<Object> args;
160
161 /**
162 * Arguments.
163 *
164 * <p>Every time this attribute is modified, we must synchronize, because
165 * a non-thread-safe {@link LinkedList} is assigned to it.</p>
166 *
167 * @since 0.13
168 */
169 private final transient Collection<Preparation> preparations;
170
171 /**
172 * Connection currently open.
173 */
174 private final transient AtomicReference<Connection> connection;
175
176 /**
177 * Shall we close/autocommit automatically?
178 */
179 private transient boolean auto;
180
181 /**
182 * The query to use.
183 */
184 private transient String query;
185
186 /**
187 * Public ctor.
188 *
189 * <p>If all you have is a {@link Connection}, wrap it inside our
190 * {@link StaticSource}, but make sure you understand the autocommit
191 * mechanism we have in place here. Read the class' javadoc (especially the
192 * last paragraph, marked with <b>IMPORTANT</b>).</p>
193 *
194 * @param src Data source
195 */
196 @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
197 public JdbcSession(final DataSource src) {
198 this.args = new LinkedList<>();
199 this.preparations = new LinkedList<>();
200 this.connection = new AtomicReference<>();
201 this.auto = true;
202 this.source = src;
203 this.preparations.add(new PrepareArgs(this.args));
204 }
205
206 /**
207 * Use this SQL query (with optional parameters inside).
208 *
209 * <p>The query will be used in {@link PreparedStatement}, that's why
210 * you can use the same formatting as there. Arguments shall be marked
211 * as {@code "?"} (question marks). For example:</p>
212 *
213 * <pre> String name = new JdbcSession(source)
214 * .sql("INSERT INTO foo (id, name) VALUES (?, ?)")
215 * .set(556677)
216 * .set("Jeffrey Lebowski")
217 * .insert(Outcome.VOID);</pre>
218 *
219 * @param sql The SQL query to use
220 * @return This object
221 */
222 public JdbcSession sql(final String sql) {
223 synchronized (this.args) {
224 this.query = sql;
225 }
226 return this;
227 }
228
229 /**
230 * Shall we auto-commit?
231 *
232 * <p>By default this flag is set to TRUE, which means that methods
233 * {@link #insert(Outcome)}, {@link #execute()}, and
234 * {@link #select(Outcome)} will
235 * call {@link Connection#commit()} after
236 * their successful execution.</p>
237 *
238 * @param autocommit Shall we?
239 * @return This object
240 */
241 public JdbcSession autocommit(final boolean autocommit) {
242 synchronized (this.args) {
243 this.auto = autocommit;
244 }
245 return this;
246 }
247
248 /**
249 * Set new parameter for the query.
250 *
251 * <p>The following types are supported: {@link Boolean},
252 * {@link java.sql.Date},
253 * {@link Utc}, {@link Long}, {@link Float}, byte[], {@link Integer}, {@link UUID}.
254 * All other types will be converted to {@link String} using
255 * their {@code toString()} methods.</p>
256 *
257 * @param value The value to add
258 * @return This object
259 */
260 public JdbcSession set(final Object value) {
261 synchronized (this.args) {
262 this.args.add(value);
263 }
264 return this;
265 }
266
267 /**
268 * Run this preparation before executing the statement.
269 *
270 * @param prp Preparation
271 * @return This object
272 * @since 0.13
273 */
274 public JdbcSession prepare(final Preparation prp) {
275 synchronized (this.args) {
276 this.preparations.add(prp);
277 }
278 return this;
279 }
280
281 /**
282 * Clear all pre-set parameters (args, preparations, etc).
283 *
284 * @return This object
285 * @since 0.13
286 */
287 public JdbcSession clear() {
288 synchronized (this.args) {
289 this.args.clear();
290 this.preparations.clear();
291 this.preparations.add(new PrepareArgs(this.args));
292 }
293 return this;
294 }
295
296 /**
297 * Commit the transaction (calls {@link Connection#commit()} and then
298 * {@link Connection#close()}).
299 *
300 * @throws SQLException If fails to do the SQL operation
301 */
302 public void commit() throws SQLException {
303 final Connection conn = this.connection.get();
304 if (conn == null) {
305 throw new IllegalStateException(
306 "Connection is not open, can't commit"
307 );
308 }
309 conn.commit();
310 this.disconnect();
311 }
312
313 /**
314 * Rollback the transaction (calls {@link Connection#rollback()} and then
315 * {@link Connection#close()}).
316 *
317 * @throws SQLException If fails to do the SQL operation
318 */
319 public void rollback() throws SQLException {
320 final Connection conn = this.connection.get();
321 if (conn == null) {
322 throw new IllegalStateException(
323 "Connection is not open, can't rollback"
324 );
325 }
326 conn.rollback();
327 this.disconnect();
328 }
329
330 /**
331 * Make SQL {@code INSERT} request.
332 *
333 * <p>{@link Outcome} will receive
334 * a {@link ResultSet} of generated keys.</p>
335 *
336 * <p>JDBC connection is opened and, optionally, closed by this method.</p>
337 *
338 * @param outcome The outcome of the operation
339 * @param <T> Type of response
340 * @return The result
341 * @throws SQLException If fails
342 */
343 public <T> T insert(final Outcome<T> outcome)
344 throws SQLException {
345 return this.run(
346 outcome,
347 new Connect.WithKeys(this.query),
348 Request.EXECUTE
349 );
350 }
351
352 /**
353 * Make SQL {@code UPDATE} request.
354 *
355 * <p>JDBC connection is opened and, optionally, closed by this method.</p>
356 *
357 * @param outcome Outcome of the operation
358 * @param <T> Type of result expected
359 * @return This object
360 * @throws SQLException If fails
361 */
362 public <T> T update(final Outcome<T> outcome)
363 throws SQLException {
364 return this.run(
365 outcome,
366 new Connect.WithKeys(this.query),
367 Request.EXECUTE_UPDATE
368 );
369 }
370
371 /**
372 * Call an SQL stored procedure.
373 *
374 * <p>JDBC connection is opened and, optionally, committed by this
375 * method, depending on the <b>autocommit</b> class attribute:
376 * if it's value is true, the connection will be committed after
377 * this call.</p>
378 *
379 * @param outcome Outcome of the operation
380 * @param <T> Type of result expected
381 * @return Result of type T
382 * @throws SQLException If fails
383 */
384 public <T> T call(final Outcome<T> outcome)
385 throws SQLException {
386 return this.run(
387 outcome, new Connect.Call(this.query), Request.EXECUTE_UPDATE
388 );
389 }
390
391 /**
392 * Make SQL request expecting no response from the server.
393 *
394 * <p>This method should be used for schema manipulation statements,
395 * like CREATE TABLE, CREATE INDEX, DROP COLUMN, etc. and server-side
396 * instructions that return no data back. Main difference between this
397 * one and {@code #execute()} is that the later requests JDBC to return
398 * generated keys. When SQL server doesn't return any keys this may
399 * cause runtime exceptions in JDBC.</p>
400 *
401 * <p>JDBC connection is opened and, optionally, closed by this method.</p>
402 *
403 * @return This object
404 * @throws SQLException If fails
405 * @since 0.9
406 */
407 public JdbcSession execute() throws SQLException {
408 final String vendor;
409 try (Connection conn = this.source.getConnection()) {
410 vendor = conn.getMetaData().getDatabaseProductName();
411 }
412 final Connect connect;
413 if (vendor.equalsIgnoreCase("mysql")) {
414 connect = new Connect.WithKeys(this.query);
415 } else {
416 connect = new Connect.Plain(this.query);
417 }
418 this.run(Outcome.VOID, connect, Request.EXECUTE);
419 return this;
420 }
421
422 /**
423 * Make SQL {@code SELECT} request.
424 *
425 * <p>JDBC connection is opened and, optionally, closed by this method.</p>
426 *
427 * @param outcome The outcome of the operation
428 * @param <T> Type of response
429 * @return The result
430 * @throws SQLException If fails
431 */
432 public <T> T select(final Outcome<T> outcome)
433 throws SQLException {
434 return this.run(
435 outcome,
436 new Connect.Plain(this.query),
437 Request.EXECUTE_QUERY
438 );
439 }
440
441 /**
442 * Run with this outcome, and this fetcher.
443 *
444 * @param outcome The outcome of the operation
445 * @param connect Connect
446 * @param request Request
447 * @param <T> Type of response
448 * @return The result
449 * @throws SQLException If fails
450 * @checkstyle ExecutableStatementCount (100 lines)
451 */
452 private <T> T run(final Outcome<T> outcome,
453 final Connect connect, final Request request)
454 throws SQLException {
455 if (this.query == null) {
456 throw new IllegalStateException("Call #sql() first");
457 }
458 final Connection conn = this.connect();
459 conn.setAutoCommit(this.auto);
460 try {
461 return this.fetch(outcome, request, connect.open(conn));
462 } catch (final SQLException ex) {
463 this.rollbackOnFailure(conn, ex);
464 throw new SQLException(ex);
465 } finally {
466 if (this.auto) {
467 this.disconnect();
468 }
469 this.clear();
470 }
471 }
472
473 /**
474 * Fetch the result.
475 * @param outcome The outcome of the operation
476 * @param request Request
477 * @param stmt Statement
478 * @param <T> Type of response
479 * @return The result
480 * @throws SQLException If fails
481 */
482 private <T> T fetch(final Outcome<T> outcome,
483 final Request request, final PreparedStatement stmt) throws SQLException {
484 final T result;
485 try {
486 this.configure(stmt);
487 final ResultSet rset = request.fetch(stmt);
488 // @checkstyle NestedTryDepth (5 lines)
489 try {
490 result = outcome.handle(rset, stmt);
491 } finally {
492 if (rset != null) {
493 rset.close();
494 }
495 }
496 } finally {
497 stmt.close();
498 }
499 return result;
500 }
501
502 /**
503 * Rollback in case of error.
504 * @param conn The connection
505 * @param failure The original failure
506 * @throws SQLException If fails
507 */
508 @SuppressWarnings("PMD.PreserveStackTrace")
509 private void rollbackOnFailure(final Connection conn, final SQLException failure)
510 throws SQLException {
511 if (!this.auto) {
512 try {
513 conn.rollback();
514 this.disconnect();
515 } catch (final SQLException exc) {
516 throw new SQLException(
517 String.format(
518 "Failed to rollback after failure: %s",
519 exc.getMessage()
520 ),
521 failure
522 );
523 }
524 }
525 }
526
527 /**
528 * Open connection and cache it locally in the class.
529 *
530 * @return Connection to use
531 * @throws SQLException If fails
532 */
533 private Connection connect() throws SQLException {
534 synchronized (this.args) {
535 if (this.connection.get() == null) {
536 this.connection.set(this.source.getConnection());
537 }
538 return this.connection.get();
539 }
540 }
541
542 /**
543 * Close connection if it's open (runtime exception otherwise).
544 *
545 * @throws SQLException If fails to do the SQL operation
546 */
547 private void disconnect() throws SQLException {
548 final Connection conn = this.connection.getAndSet(null);
549 if (conn == null) {
550 throw new IllegalStateException(
551 "Connection is not open, can't close"
552 );
553 }
554 conn.close();
555 }
556
557 /**
558 * Configure the statement.
559 *
560 * @param stmt Statement
561 * @throws SQLException If fails
562 */
563 private void configure(final PreparedStatement stmt) throws SQLException {
564 for (final Preparation prep : this.preparations) {
565 prep.prepare(stmt);
566 }
567 }
568
569 }