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}