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