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 }