1 /* 2 * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License, version 2.0, 6 * as published by the Free Software Foundation. 7 * 8 * This program is also distributed with certain software (including 9 * but not limited to OpenSSL) that is licensed under separate terms, 10 * as designated in a particular file or component or in included license 11 * documentation. The authors of MySQL hereby grant you an additional 12 * permission to link the program and your derivative works with the 13 * separately licensed software that they have included with MySQL. 14 * 15 * This program is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License, version 2.0, for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with this program; if not, write to the Free Software 22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 */ 24 25 package com.mysql.clusterj.jdbc; 26 27 import com.mysql.clusterj.ClusterJHelper; 28 import com.mysql.clusterj.ClusterJUserException; 29 import com.mysql.clusterj.SessionFactory; 30 import com.mysql.clusterj.core.query.QueryDomainTypeImpl; 31 import com.mysql.clusterj.core.spi.SessionSPI; 32 import com.mysql.clusterj.core.store.Dictionary; 33 import com.mysql.clusterj.core.util.I18NHelper; 34 import com.mysql.clusterj.core.util.Logger; 35 import com.mysql.clusterj.core.util.LoggerFactoryService; 36 import com.mysql.jdbc.Connection; 37 import com.mysql.jdbc.ResultSetInternalMethods; 38 import com.mysql.jdbc.Statement; 39 import com.mysql.clusterj.jdbc.antlr.ANTLRNoCaseStringStream; 40 import com.mysql.clusterj.jdbc.antlr.MySQL51Parser; 41 import com.mysql.clusterj.jdbc.antlr.MySQL51Lexer; 42 import com.mysql.clusterj.jdbc.antlr.QueuingErrorListener; 43 import com.mysql.clusterj.jdbc.antlr.node.Node; 44 import com.mysql.clusterj.jdbc.antlr.node.PlaceholderNode; 45 import com.mysql.clusterj.jdbc.antlr.node.SelectNode; 46 import com.mysql.clusterj.jdbc.antlr.node.WhereNode; 47 import com.mysql.clusterj.query.Predicate; 48 49 import com.mysql.clusterj.jdbc.SQLExecutor.Executor; 50 import java.sql.SQLException; 51 import java.sql.Savepoint; 52 import java.util.ArrayList; 53 import java.util.IdentityHashMap; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Properties; 57 58 import org.antlr.runtime.CommonTokenStream; 59 import org.antlr.runtime.RecognitionException; 60 import org.antlr.runtime.Token; 61 import org.antlr.runtime.TokenStream; 62 import org.antlr.runtime.tree.CommonErrorNode; 63 import org.antlr.runtime.tree.CommonTree; 64 import org.antlr.runtime.tree.CommonTreeAdaptor; 65 import org.antlr.runtime.tree.TreeAdaptor; 66 67 /** This class implements the behavior associated with connection callbacks for statement execution 68 * and connection lifecycle. There is a clusterj session associated with the interceptor that 69 * is used to interact with the cluster. There is exactly one statement interceptor and one 70 * connection lifecycle interceptor associated with the interceptor. 71 * All of the SQL post-parsing behavior is contained here, and uses classes in the org.antlr.runtime and 72 * com.mysql.clusterj.jdbc.antlr packages to perform the parsing of the SQL statement. Analysis 73 * of the parsed SQL statement occurs here, and clusterj artifacts are constructed for use in 74 * other classes, in particular SQLExecutor and its command-specific subclasses. 75 */ 76 public class InterceptorImpl { 77 78 /** Register logger for JDBC stuff. */ 79 static { 80 LoggerFactoryService.getFactory().registerLogger("com.mysql.clusterj.jdbc"); 81 } 82 83 /** My message translator */ 84 static final I18NHelper local = I18NHelper.getInstance(InterceptorImpl.class); 85 86 /** My logger */ 87 static final Logger logger = LoggerFactoryService.getFactory().getInstance(InterceptorImpl.class); 88 89 static Map<String, Executor> parsedSqlMap = new IdentityHashMap<String, Executor>(); 90 91 /** The map of connection to interceptor */ 92 private static Map<Connection, InterceptorImpl> interceptorImplMap = 93 new IdentityHashMap<Connection, InterceptorImpl>(); 94 95 /** The connection properties */ 96 private Properties properties; 97 98 /** The connection being intercepted */ 99 private Connection connection; 100 101 /** The session factory for this connection */ 102 SessionFactory sessionFactory; 103 104 /** The current session (null if no session) */ 105 private SessionSPI session; 106 107 /** The statement interceptor (only used during initialization) */ 108 private StatementInterceptor statementInterceptor; 109 110 /** The connection lifecycle interceptor (only used during initialization) */ 111 private ConnectionLifecycleInterceptor connectionLifecycleInterceptor; 112 113 /** The interceptor is ready (both interceptors are registered) */ 114 private boolean ready = false; 115 116 private boolean autocommit; 117 118 private static String LOTSOBLANKS = " "; 119 120 /** Create the interceptor. 121 * 122 * @param connection the connection being intercepted 123 * @param properties the connection properties 124 */ InterceptorImpl(Connection connection, Properties properties)125 public InterceptorImpl(Connection connection, Properties properties) { 126 if (logger.isDebugEnabled()) logger.debug("constructed with properties: " + properties); 127 this.properties = properties; 128 this.connection = connection; 129 // if database name is not specified, translate DBNAME to the required clusterj property 130 String dbname = properties.getProperty("com.mysql.clusterj.database", 131 properties.getProperty("DBNAME")); 132 properties.put("com.mysql.clusterj.database", dbname); 133 } 134 135 /** Return the interceptor for the connection lifecycle callbacks. 136 * 137 * @param connectionLifecycleInterceptor the connection lifecycle interceptor 138 * @param connection the connection 139 * @param properties the connection properties 140 * @return the interceptor delegate 141 */ getInterceptorImpl( ConnectionLifecycleInterceptor connectionLifecycleInterceptor, Connection connection, Properties properties)142 public static InterceptorImpl getInterceptorImpl( 143 ConnectionLifecycleInterceptor connectionLifecycleInterceptor, 144 Connection connection, Properties properties) { 145 InterceptorImpl result = getInterceptorImpl(connection, properties); 146 if (result.connectionLifecycleInterceptor != null) { 147 if (result.connectionLifecycleInterceptor != connectionLifecycleInterceptor) { 148 throw new ClusterJUserException( 149 local.message("ERR_Duplicate_Connection_Lifecycle_Interceptor")); 150 } 151 } else { 152 result.connectionLifecycleInterceptor = connectionLifecycleInterceptor; 153 } 154 if (result.statementInterceptor != null) { 155 result.ready = true; 156 } 157 return result; 158 } 159 160 /** Return the interceptor for the statement interceptor callbacks. 161 * 162 * @param statementInterceptor the statement interceptor 163 * @param connection the connection 164 * @param properties the connection properties 165 * @return the interceptor delegate 166 */ getInterceptorImpl( StatementInterceptor statementInterceptor, Connection connection, Properties properties)167 public static InterceptorImpl getInterceptorImpl( 168 StatementInterceptor statementInterceptor, Connection connection, 169 Properties properties) { 170 InterceptorImpl result = getInterceptorImpl(connection, properties); 171 if (result.statementInterceptor != null) { 172 throw new ClusterJUserException( 173 local.message("ERR_Duplicate_Statement_Interceptor")); 174 } 175 result.statementInterceptor = statementInterceptor; 176 if (result.connectionLifecycleInterceptor != null) { 177 result.ready = true; 178 } 179 return result; 180 } 181 182 /** Create the interceptor to handle both connection lifecycle and statement interceptors. 183 * 184 * @param connection the connection 185 * @param properties the connection properties 186 * @return 187 */ getInterceptorImpl(Connection connection, Properties properties)188 public static InterceptorImpl getInterceptorImpl(Connection connection, Properties properties) { 189 InterceptorImpl result; 190 synchronized(interceptorImplMap) { 191 result = interceptorImplMap.get(connection); 192 if (result == null) { 193 result = new InterceptorImpl(connection, properties); 194 interceptorImplMap.put(connection, result); 195 } 196 } 197 return result; 198 } 199 200 /** Return the interceptor assigned to the connection. If there is no interceptor, return null. 201 * 202 * @param connection the connection 203 * @return the interceptor for this connection or null if there is no interceptor 204 */ getInterceptorImpl(java.sql.Connection connection)205 public static InterceptorImpl getInterceptorImpl(java.sql.Connection connection) { 206 synchronized (interceptorImplMap) { 207 return interceptorImplMap.get(connection); 208 } 209 } 210 211 @Override toString()212 public String toString() { 213 return "InterceptorImpl " 214 // + " properties: "+ properties.toString() 215 ; 216 } 217 destroy()218 void destroy() { 219 if (sessionFactory != null) { 220 if (session != null) { 221 session.close(); 222 } 223 sessionFactory.close(); 224 sessionFactory = null; 225 synchronized(interceptorImplMap) { 226 interceptorImplMap.remove(connection); 227 } 228 } 229 } 230 getSession()231 public SessionSPI getSession() { 232 if (session == null) { 233 session = (SessionSPI)sessionFactory.getSession(); 234 } 235 return session; 236 } 237 executeTopLevelOnly()238 public boolean executeTopLevelOnly() { 239 // assertReady(); 240 boolean result = true; 241 return result; 242 } 243 postProcess(String sql, Statement statement, ResultSetInternalMethods result, Connection connection, int arg4, boolean arg5, boolean arg6, SQLException sqlException)244 public ResultSetInternalMethods postProcess(String sql, Statement statement, 245 ResultSetInternalMethods result, Connection connection, int arg4, 246 boolean arg5, boolean arg6, SQLException sqlException) throws SQLException { 247 assertReady(); 248 return null; 249 } 250 preProcess(String sql, Statement statement, Connection connection)251 public ResultSetInternalMethods preProcess(String sql, Statement statement, 252 Connection connection) throws SQLException { 253 assertReady(); 254 if (statement instanceof com.mysql.jdbc.PreparedStatement) { 255 com.mysql.jdbc.PreparedStatement preparedStatement = 256 (com.mysql.jdbc.PreparedStatement)statement; 257 // key must be interned because we are using IdentityHashMap 258 String preparedSql = preparedStatement.getPreparedSql().intern(); 259 // see if we have a parsed version of this query 260 Executor sQLExecutor = null; 261 synchronized(parsedSqlMap) { 262 sQLExecutor = parsedSqlMap.get(preparedSql); 263 } 264 // if no cached SQLExecutor, create it, which might take some time 265 if (sQLExecutor == null) { 266 sQLExecutor = createSQLExecutor(preparedSql); 267 if (sQLExecutor != null) { 268 // multiple thread might have created a SQLExecutor but it's ok 269 synchronized(parsedSqlMap) { 270 parsedSqlMap.put(preparedSql, sQLExecutor); 271 } 272 } 273 } 274 return sQLExecutor.execute(this, preparedStatement.getParameterBindings()); 275 } 276 return null; 277 } 278 279 /** 280 * @param preparedSql 281 */ createSQLExecutor(String preparedSql)282 private Executor createSQLExecutor(String preparedSql) { 283 if (logger.isDetailEnabled()) logger.detail(preparedSql); 284 Executor result = null; 285 // parse the sql 286 CommonTree root = parse(preparedSql); 287 // get the root of the tree 288 int tokenType = root.getType(); 289 // perform command-specific actions 290 String tableName = ""; 291 CommonTree tableNode; 292 WhereNode whereNode; 293 List<String> columnNames = new ArrayList<String>(); 294 Dictionary dictionary; 295 DomainTypeHandlerImpl<?> domainTypeHandler; 296 QueryDomainTypeImpl<?> queryDomainType = null; 297 switch (tokenType) { 298 case MySQL51Parser.INSERT: 299 tableNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.TABLE); 300 tableName = getTableName(tableNode); 301 getSession(); 302 dictionary = session.getDictionary(); 303 domainTypeHandler = getDomainTypeHandler(tableName, dictionary); 304 CommonTree insertValuesNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.INSERT_VALUES); 305 CommonTree columnsNode = (CommonTree)insertValuesNode.getFirstChildWithType(MySQL51Parser.COLUMNS); 306 List<CommonTree> fields = columnsNode.getChildren(); 307 for (CommonTree field: fields) { 308 columnNames.add(getColumnName(field)); 309 } 310 if (logger.isDetailEnabled()) logger.detail( 311 "StatementInterceptorImpl.preProcess parse result INSERT INTO " + tableName 312 + " COLUMNS " + columnNames); 313 result = new SQLExecutor.Insert(domainTypeHandler, columnNames); 314 break; 315 case MySQL51Parser.SELECT: 316 CommonTree fromNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.FROM); 317 if (fromNode == null) { 318 // no from clause; cannot handle this case so return a do-nothing ParsedSQL 319 result = new SQLExecutor.Noop(); 320 break; 321 } 322 try { 323 // this currently handles only FROM clauses with a single table 324 tableNode = (CommonTree) fromNode.getFirstChildWithType(MySQL51Parser.TABLE); 325 tableName = getTableName(tableNode); 326 } catch (Exception e) { 327 // trouble with the FROM clause; log the SQL statement and the parser output 328 logger.info("Problem with FROM clause in SQL statement: " + preparedSql); 329 logger.info(walk(root)); 330 result = new SQLExecutor.Noop(); 331 break; 332 } 333 getSession(); 334 dictionary = session.getDictionary(); 335 domainTypeHandler = getDomainTypeHandler(tableName, dictionary); 336 columnsNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.COLUMNS); 337 List<CommonTree> selectExprNodes = columnsNode.getChildren(); 338 for (CommonTree selectExprNode: selectExprNodes) { 339 columnNames.add(getColumnName(getFieldNode(selectExprNode))); 340 } 341 String whereType = "empty"; 342 if (logger.isDetailEnabled()) logger.detail( 343 "SELECT FROM " + tableName 344 + " COLUMNS " + columnNames); 345 // we need to distinguish three cases: 346 // - no where clause (select all rows) 347 // - where clause that cannot be executed by clusterj 348 // - where clause that can be executed by clusterj 349 whereNode = ((SelectNode)root).getWhereNode(); 350 queryDomainType = (QueryDomainTypeImpl<?>) session.createQueryDomainType(domainTypeHandler); 351 if (whereNode == null) { 352 // no where clause (select all rows) 353 result = new SQLExecutor.Select(domainTypeHandler, columnNames, queryDomainType); 354 } else { 355 // create a predicate from the tree 356 Predicate predicate = whereNode.getPredicate(queryDomainType); 357 if (predicate != null) { 358 // where clause that can be executed by clusterj 359 queryDomainType.where(predicate); 360 result = new SQLExecutor.Select(domainTypeHandler, columnNames, queryDomainType); 361 whereType = "clusterj"; 362 } else { 363 // where clause that cannot be executed by clusterj 364 result = new SQLExecutor.Noop(); 365 whereType = "non-clusterj"; 366 } 367 if (logger.isDetailEnabled()) logger.detail(walk(root)); 368 } 369 if (logger.isDetailEnabled()) { 370 logger.detail( 371 "SELECT FROM " + tableName 372 + " COLUMNS " + columnNames + " whereType " + whereType); 373 logger.detail(walk(root)); 374 } 375 break; 376 case MySQL51Parser.DELETE: 377 tableNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.TABLE); 378 tableName = getTableName(tableNode); 379 getSession(); 380 dictionary = session.getDictionary(); 381 domainTypeHandler = getDomainTypeHandler(tableName, dictionary); 382 whereNode = ((WhereNode)root.getFirstChildWithType(MySQL51Parser.WHERE)); 383 int numberOfParameters = 0; 384 if (whereNode == null) { 385 // no where clause (delete all rows) 386 result = new SQLExecutor.Delete(domainTypeHandler); 387 whereType = "empty"; 388 } else { 389 // create a predicate from the tree 390 queryDomainType = (QueryDomainTypeImpl<?>) session.createQueryDomainType(domainTypeHandler); 391 Predicate predicate = whereNode.getPredicate(queryDomainType); 392 if (predicate != null) { 393 // where clause that can be executed by clusterj 394 queryDomainType.where(predicate); 395 numberOfParameters = whereNode.getNumberOfParameters(); 396 result = new SQLExecutor.Delete(domainTypeHandler, queryDomainType, numberOfParameters); 397 whereType = "clusterj"; 398 } else { 399 // where clause that cannot be executed by clusterj 400 result = new SQLExecutor.Noop(); 401 whereType = "non-clusterj"; 402 } 403 if (logger.isDetailEnabled()) logger.detail(walk(root)); 404 } 405 if (logger.isDetailEnabled()) logger.detail( 406 "DELETE FROM " + tableName 407 + " whereType " + whereType 408 + " number of parameters " + numberOfParameters); 409 break; 410 default: 411 // return a do-nothing ParsedSQL 412 if (logger.isDetailEnabled()) logger.detail("ClusterJ cannot process this SQL statement: unsupported statement type."); 413 result = new SQLExecutor.Noop(); 414 } 415 return result; 416 } 417 getPrimaryKeyFieldName(CommonTree whereNode)418 private String getPrimaryKeyFieldName(CommonTree whereNode) { 419 String result = null; 420 CommonTree operation = (CommonTree) whereNode.getChild(0); 421 if (MySQL51Parser.EQUALS == operation.getType()) { 422 result = operation.getChild(0).getChild(0).getText(); 423 } else { 424 throw new ClusterJUserException("Cannot find primary key in WHERE clause."); 425 } 426 return result; 427 } 428 walk(CommonTree tree)429 private String walk(CommonTree tree) { 430 StringBuilder buffer = new StringBuilder(); 431 walk(tree, buffer, 0); 432 return buffer.toString(); 433 } 434 435 @SuppressWarnings("unchecked") // tree.getChildren() walk(CommonTree tree, StringBuilder buffer, int level)436 private void walk(CommonTree tree, StringBuilder buffer, int level) { 437 String indent = LOTSOBLANKS.substring(0, level); 438 Token token = tree.token; 439 int tokenType = token.getType(); 440 String tokenText = token.getText(); 441 int childCount = tree.getChildCount(); 442 int childIndex = tree.getChildIndex(); 443 buffer.append('\n'); 444 buffer.append(indent); 445 buffer.append(tokenText); 446 buffer.append(" class: "); 447 buffer.append(tree.getClass().getName()); 448 buffer.append(" tokenType "); 449 buffer.append(tokenType); 450 buffer.append(" child count "); 451 buffer.append(childCount); 452 buffer.append(" child index "); 453 buffer.append(childIndex); 454 List<CommonTree> children = tree.getChildren(); 455 if (children == null) { 456 return; 457 } 458 for (CommonTree child: children) { 459 walk(child, buffer, level + 2); 460 } 461 } 462 parse(String preparedSql)463 private CommonTree parse(String preparedSql) { 464 CommonTree result = null; 465 ANTLRNoCaseStringStream inputStream = new ANTLRNoCaseStringStream(preparedSql); 466 MySQL51Lexer lexer = new MySQL51Lexer(inputStream); 467 CommonTokenStream tokens = new CommonTokenStream(lexer); 468 lexer.setErrorListener(new QueuingErrorListener(lexer)); 469 tokens.getTokens(); 470 if (lexer.getErrorListener().hasErrors()) { 471 logger.warn(local.message("ERR_Lexing_SQ",preparedSql)); 472 return result; 473 } 474 PlaceholderNode.resetId(); 475 MySQL51Parser parser = new MySQL51Parser(tokens); 476 parser.setTreeAdaptor(mySQLTreeAdaptor); 477 parser.setErrorListener(new QueuingErrorListener(parser)); 478 try { 479 CommonTree stmtTree = (CommonTree) parser.statement().getTree(); 480 result = stmtTree; 481 } catch (RecognitionException e) { 482 logger.warn(local.message("ERR_Parsing_SQL", preparedSql)); 483 } 484 if (parser.getErrorListener().hasErrors()) { 485 logger.warn(local.message("ERR_Parsing_SQL", preparedSql)); 486 } 487 return result; 488 } 489 490 private TreeAdaptor mySQLTreeAdaptor = new CommonTreeAdaptor() { 491 public Object create(Token token) { return new Node(token); } 492 public Object dupNode(Object t) { 493 if ( t==null ) return null; 494 return create(((Node)t).token); 495 } 496 }; 497 getTableName(CommonTree tableNode)498 private String getTableName(CommonTree tableNode) { 499 return tableNode.getChild(0).getText(); 500 } 501 getColumnName(CommonTree fieldNode)502 private String getColumnName(CommonTree fieldNode) { 503 return fieldNode.getChild(0).getText(); 504 } 505 getFieldNode(CommonTree selectExprNode)506 private CommonTree getFieldNode(CommonTree selectExprNode) { 507 return (CommonTree)selectExprNode.getChild(0); 508 } 509 destroy(StatementInterceptor statementInterceptor)510 public void destroy(StatementInterceptor statementInterceptor) { 511 } 512 destroy( ConnectionLifecycleInterceptor connectionLifecycleInterceptor)513 public void destroy( 514 ConnectionLifecycleInterceptor connectionLifecycleInterceptor) { 515 } 516 assertReady()517 private void assertReady() { 518 if (!ready) { 519 if (statementInterceptor == null) { 520 throw new ClusterJUserException(local.message("ERR_No_Statement_Interceptor")); 521 } 522 if (connectionLifecycleInterceptor == null) { 523 throw new ClusterJUserException(local.message("ERR_No_Connection_Lifecycle_Interceptor")); 524 } 525 } else { 526 if (sessionFactory == null) { 527 sessionFactory = ClusterJHelper.getSessionFactory(properties); 528 } 529 } 530 } 531 532 /** TODO This needs to be rewritten with a proper state machine. */ setAutoCommit(boolean autocommit)533 public boolean setAutoCommit(boolean autocommit) throws SQLException { 534 assertReady(); 535 logStatus("setAutoCommit(" + autocommit + ")"); 536 this.autocommit = autocommit; 537 getSession(); 538 if (!autocommit) { 539 // start a transaction 540 if (!session.currentTransaction().isActive()) { 541 session.begin(); 542 } 543 } else { 544 // roll back the previous transaction if active 545 if (session.currentTransaction().isActive()) { 546 session.rollback(); 547 } 548 } 549 return true; // let the driver perform its own autocommit behavior 550 } 551 close()552 public void close() { 553 } 554 commit()555 public boolean commit() throws SQLException { 556 logStatus("commit"); 557 if (session.currentTransaction().isActive()) { 558 session.commit(); 559 } else { 560 System.out.println("WARNING: commit called when session.transaction is not active"); 561 } 562 session.begin(); 563 return true; 564 } 565 rollback()566 public boolean rollback() throws SQLException { 567 logStatus("rollback"); 568 session.rollback(); 569 session.begin(); 570 return true; 571 } 572 rollback(Savepoint savepoint)573 public boolean rollback(Savepoint savepoint) throws SQLException { 574 logStatus("rollback(Savepoint)"); 575 return true; 576 } 577 setCatalog(String catalog)578 public boolean setCatalog(String catalog) throws SQLException { 579 if (logger.isDebugEnabled()) logger.debug("catalog: " + catalog); 580 return true; 581 } 582 transactionCompleted()583 public boolean transactionCompleted() throws SQLException { 584 logStatus("transactionCompleted"); 585 return true; 586 } 587 transactionBegun()588 public boolean transactionBegun() throws SQLException { 589 logStatus("transactionBegun"); 590 return true; 591 } 592 getDomainTypeHandler(String tableName, Dictionary dictionary)593 private DomainTypeHandlerImpl<?> getDomainTypeHandler(String tableName, Dictionary dictionary) { 594 DomainTypeHandlerImpl<?> domainTypeHandler = 595 DomainTypeHandlerImpl.getDomainTypeHandler(tableName, dictionary); 596 return domainTypeHandler; 597 } 598 logStatus(String s)599 private void logStatus(String s) throws SQLException { 600 if (logger.isDetailEnabled()) { 601 StringBuilder builder = new StringBuilder("In "); 602 builder.append(s); 603 builder.append(" with"); 604 if (connection != null) { 605 builder.append(" connection.getAutocommit: " + connection.getAutoCommit()); 606 } 607 if (session != null) { 608 builder.append(" session.isActive: " + session.currentTransaction().isActive()); 609 } 610 builder.append('\n'); 611 String message = builder.toString(); 612 logger.detail(message); 613 } 614 } 615 616 } 617