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-2013 UnboundID Corp.
026     */
027    package com.unboundid.directory.sdk.sync.types;
028    
029    import static com.unboundid.directory.sdk.sync.util.ScriptUtils.*;
030    
031    import java.sql.Blob;
032    import java.sql.CallableStatement;
033    import java.sql.Clob;
034    import java.sql.Connection;
035    import java.sql.Date;
036    import java.sql.PreparedStatement;
037    import java.sql.ResultSet;
038    import java.sql.ResultSetMetaData;
039    import java.sql.SQLException;
040    import java.sql.Statement;
041    import java.util.Timer;
042    import java.util.TimerTask;
043    import com.unboundid.ldap.sdk.Entry;
044    import 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     */
058    public 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    }