001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * docs/licenses/cddl.txt
011 * or http://www.opensource.org/licenses/cddl1.php.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * docs/licenses/cddl.txt.  If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 *      Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 *      Copyright 2010-2016 UnboundID Corp.
026 */
027package com.unboundid.directory.sdk.sync.types;
028
029import static com.unboundid.directory.sdk.sync.util.ScriptUtils.*;
030
031import java.sql.Blob;
032import java.sql.CallableStatement;
033import java.sql.Clob;
034import java.sql.Connection;
035import java.sql.PreparedStatement;
036import java.sql.ResultSet;
037import java.sql.ResultSetMetaData;
038import java.sql.SQLException;
039import java.sql.Statement;
040import java.util.Date;
041import java.util.Timer;
042import java.util.TimerTask;
043import com.unboundid.ldap.sdk.Entry;
044import com.unboundid.util.StaticUtils;
045
046/**
047 * This class wraps an open JDBC Connection and provides controlled access to it
048 * via a handful of delegating methods. The underlying connection is always part
049 * of a new transaction. The Synchronization Server will always automatically
050 * commit or rollback the transaction after a method is finished with it,
051 * depending on if the method returned successfully or threw an exception.
052 * <p>
053 * There are also facilities for setting a timeout, after
054 * which the TransactionContext will automatically become invalidated, and
055 * subsequent operations on it will throw a {@link RuntimeException}.
056 * <p>
057 */
058public final class TransactionContext
059{
060
061  // The underlying JDBC Connection (this is actually a DBCP delegating
062  // connection)
063  private final Connection connection;
064
065  // Timer used to invalidate TransactionContexts
066  private static final Timer timer = new Timer(true);
067
068  // The timer task local to this instance
069  private TimerTask timeOutTask;
070
071  // The number of millis after which to timeout
072  private long timeOutMs;
073
074  // Whether this context has timed out
075  private volatile boolean isTimedOut = false;
076
077  // Whether this context has been closed
078  private volatile boolean isClosed = false;
079
080  /**
081   * Constructor. The connection instance may not be null.
082   * @param connection
083   *          a fresh JDBC Connection from the pool
084   */
085  public TransactionContext(final Connection connection)
086  {
087    if(connection == null)
088    {
089      throw new NullPointerException("Connection is null.");
090    }
091    this.connection = connection;
092
093    try
094    {
095      // Start a fresh transaction
096      this.connection.commit();
097    }
098    catch(SQLException e)
099    {
100      throw new IllegalStateException(
101              "Could not initialize TransactionContext:" +
102                                  StaticUtils.getExceptionMessage(e));
103    }
104  }
105
106  /**
107   * Executes the given SQL query and converts the results into a raw LDAP
108   * {@link Entry}, where the attribute names are the raw column names from the
109   * result of the query. The <code>rdnColumn</code> parameter is the column
110   * name
111   * to use as the RDN attribute for the entry. For example if the rdnColumn is
112   * "accountID", then the DN of the returned Entry will be
113   * "accountID={accountID}".
114   * @param sql
115   *          the SQL query to execute
116   * @param rdnColumn
117   *          the column name in the result set which is to be used for
118   *          the DN of the returned {@link Entry}
119   * @return the resulting LDAP Entry
120   * @throws SQLException
121   *           if a database access error occurs or the query matches more than
122   *           one row
123   */
124  public synchronized Entry searchToRawEntry(final String sql,
125          final String rdnColumn)
126          throws SQLException
127  {
128    checkStatus();
129    PreparedStatement stmt = connection.prepareStatement(sql);
130    return searchToRawEntry(stmt, rdnColumn);
131  }
132
133  /**
134   * Executes the given {@link PreparedStatement} and converts the results into
135   * a raw LDAP {@link Entry}, where the attribute names are the raw column
136   * names from the
137   * result of the query. The <code>rdnColumn</code> parameter is the column
138   * name
139   * to use as the RDN attribute for the entry. For example if the rdnColumn is
140   * "accountID", then the DN of the returned Entry will be
141   * "accountID={accountID}".
142   * @param statement
143   *          the PreparedStatement to execute
144   * @param rdnColumn
145   *          the column name in the result set which is to be used for
146   *          the DN of the returned {@link Entry}
147   * @return the resulting LDAP Entry
148   * @throws SQLException
149   *           if a database access error occurs or the query matches more than
150   *           one row
151   */
152  public synchronized Entry searchToRawEntry(final PreparedStatement statement,
153          final String rdnColumn) throws SQLException
154  {
155    checkStatus();
156    ResultSet rset = statement.executeQuery();
157    Entry entry = null;
158    try
159    {
160      if(rset.next())
161      {
162        Object rdnValue = rset.getObject(rdnColumn);
163        if(rdnValue == null)
164        {
165          throw new SQLException("The RDN column '" + rdnColumn +
166                  "' was not found in the resulting row.");
167        }
168        entry = new Entry(rdnColumn + "=" + rdnValue);
169        ResultSetMetaData metaData = rset.getMetaData();
170        for(int i = 1; i <= metaData.getColumnCount(); i++)
171        {
172          Object o = rset.getObject(i);
173          String attrName = metaData.getColumnLabel(i);
174          String columnClass = metaData.getColumnClassName(i);
175
176          // figure out what type of class this column maps to
177          Class<?> clazz = null;
178          try
179          {
180            clazz = Class.forName(columnClass);
181          }
182          catch(ClassNotFoundException e)
183          {
184            throw new SQLException("Couldn't create class for column: " +
185                    attrName, e);
186          }
187
188          if(String.class.isAssignableFrom(clazz))
189          {
190            String str = (String) o;
191            addStringAttribute(entry, attrName, str);
192          }
193          else if(Number.class.isAssignableFrom(clazz))
194          {
195            Number num = (Number) o;
196            addNumericAttribute(entry, attrName, num);
197          }
198          else if(Date.class.isAssignableFrom(clazz))
199          {
200            Date date = (Date) o;
201            addDateAttribute(entry, attrName, date, true);
202          }
203          else if(Character.class.isAssignableFrom(clazz))
204          {
205            Character c = (Character) o;
206            addStringAttribute(entry, attrName, c.toString());
207          }
208          else if(Boolean.class.isAssignableFrom(clazz))
209          {
210            Boolean b = (Boolean) o;
211            addBooleanAttribute(entry, attrName, b);
212          }
213          else if(Blob.class.isAssignableFrom(clazz))
214          {
215            Blob blob = (Blob) o;
216            addBinaryAttribute(entry, attrName, blob, Integer.MAX_VALUE);
217          }
218          else if(Clob.class.isAssignableFrom(clazz))
219          {
220            Clob clob = (Clob) o;
221            if(clob != null)
222            {
223              addStringAttribute(entry, attrName,
224                      clob.getSubString(1, Integer.MAX_VALUE));
225              clob.free();
226            }
227          }
228          else
229          {
230            throw new SQLException("Column '" + attrName +
231                    "' has an unhandled type: " + columnClass);
232          }
233        }
234      }
235      if(rset.next())
236      {
237        throw new SQLException("The query matched more than one row.");
238      }
239    }
240    finally
241    {
242      rset.close();
243      statement.close();
244    }
245    return entry;
246  }
247
248  /**
249   * Creates a <code>Statement</code> object for sending
250   * SQL statements to the database.
251   * SQL statements without parameters are normally
252   * executed using <code>Statement</code> objects. If the same SQL statement
253   * is executed many times, it may be more efficient to use a
254   * <code>PreparedStatement</code> object.
255   * <P>
256   * Result sets created using the returned <code>Statement</code> object will
257   * by default be type <code>TYPE_FORWARD_ONLY</code> and have a concurrency
258   * level of <code>CONCUR_READ_ONLY</code>. The holdability of the created
259   * result sets can be determined by calling {@link Connection#getHoldability}.
260   * @return a new default <code>Statement</code> object
261   * @throws SQLException
262   *           if a database access error occurs
263   *           or this method is called on a closed connection
264   */
265  public synchronized Statement createStatement() throws SQLException
266  {
267    checkStatus();
268    return connection.createStatement();
269  }
270
271  /**
272   * Creates a <code>PreparedStatement</code> object for sending
273   * parameterized SQL statements to the database.
274   * <P>
275   * A SQL statement with or without IN parameters can be pre-compiled and
276   * stored in a <code>PreparedStatement</code> object. This object can then be
277   * used to efficiently execute this statement multiple times.
278   * <P>
279   * <B>Note:</B> This method is optimized for handling parametric SQL
280   * statements that benefit from precompilation. If the driver supports
281   * precompilation, the method <code>prepareStatement</code> will send the
282   * statement to the database for precompilation. Some drivers may not support
283   * precompilation. In this case, the statement may not be sent to the database
284   * until the <code>PreparedStatement</code> object is executed. This has no
285   * direct effect on users; however, it does affect which methods throw certain
286   * <code>SQLException</code> objects.
287   * <P>
288   * Result sets created using the returned <code>PreparedStatement</code>
289   * object will by default be type <code>TYPE_FORWARD_ONLY</code> and have a
290   * concurrency level of <code>CONCUR_READ_ONLY</code>. The holdability of the
291   * created result sets can be determined by calling
292   * {@link Connection#getHoldability}.
293   * @param sql
294   *          an SQL statement that may contain one or more '?' IN
295   *          parameter placeholders
296   * @return a new default <code>PreparedStatement</code> object containing the
297   *         pre-compiled SQL statement
298   * @throws SQLException
299   *           if a database access error occurs
300   *           or this method is called on a closed connection
301   */
302  public synchronized PreparedStatement prepareStatement(final String sql)
303          throws SQLException
304  {
305    checkStatus();
306    return connection.prepareStatement(sql);
307  }
308
309  /**
310   * Creates a <code>CallableStatement</code> object for calling
311   * database stored procedures.
312   * The <code>CallableStatement</code> object provides
313   * methods for setting up its IN and OUT parameters, and
314   * methods for executing the call to a stored procedure.
315   * <P>
316   * <B>Note:</B> This method is optimized for handling stored procedure call
317   * statements. Some drivers may send the call statement to the database when
318   * the method <code>prepareCall</code> is done; others may wait until the
319   * <code>CallableStatement</code> object is executed. This has no direct
320   * effect on users; however, it does affect which method throws certain
321   * SQLExceptions.
322   * <P>
323   * Result sets created using the returned <code>CallableStatement</code>
324   * object will by default be type <code>TYPE_FORWARD_ONLY</code> and have a
325   * concurrency level of <code>CONCUR_READ_ONLY</code>. The holdability of the
326   * created result sets can be determined by calling
327   * {@link Connection#getHoldability}.
328   * @param sql
329   *          an SQL statement that may contain one or more '?'
330   *          parameter placeholders. Typically this statement is specified
331   *          using JDBC
332   *          call escape syntax.
333   * @return a new default <code>CallableStatement</code> object containing the
334   *         pre-compiled SQL statement
335   * @throws SQLException
336   *           if a database access error occurs
337   *           or this method is called on a closed connection
338   */
339  public synchronized CallableStatement prepareCall(final String sql)
340          throws SQLException
341  {
342    checkStatus();
343    return connection.prepareCall(sql);
344  }
345
346  /**
347   * Retrieves the current transaction isolation level for the underlying
348   * <code>Connection</code>.
349   * @return the current transaction isolation level, which will be one
350   *         of the following constants:
351   *         <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
352   *         <code>Connection.TRANSACTION_READ_COMMITTED</code>,
353   *         <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
354   *         <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
355   *         <code>Connection.TRANSACTION_NONE</code>.
356   * @throws SQLException
357   *           if a database access error occurs or this method is called on a
358   *           closed connection
359   */
360  public synchronized int getTransactionIsolation() throws SQLException
361  {
362    checkStatus();
363    return connection.getTransactionIsolation();
364  }
365
366  /**
367   * Attempts to change the transaction isolation level for the underlying
368   * <code>Connection</code> object to the one given.
369   * The constants defined in the interface {@link Connection} are the possible
370   * transaction isolation levels.
371   * <P>
372   * <B>Note:</B> If this method is called during a transaction, the result is
373   * implementation-defined.
374   * @param level
375   *          one of the following <code>Connection</code> constants:
376   *          <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
377   *          <code>Connection.TRANSACTION_READ_COMMITTED</code>,
378   *          <code>Connection.TRANSACTION_REPEATABLE_READ</code>, or
379   *          <code>Connection.TRANSACTION_SERIALIZABLE</code>.
380   *          (Note that <code>Connection.TRANSACTION_NONE</code> cannot be used
381   *          because it specifies that transactions are not supported.)
382   * @throws SQLException
383   *           if a database access error occurs, this
384   *           method is called on a closed connection
385   *           or the given parameter is not one of the <code>Connection</code>
386   *           constants
387   */
388  public synchronized void setTransactionIsolation(final int level)
389          throws SQLException
390  {
391    checkStatus();
392    connection.setTransactionIsolation(level);
393  }
394
395  /**
396   * Commits the internal Connection if the transaction has not timed out and is
397   * still valid.
398   * @throws SQLException if a database access error occurs
399   */
400  public synchronized void commit() throws SQLException
401  {
402    checkStatus();
403    connection.commit();
404  }
405
406  /**
407   * Rolls back the internal Connection if the transaction has not timed out and
408   * is still valid.
409   * @throws SQLException if a database access error occurs
410   */
411  public synchronized void rollBack() throws SQLException
412  {
413    checkStatus();
414    connection.rollback();
415  }
416
417  /**
418   * Releases the internal connection back the pool and invalidates this context
419   * object.
420   */
421  public synchronized void close()
422  {
423    if(timeOutTask != null)
424    {
425      timeOutTask.cancel();
426    }
427    try
428    {
429      connection.close(); // return the connection to the pool
430    }
431    catch(SQLException e)
432    {
433      //suppress
434    }
435    finally
436    {
437      isClosed = true;
438    }
439  }
440
441  /**
442   * Gets the underlying {@link Connection} instance used by this context. This
443   * is not assignable to any vendor-specific types (e.g. OracleConnection or
444   * SQLServerConnection).
445   * If possible, you should avoid getting the connection directly, and instead
446   * use the
447   * wrapper methods provided by this class.
448   * @return a valid Connection instance
449   */
450  synchronized Connection getConnection()
451  {
452    checkStatus();
453    return connection;
454  }
455
456  /**
457   * Sets a timeout for this TransactionContext. The count begins immediately
458   * when this method is called. This method may be called multiple times if the
459   * timeout needs to be reset, provided that a previous timeout hasn't already
460   * expired. A value of zero indicates that there should be no timeout.
461   * Negative values are not allowed.
462   * <p>
463   * After the timeout has expired, all methods except {@link #isTimedOut} and
464   * {@link #isClosed} will throw a <code>IllegalStateException</code>.
465   * @param timeOutMillis
466   *          the delay in milliseconds after which this TransactionContext will
467   *          time out
468   */
469  public synchronized void setTimeout(final long timeOutMillis)
470  {
471    checkStatus();
472    isTimedOut = false;
473    timeOutMs = timeOutMillis;
474    if(timeOutTask != null)
475    {
476      timeOutTask.cancel();
477    }
478    timer.purge();
479    if(timeOutMillis == 0)
480    {
481      return;
482    }
483    timeOutTask = new TimerTask()
484    {
485      @Override
486      public void run()
487      {
488        try
489        {
490          rollBack();
491        }
492        catch (Throwable t)
493        {
494          //suppress
495        }
496        close();
497        isTimedOut = true;
498      }
499    };
500    timer.schedule(timeOutTask, timeOutMillis);
501  }
502
503  /**
504   * Checks whether this TransactionContext has timed out.
505   * <p>
506   * After the timeout has expired, all methods except {@link #isTimedOut},
507   * {@link #isClosed}, and <code>log[Error|Info|Debug|Exception]</code> will
508   * throw an <code>IllegalStateException</code>.
509   * @return true if the timeout has expired, false if not.
510   */
511  public boolean isTimedOut()
512  {
513    return isTimedOut;
514  }
515
516  /**
517   * Checks whether this TransactionContext is closed.
518   * @return true if the context is closed and no longer usable, false if not.
519   */
520  public boolean isClosed()
521  {
522    return isClosed;
523  }
524
525  /**
526   * Throws an IllegalStateException if this context is timed out or closed.
527   */
528  private void checkStatus()
529  {
530    if(isTimedOut)
531    {
532      throw new IllegalStateException("The transaction timed out after " +
533              timeOutMs + "ms.");
534    }
535    else if(isClosed)
536    {
537      throw new IllegalStateException(
538              "The transaction has already been closed.");
539    }
540  }
541}