View Javadoc
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&lt;String&gt;() {
54   *       &#64;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(&#47;* H2 Database data source *&#47;)
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 }