Coverage Report - com.jcabi.jdbc.JdbcSession
 
Classes in this File Line Coverage Branch Coverage Complexity
JdbcSession
85%
68/80
17%
10/56
1.812
JdbcSession$AjcClosure1
100%
1/1
N/A
1.812
JdbcSession$AjcClosure11
100%
1/1
N/A
1.812
JdbcSession$AjcClosure13
100%
1/1
N/A
1.812
JdbcSession$AjcClosure15
100%
1/1
N/A
1.812
JdbcSession$AjcClosure17
100%
1/1
N/A
1.812
JdbcSession$AjcClosure19
100%
1/1
N/A
1.812
JdbcSession$AjcClosure3
100%
1/1
N/A
1.812
JdbcSession$AjcClosure5
100%
1/1
N/A
1.812
JdbcSession$AjcClosure7
100%
1/1
N/A
1.812
JdbcSession$AjcClosure9
100%
1/1
N/A
1.812
 
 1  10
 /**
 2  
  * Copyright (c) 2012-2015, 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 com.jcabi.aspects.Loggable;
 33  
 import java.sql.Connection;
 34  
 import java.sql.PreparedStatement;
 35  
 import java.sql.ResultSet;
 36  
 import java.sql.SQLException;
 37  
 import java.util.Collection;
 38  
 import java.util.concurrent.CopyOnWriteArrayList;
 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:
 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.
 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):
 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:
 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:
 98  
  *
 99  
  * <pre> new JdbcSession(&#47;* H2 Database data source *&#47;)
 100  
  *   .autocommit(false)
 101  
  *   .sql("SHUTDOWN COMPACT")
 102  
  *   .execute();</pre>
 103  
  *
 104  
  * <p>This class is thread-safe.
 105  
  *
 106  
  * @author Yegor Bugayenko (yegor@teamed.io)
 107  
  * @version $Id: b56096f367c458f25b93276be046195c60bf0849 $
 108  
  * @since 0.1.8
 109  
  */
 110  122
 @ToString
 111  0
 @EqualsAndHashCode(of = { "source", "connection", "args", "auto", "query" })
 112  
 @Loggable(Loggable.DEBUG)
 113  
 @SuppressWarnings({ "PMD.TooManyMethods", "PMD.CloseResource" })
 114  
 public final class JdbcSession {
 115  
 
 116  
     /**
 117  
      * JDBC DataSource to get connections from.
 118  
      */
 119  
     private final transient DataSource source;
 120  
 
 121  
     /**
 122  
      * Arguments.
 123  
      */
 124  
     private final transient Collection<Object> args;
 125  
 
 126  
     /**
 127  
      * Arguments.
 128  
      * @since 0.13
 129  
      */
 130  
     private final transient Collection<Preparation> preparations;
 131  
 
 132  
     /**
 133  
      * Connection currently open.
 134  
      */
 135  
     private final transient AtomicReference<Connection> connection;
 136  
 
 137  
     /**
 138  
      * Shall we close/autocommit automatically?
 139  
      */
 140  
     private transient boolean auto;
 141  
 
 142  
     /**
 143  
      * The query to use.
 144  
      */
 145  
     private transient String query;
 146  
 
 147  
     /**
 148  
      * Public ctor.
 149  
      * @param src Data source
 150  
      */
 151  29
     public JdbcSession(final DataSource src) {
 152  29
         this.args = new CopyOnWriteArrayList<Object>();
 153  29
         this.preparations = new CopyOnWriteArrayList<Preparation>();
 154  29
         this.connection = new AtomicReference<Connection>();
 155  29
         this.auto = true;
 156  29
         this.source = src;
 157  29
         this.preparations.add(new PrepareArgs(this.args));
 158  29
     }
 159  
 
 160  
     /**
 161  
      * Public ctor.
 162  
      * @param cnx Connection to use
 163  
      * @since 0.10
 164  
      */
 165  
     public JdbcSession(final Connection cnx) {
 166  0
         this(new StaticSource(cnx));
 167  0
     }
 168  
 
 169  
     /**
 170  
      * Use this SQL query (with optional parameters inside).
 171  
      *
 172  
      * <p>The query will be used in {@link PreparedStatement}, that's why
 173  
      * you can use the same formatting as there. Arguments shall be marked
 174  
      * as {@code "?"} (question marks). For example:
 175  
      *
 176  
      * <pre> String name = new JdbcSession(source)
 177  
      *   .sql("INSERT INTO foo (id, name) VALUES (?, ?)")
 178  
      *   .set(556677)
 179  
      *   .set("Jeffrey Lebowski")
 180  
      *   .insert(Outcome.VOID);</pre>
 181  
      *
 182  
      * @param sql The SQL query to use
 183  
      * @return This object
 184  
      */
 185  
     public JdbcSession sql(final String sql) {
 186  72
         synchronized (this.args) {
 187  36
             this.query = sql;
 188  36
         }
 189  36
         return this;
 190  
     }
 191  
 
 192  
     /**
 193  
      * Shall we auto-commit?
 194  
      *
 195  
      * <p>By default this flag is set to TRUE, which means that methods
 196  
      * {@link #insert(Outcome)}, {@link #execute()}, and
 197  
      * {@link #select(Outcome)} will
 198  
      * call {@link Connection#commit()} after
 199  
      * their successful execution.
 200  
      *
 201  
      * @param autocommit Shall we?
 202  
      * @return This object
 203  
      */
 204  
     public JdbcSession autocommit(final boolean autocommit) {
 205  10
         synchronized (this.args) {
 206  5
             this.auto = autocommit;
 207  5
         }
 208  5
         return this;
 209  
     }
 210  
 
 211  
     /**
 212  
      * Set new parameter for the query.
 213  
      *
 214  
      * <p>The following types are supported: {@link Boolean},
 215  
      * {@link java.sql.Date},
 216  
      * {@link Utc}, {@link Long}, {@link Integer}. All other types will be
 217  
      * converted to {@link String} using their {@code toString()} methods.
 218  
      *
 219  
      * @param value The value to add
 220  
      * @return This object
 221  
      */
 222  
     public JdbcSession set(final Object value) {
 223  18
         this.args.add(value);
 224  9
         return this;
 225  
     }
 226  
 
 227  
     /**
 228  
      * Run this preparation before executing the statement.
 229  
      * @param prp Preparation
 230  
      * @return This object
 231  
      * @since 0.13
 232  
      */
 233  
     public JdbcSession prepare(final Preparation prp) {
 234  2
         synchronized (this.args) {
 235  1
             this.preparations.add(prp);
 236  1
         }
 237  1
         return this;
 238  
     }
 239  
 
 240  
     /**
 241  
      * Clear all pre-set parameters (args, preparations, etc).
 242  
      * @return This object
 243  
      * @since 0.13
 244  
      */
 245  
     public JdbcSession clear() {
 246  78
         synchronized (this.args) {
 247  39
             this.args.clear();
 248  39
             this.preparations.clear();
 249  39
             this.preparations.add(new PrepareArgs(this.args));
 250  39
         }
 251  39
         return this;
 252  
     }
 253  
 
 254  
     /**
 255  
      * Commit the transaction (calls {@link Connection#commit()} and then
 256  
      * {@link Connection#close()}).
 257  
      * @throws SQLException If fails to do the SQL operation
 258  
      */
 259  
     public void commit() throws SQLException {
 260  10
         final Connection conn = this.connection.get();
 261  5
         if (conn == null) {
 262  0
             throw new IllegalStateException(
 263  
                 "connection is not open, can't commit"
 264  
             );
 265  
         }
 266  5
         conn.commit();
 267  5
         this.disconnect();
 268  5
     }
 269  
 
 270  
     /**
 271  
      * Make SQL {@code INSERT} request.
 272  
      *
 273  
      * <p>{@link Outcome} will receive
 274  
      * a {@link ResultSet} of generated keys.
 275  
      *
 276  
      * <p>JDBC connection is opened and, optionally, closed by this method.
 277  
      *
 278  
      * @param outcome The outcome of the operation
 279  
      * @return The result
 280  
      * @param <T> Type of response
 281  
      * @throws SQLException If fails
 282  
      */
 283  
     public <T> T insert(final Outcome<T> outcome)
 284  
         throws SQLException {
 285  2
         return this.run(
 286  
             outcome,
 287  
             new Connect.WithKeys(this.query),
 288  
             Request.EXECUTE
 289  
         );
 290  
     }
 291  
 
 292  
     /**
 293  
      * Make SQL {@code UPDATE} request.
 294  
      *
 295  
      * <p>JDBC connection is opened and, optionally, closed by this method.
 296  
      *
 297  
      * @param <T> Type of result expected
 298  
      * @param outcome Outcome of the operation
 299  
      * @return This object
 300  
      * @throws SQLException If fails
 301  
      */
 302  
     public <T> T update(final Outcome<T> outcome)
 303  
         throws SQLException {
 304  2
         return this.run(
 305  
             outcome,
 306  
             new Connect.WithKeys(this.query),
 307  
             Request.EXECUTE_UPDATE
 308  
         );
 309  
     }
 310  
 
 311  
     /**
 312  
      * Make SQL request expecting no response from the server.
 313  
      *
 314  
      * <p>This method should be used for schema manipulation statements,
 315  
      * like CREATE TABLE, CREATE INDEX, DROP COLUMN, etc. and server-side
 316  
      * instructions that return no data back. Main difference between this
 317  
      * one and {@code #execute()} is that the later requests JDBC to return
 318  
      * generated keys. When SQL server doesn't return any keys this may
 319  
      * cause runtime exceptions in JDBC.
 320  
      *
 321  
      * <p>JDBC connection is opened and, optionally, closed by this method.
 322  
      *
 323  
      * @return This object
 324  
      * @throws SQLException If fails
 325  
      * @since 0.9
 326  
      */
 327  
     public JdbcSession execute() throws SQLException {
 328  63
         this.run(
 329  
             Outcome.VOID,
 330  
             new Connect.Plain(this.query),
 331  
             Request.EXECUTE
 332  
         );
 333  32
         return this;
 334  
     }
 335  
 
 336  
     /**
 337  
      * Make SQL {@code SELECT} request.
 338  
      *
 339  
      * <p>JDBC connection is opened and, optionally, closed by this method.
 340  
      *
 341  
      * @param outcome The outcome of the operaton
 342  
      * @return The result
 343  
      * @param <T> Type of response
 344  
      * @throws SQLException If fails
 345  
      */
 346  
     public <T> T select(final Outcome<T> outcome)
 347  
         throws SQLException {
 348  10
         return this.run(
 349  
             outcome,
 350  
             new Connect.Plain(this.query),
 351  
             Request.EXECUTE_QUERY
 352  
         );
 353  
     }
 354  
 
 355  
     /**
 356  
      * Run with this outcome, and this fetcher.
 357  
      * @param outcome The outcome of the operation
 358  
      * @param connect Connect
 359  
      * @param request Request
 360  
      * @return The result
 361  
      * @param <T> Type of response
 362  
      * @throws SQLException If fails
 363  
      * @checkstyle ExecutableStatementCount (100 lines)
 364  
      */
 365  
     private <T> T run(final Outcome<T> outcome,
 366  
         final Connect connect, final Request request)
 367  
         throws SQLException {
 368  39
         if (this.query == null) {
 369  0
             throw new IllegalStateException("call #sql() first");
 370  
         }
 371  39
         final Connection conn = this.connect();
 372  
         T result;
 373  
         try {
 374  39
             conn.setAutoCommit(this.auto);
 375  39
             final PreparedStatement stmt = connect.open(conn);
 376  
             try {
 377  39
                 this.configure(stmt);
 378  39
                 final ResultSet rset = request.fetch(stmt);
 379  
                 // @checkstyle NestedTryDepth (5 lines)
 380  
                 try {
 381  39
                     result = outcome.handle(rset, stmt);
 382  
                 } finally {
 383  39
                     if (rset != null) {
 384  39
                         rset.close();
 385  
                     }
 386  
                 }
 387  
             } finally {
 388  39
                 stmt.close();
 389  39
             }
 390  0
         } catch (final SQLException ex) {
 391  0
             if (!this.auto) {
 392  0
                 conn.rollback();
 393  0
                 this.disconnect();
 394  
             }
 395  0
             throw new SQLException(ex);
 396  
         } finally {
 397  39
             if (this.auto) {
 398  26
                 this.disconnect();
 399  
             }
 400  39
             this.clear();
 401  39
         }
 402  39
         return result;
 403  
     }
 404  
 
 405  
     /**
 406  
      * Open connection and cache it locally in the class.
 407  
      * @return Connection to use
 408  
      * @throws SQLException If fails
 409  
      */
 410  
     private Connection connect() throws SQLException {
 411  39
         synchronized (this.args) {
 412  39
             if (this.connection.get() == null) {
 413  31
                 this.connection.set(this.source.getConnection());
 414  
             }
 415  39
             return this.connection.get();
 416  0
         }
 417  
     }
 418  
 
 419  
     /**
 420  
      * Close connection if it's open (runtime exception otherwise).
 421  
      * @throws SQLException If fails to do the SQL operation
 422  
      */
 423  
     private void disconnect() throws SQLException {
 424  31
         final Connection conn = this.connection.getAndSet(null);
 425  31
         if (conn == null) {
 426  0
             throw new IllegalStateException(
 427  
                 "connection is not open, can't close"
 428  
             );
 429  
         }
 430  31
         conn.close();
 431  31
     }
 432  
 
 433  
     /**
 434  
      * Configure the statement.
 435  
      * @param stmt Statement
 436  
      * @throws SQLException If fails
 437  
      */
 438  
     private void configure(final PreparedStatement stmt) throws SQLException {
 439  39
         for (final Preparation prep : this.preparations) {
 440  40
             prep.prepare(stmt);
 441  39
         }
 442  39
     }
 443  
 
 444  
 }