001/* 002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Stephane Lacoin 018 */ 019package org.nuxeo.runtime.datasource; 020 021import java.io.PrintWriter; 022import java.security.AccessController; 023import java.security.PrivilegedAction; 024import java.sql.Connection; 025import java.sql.Driver; 026import java.sql.SQLException; 027import java.sql.SQLFeatureNotSupportedException; 028import java.util.Hashtable; 029import java.util.Iterator; 030import java.util.Properties; 031import java.util.Set; 032import java.util.logging.Logger; 033 034import javax.naming.Context; 035import javax.naming.Name; 036import javax.naming.NamingException; 037import javax.naming.RefAddr; 038import javax.naming.Reference; 039import javax.naming.spi.ObjectFactory; 040import javax.resource.NotSupportedException; 041import javax.resource.ResourceException; 042import javax.resource.spi.ConnectionManager; 043import javax.resource.spi.ConnectionRequestInfo; 044import javax.resource.spi.InvalidPropertyException; 045import javax.resource.spi.LocalTransaction; 046import javax.resource.spi.LocalTransactionException; 047import javax.resource.spi.ManagedConnection; 048import javax.resource.spi.ManagedConnectionFactory; 049import javax.resource.spi.ManagedConnectionMetaData; 050import javax.resource.spi.ResourceAdapterInternalException; 051import javax.resource.spi.ResourceAllocationException; 052import javax.security.auth.Subject; 053import javax.sql.DataSource; 054import javax.sql.XADataSource; 055import javax.transaction.xa.XAResource; 056 057import org.apache.commons.logging.LogFactory; 058import org.nuxeo.runtime.datasource.PooledDataSourceRegistry.PooledDataSource; 059import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerConfiguration; 060import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerFactory; 061import org.nuxeo.runtime.jtajca.NuxeoContainer; 062import org.nuxeo.runtime.jtajca.NuxeoContainer.ConnectionManagerWrapper; 063import org.nuxeo.runtime.transaction.TransactionHelper; 064import org.tranql.connector.AbstractManagedConnection; 065import org.tranql.connector.CredentialExtractor; 066import org.tranql.connector.ExceptionSorter; 067import org.tranql.connector.ManagedConnectionHandle; 068import org.tranql.connector.UserPasswordManagedConnectionFactory; 069import org.tranql.connector.jdbc.AutocommitSpecCompliant; 070import org.tranql.connector.jdbc.ConnectionHandle; 071import org.tranql.connector.jdbc.KnownSQLStateExceptionSorter; 072import org.tranql.connector.jdbc.LocalDataSourceWrapper; 073import org.tranql.connector.jdbc.TranqlDataSource; 074import org.tranql.connector.jdbc.XADataSourceWrapper; 075 076public class PooledDataSourceFactory implements ObjectFactory { 077 078 @Override 079 public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> environment) { 080 class NuxeoDataSource extends TranqlDataSource implements PooledDataSource { 081 082 protected ConnectionManagerWrapper wrapper; 083 084 public NuxeoDataSource(ManagedConnectionFactory mcf, ConnectionManagerWrapper wrapper) { 085 super(mcf, wrapper); 086 this.wrapper = wrapper; 087 } 088 089 @Override 090 public void dispose() { 091 wrapper.dispose(); 092 } 093 094 @Override 095 public Connection getConnection(boolean noSharing) throws SQLException { 096 if (!noSharing) { 097 return getConnection(); 098 } 099 wrapper.enterNoSharing(); 100 try { 101 return getConnection(); 102 } finally { 103 wrapper.exitNoSharing(); 104 } 105 } 106 107 @Override 108 public Logger getParentLogger() throws SQLFeatureNotSupportedException { 109 throw new SQLFeatureNotSupportedException("not yet available"); 110 } 111 } 112 Reference ref = (Reference) obj; 113 ManagedConnectionFactory mcf; 114 ConnectionManagerWrapper cm; 115 try { 116 mcf = createFactory(ref, ctx); 117 cm = createManager(ref, ctx); 118 } catch (ResourceException | NamingException e) { 119 throw new RuntimeException(e); 120 } 121 return new NuxeoDataSource(mcf, cm); 122 } 123 124 protected ConnectionManagerWrapper createManager(Reference ref, Context ctx) throws ResourceException { 125 NuxeoConnectionManagerConfiguration config = NuxeoConnectionManagerFactory.getConfig(ref); 126 String className = ref.getClassName(); 127 config.setXAMode(XADataSource.class.getName().equals(className)); 128 return NuxeoContainer.initConnectionManager(config); 129 } 130 131 protected ManagedConnectionFactory createFactory(Reference ref, Context ctx) throws NamingException, 132 InvalidPropertyException { 133 String className = ref.getClassName(); 134 if (XADataSource.class.getName().equals(className)) { 135 String user = refAttribute(ref, "User", ""); 136 String password = refAttribute(ref, "Password", ""); 137 String name = refAttribute(ref, "dataSourceJNDI", null); 138 XADataSource ds = NuxeoContainer.lookup(name, XADataSource.class); 139 XADataSourceWrapper wrapper = new XADataSourceWrapper(ds); 140 wrapper.setUserName(user); 141 wrapper.setPassword(password); 142 return wrapper; 143 } 144 if (javax.sql.DataSource.class.getName().equals(className)) { 145 String user = refAttribute(ref, "username", ""); 146 if (user.isEmpty()) { 147 user = refAttribute(ref, "user", ""); 148 if (!user.isEmpty()) { 149 LogFactory.getLog(PooledDataSourceFactory.class).warn( 150 "wrong attribute 'user' in datasource descriptor, should use 'username' instead"); 151 } 152 } 153 String password = refAttribute(ref, "password", ""); 154 String dsname = refAttribute(ref, "dataSourceJNDI", ""); 155 if (!dsname.isEmpty()) { 156 javax.sql.DataSource ds = NuxeoContainer.lookup(dsname, DataSource.class); 157 LocalDataSourceWrapper wrapper = new LocalDataSourceWrapper(ds); 158 wrapper.setUserName(user); 159 wrapper.setPassword(password); 160 return wrapper; 161 } 162 String name = refAttribute(ref, "driverClassName", null); 163 String url = refAttribute(ref, "url", null); 164 String sqlExceptionSorter = refAttribute(ref, "sqlExceptionSorter", 165 DatasourceExceptionSorter.class.getName()); 166 boolean commitBeforeAutocommit = Boolean.valueOf(refAttribute(ref, "commitBeforeAutocommit", "true")).booleanValue(); 167 JdbcConnectionFactory factory = new JdbcConnectionFactory(); 168 factory.setDriver(name); 169 factory.setUserName(user); 170 factory.setPassword(password); 171 factory.setConnectionURL(url); 172 factory.setExceptionSorterClass(sqlExceptionSorter); 173 factory.setCommitBeforeAutocommit(commitBeforeAutocommit); 174 return factory; 175 } 176 throw new IllegalArgumentException("unsupported class " + className); 177 } 178 179 static class JdbcConnectionFactory implements UserPasswordManagedConnectionFactory, AutocommitSpecCompliant { 180 private static final long serialVersionUID = 4317141492511322929L; 181 private Driver driver; 182 private String url; 183 private String user; 184 private String password; 185 private ExceptionSorter exceptionSorter = new KnownSQLStateExceptionSorter(); 186 private boolean commitBeforeAutocommit = false; 187 188 private PrintWriter log; 189 190 @Override 191 public Object createConnectionFactory() throws ResourceException { 192 throw new NotSupportedException("ConnectionManager is required"); 193 } 194 195 @Override 196 public Object createConnectionFactory(ConnectionManager connectionManager) throws ResourceException { 197 return new TranqlDataSource(this, connectionManager); 198 } 199 200 @Override 201 public ManagedConnection createManagedConnection(Subject subject, ConnectionRequestInfo connectionRequestInfo) throws ResourceException { 202 203 class ManagedJDBCConnection extends AbstractManagedConnection<Connection, ConnectionHandle> { 204 final CredentialExtractor credentialExtractor; 205 final LocalTransactionImpl localTx; 206 final LocalTransactionImpl localClientTx; 207 final boolean commitBeforeAutoCommit; 208 209 Exception fatalError; 210 211 ManagedJDBCConnection(UserPasswordManagedConnectionFactory mcf, Connection physicalConnection, 212 CredentialExtractor credentialExtractor, ExceptionSorter exceptionSorter, boolean commitBeforeAutoCommit) { 213 super(mcf, physicalConnection, exceptionSorter); 214 this.credentialExtractor = credentialExtractor; 215 localTx = new LocalTransactionImpl(true); 216 localClientTx = new LocalTransactionImpl(false); 217 this.commitBeforeAutoCommit = commitBeforeAutoCommit; 218 } 219 220 @Override 221 public boolean matches(ManagedConnectionFactory mcf, Subject subject, ConnectionRequestInfo connectionRequestInfo) 222 throws ResourceAdapterInternalException { 223 return credentialExtractor.matches(subject, connectionRequestInfo, (UserPasswordManagedConnectionFactory) mcf); 224 } 225 226 @Override 227 public LocalTransaction getClientLocalTransaction() { 228 return localClientTx; 229 } 230 231 @Override 232 public LocalTransaction getLocalTransaction() throws ResourceException { 233 return localTx; 234 } 235 236 Connection physicalConnection() throws ResourceException { 237 return physicalConnection; 238 } 239 240 @Override 241 protected void localTransactionStart(boolean isSPI) throws ResourceException { 242 Connection c = physicalConnection(); 243 try { 244 c.setAutoCommit(false); 245 } catch (SQLException e) { 246 throw new LocalTransactionException("Unable to disable autoCommit", e); 247 } 248 super.localTransactionStart(isSPI); 249 } 250 251 @Override 252 protected void localTransactionCommit(boolean isSPI) throws ResourceException { 253 Connection c = physicalConnection(); 254 try { 255 if (commitBeforeAutoCommit) { 256 c.commit(); 257 } 258 } catch (SQLException e) { 259 try { 260 c.rollback(); 261 } catch (SQLException e1) { 262 if (log != null) { 263 e.printStackTrace(log); 264 } 265 } 266 throw new LocalTransactionException("Unable to commit", e); 267 } finally { 268 try { 269 c.setAutoCommit(true); 270 } catch (SQLException e) { 271 throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e); 272 } 273 } 274 super.localTransactionCommit(isSPI); 275 } 276 277 @Override 278 protected void localTransactionRollback(boolean isSPI) throws ResourceException { 279 Connection c = physicalConnection; 280 try { 281 c.rollback(); 282 } catch (SQLException e) { 283 throw new LocalTransactionException("Unable to rollback", e); 284 } 285 super.localTransactionRollback(isSPI); 286 try { 287 c.setAutoCommit(true); 288 } catch (SQLException e) { 289 throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e); 290 } 291 } 292 293 @Override 294 public XAResource getXAResource() throws ResourceException { 295 throw new NotSupportedException("XAResource not available from a LocalTransaction connection"); 296 } 297 298 @Override 299 protected void closePhysicalConnection() throws ResourceException { 300 Connection c = physicalConnection; 301 try { 302 c.close(); 303 } catch (SQLException e) { 304 throw new ResourceAdapterInternalException("Error attempting to destroy managed connection", e); 305 } 306 } 307 308 @Override 309 public ManagedConnectionMetaData getMetaData() throws ResourceException { 310 throw new NotSupportedException("no metadata available yet"); 311 } 312 313 @Override 314 public void connectionError(Exception e) { 315 if (fatalError != null) { 316 return; 317 } 318 319 if (isFatal(e)) { 320 fatalError = e; 321 if (exceptionSorter.rollbackOnFatalException()) { 322 if (TransactionHelper.isTransactionActive()) { 323 // will roll-back at tx end through #localTransactionRollback 324 TransactionHelper.setTransactionRollbackOnly(); 325 } else { 326 attemptRollback(); 327 } 328 } 329 } 330 } 331 332 @Override 333 public void cleanup() throws ResourceException { 334 super.cleanup(); 335 if (fatalError != null) { 336 ResourceException error = new ResourceException(String.format("fatal error occurred on %s, destroying", this), fatalError); 337 LogFactory.getLog(ManagedJDBCConnection.class).warn(error.getMessage(), error.getCause()); 338 throw error; 339 } 340 } 341 342 protected boolean isFatal(Exception e) { 343 if (exceptionSorter.isExceptionFatal(e)) { 344 return true; 345 } 346 try { 347 return !physicalConnection.isValid(10); 348 } catch (SQLException cause) { 349 return false; // could not state 350 } catch (LinkageError cause) { 351 return false; // not compliant JDBC4 driver 352 } 353 } 354 355 @Override 356 protected void attemptRollback() { 357 try { 358 physicalConnection.rollback(); 359 } catch (SQLException e) { 360 // ignore.... presumably the connection is actually dead 361 } 362 } 363 364 @Override 365 public String toString() { 366 return super.toString() + ". jdbc=" + physicalConnection; 367 } 368 } 369 370 CredentialExtractor credentialExtractor = new CredentialExtractor(subject, connectionRequestInfo, this); 371 Connection sqlConnection = getPhysicalConnection(subject, credentialExtractor); 372 return new ManagedJDBCConnection(this, sqlConnection, credentialExtractor, exceptionSorter, commitBeforeAutocommit); 373 } 374 375 protected Connection getPhysicalConnection(Subject subject, CredentialExtractor credentialExtractor) throws ResourceException { 376 try { 377 if (!driver.acceptsURL(url)) { 378 throw new ResourceAdapterInternalException("JDBC Driver cannot handle url: " + url); 379 } 380 } catch (SQLException e) { 381 throw new ResourceAdapterInternalException("JDBC Driver rejected url: " + url); 382 } 383 384 Properties info = new Properties(); 385 String user = credentialExtractor.getUserName(); 386 if (user != null) { 387 info.setProperty("user", user); 388 } 389 String password = credentialExtractor.getPassword(); 390 if (password != null) { 391 info.setProperty("password", password); 392 } 393 try { 394 return driver.connect(url, info); 395 } catch (SQLException e) { 396 throw new ResourceAllocationException("Unable to obtain physical connection to " + url, e); 397 } 398 } 399 400 @Override 401 public ManagedConnection matchManagedConnections(Set set, Subject subject, ConnectionRequestInfo connectionRequestInfo) 402 throws ResourceException { 403 for (Iterator<Object> i = set.iterator(); i.hasNext();) { 404 Object o = i.next(); 405 if (o instanceof ManagedConnectionHandle) { 406 ManagedConnectionHandle mc = (ManagedConnectionHandle) o; 407 if (mc.matches(this, subject, connectionRequestInfo)) { 408 return mc; 409 } 410 } 411 } 412 return null; 413 } 414 415 @Override 416 public PrintWriter getLogWriter() { 417 return log; 418 } 419 420 @Override 421 public void setLogWriter(PrintWriter log) { 422 this.log = log; 423 } 424 425 void setDriver(String driver) throws InvalidPropertyException { 426 if (driver == null || driver.length() == 0) { 427 throw new InvalidPropertyException("Empty driver class name"); 428 } 429 try { 430 @SuppressWarnings("unchecked") 431 Class<Driver> driverClass = (Class<Driver>) Class.forName(driver); 432 this.driver = driverClass.newInstance(); 433 } catch (ClassNotFoundException e) { 434 throw new InvalidPropertyException("Unable to load driver class: " + driver, e); 435 } catch (InstantiationException e) { 436 throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e); 437 } catch (IllegalAccessException e) { 438 throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e); 439 } catch (ClassCastException e) { 440 throw new InvalidPropertyException("Class is not a " + Driver.class.getName() + ": " + driver, e); 441 } 442 } 443 444 void setConnectionURL(String url) throws InvalidPropertyException { 445 if (url == null || url.length() == 0) { 446 throw new InvalidPropertyException("Empty connection URL"); 447 } 448 this.url = url; 449 } 450 451 @Override 452 public String getUserName() { 453 return user; 454 } 455 456 void setUserName(String user) { 457 this.user = user; 458 } 459 460 @Override 461 public String getPassword() { 462 return password; 463 } 464 465 void setPassword(String password) { 466 this.password = password; 467 } 468 469 @Override 470 public Boolean isCommitBeforeAutocommit() { 471 return Boolean.valueOf(commitBeforeAutocommit); 472 } 473 474 void setCommitBeforeAutocommit(Boolean commitBeforeAutocommit) { 475 this.commitBeforeAutocommit = commitBeforeAutocommit != null && commitBeforeAutocommit.booleanValue(); 476 } 477 478 void setExceptionSorterClass(String className) throws InvalidPropertyException { 479 if (className == null || className.length() == 0) { 480 throw new InvalidPropertyException("Empty class name"); 481 } 482 try { 483 @SuppressWarnings("unchecked") 484 Class<ExceptionSorter> clazz = (Class<ExceptionSorter>) Class.forName(className); 485 exceptionSorter = clazz.newInstance(); 486 } catch (ClassNotFoundException e) { 487 throw new InvalidPropertyException("Unable to load class: " + className, e); 488 } catch (IllegalAccessException e) { 489 throw new InvalidPropertyException("Unable to instantiate class: " + className, e); 490 } catch (InstantiationException e) { 491 throw new InvalidPropertyException("Unable to instantiate class: " + className, e); 492 } catch (ClassCastException e) { 493 throw new InvalidPropertyException("Class is not a " + ExceptionSorter.class.getName() + ": " + driver, e); 494 } 495 } 496 497 @Override 498 public boolean equals(Object obj) { 499 if (obj == this) { 500 return true; 501 } 502 if (obj instanceof JdbcConnectionFactory) { 503 JdbcConnectionFactory other = (JdbcConnectionFactory) obj; 504 return url == other.url || url != null && url.equals(other.url); 505 } 506 return false; 507 } 508 509 @Override 510 public int hashCode() { 511 return url == null ? 0 : url.hashCode(); 512 } 513 514 @Override 515 public String toString() { 516 return "Pooled JDBC Driver Connection Factory [" + user + "@" + url + "]"; 517 } 518 519 private Class<?> loadClass(String name) throws ClassNotFoundException { 520 // first try the TCL, then the classloader that defined us 521 ClassLoader cl = getContextClassLoader(); 522 if (cl != null) { 523 try { 524 return cl.loadClass(name); 525 } catch (ClassNotFoundException e) { 526 // ignore this 527 } 528 } 529 return Class.forName(name); 530 } 531 532 private ClassLoader getContextClassLoader() { 533 return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { 534 @Override 535 public ClassLoader run() { 536 try { 537 return Thread.currentThread().getContextClassLoader(); 538 } catch (SecurityException e) { 539 return null; 540 } 541 } 542 }); 543 } 544 } 545 546 protected String refAttribute(Reference ref, String key, String defvalue) { 547 RefAddr addr = ref.get(key); 548 if (addr == null) { 549 if (defvalue == null) { 550 throw new IllegalArgumentException(key + " address is mandatory"); 551 } 552 return defvalue; 553 } 554 return (String) addr.getContent(); 555 } 556 557}