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.sql.Connection; 023import java.sql.Driver; 024import java.sql.SQLException; 025import java.sql.SQLFeatureNotSupportedException; 026import java.util.Hashtable; 027import java.util.Iterator; 028import java.util.Properties; 029import java.util.Set; 030import java.util.logging.Logger; 031 032import javax.naming.Context; 033import javax.naming.Name; 034import javax.naming.NamingException; 035import javax.naming.RefAddr; 036import javax.naming.Reference; 037import javax.naming.spi.ObjectFactory; 038import javax.resource.NotSupportedException; 039import javax.resource.ResourceException; 040import javax.resource.spi.ConnectionManager; 041import javax.resource.spi.ConnectionRequestInfo; 042import javax.resource.spi.InvalidPropertyException; 043import javax.resource.spi.LocalTransaction; 044import javax.resource.spi.LocalTransactionException; 045import javax.resource.spi.ManagedConnection; 046import javax.resource.spi.ManagedConnectionFactory; 047import javax.resource.spi.ManagedConnectionMetaData; 048import javax.resource.spi.ResourceAdapterInternalException; 049import javax.resource.spi.ResourceAllocationException; 050import javax.security.auth.Subject; 051import javax.sql.DataSource; 052import javax.sql.XADataSource; 053import javax.transaction.xa.XAResource; 054 055import org.apache.commons.logging.LogFactory; 056import org.nuxeo.runtime.datasource.PooledDataSourceRegistry.PooledDataSource; 057import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerConfiguration; 058import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerFactory; 059import org.nuxeo.runtime.jtajca.NuxeoContainer; 060import org.nuxeo.runtime.jtajca.NuxeoContainer.ConnectionManagerWrapper; 061import org.nuxeo.runtime.transaction.TransactionHelper; 062import org.tranql.connector.AbstractManagedConnection; 063import org.tranql.connector.CredentialExtractor; 064import org.tranql.connector.ExceptionSorter; 065import org.tranql.connector.ManagedConnectionHandle; 066import org.tranql.connector.UserPasswordManagedConnectionFactory; 067import org.tranql.connector.jdbc.AutocommitSpecCompliant; 068import org.tranql.connector.jdbc.ConnectionHandle; 069import org.tranql.connector.jdbc.KnownSQLStateExceptionSorter; 070import org.tranql.connector.jdbc.LocalDataSourceWrapper; 071import org.tranql.connector.jdbc.TranqlDataSource; 072import org.tranql.connector.jdbc.XADataSourceWrapper; 073 074public class PooledDataSourceFactory implements ObjectFactory { 075 076 @Override 077 public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> environment) { 078 class NuxeoDataSource extends TranqlDataSource implements PooledDataSource { 079 080 protected ConnectionManagerWrapper wrapper; 081 082 public NuxeoDataSource(ManagedConnectionFactory mcf, ConnectionManagerWrapper wrapper) { 083 super(mcf, wrapper); 084 this.wrapper = wrapper; 085 } 086 087 @Override 088 public void dispose() { 089 wrapper.dispose(); 090 } 091 092 @Override 093 public Connection getConnection(boolean noSharing) throws SQLException { 094 if (!noSharing) { 095 return getConnection(); 096 } 097 wrapper.getManager().enterNoSharing(); 098 try { 099 return getConnection(); 100 } finally { 101 wrapper.getManager().exitNoSharing(); 102 } 103 } 104 105 @Override 106 public Logger getParentLogger() throws SQLFeatureNotSupportedException { 107 throw new SQLFeatureNotSupportedException("not yet available"); 108 } 109 } 110 Reference ref = (Reference) obj; 111 ManagedConnectionFactory mcf; 112 ConnectionManagerWrapper cm; 113 try { 114 mcf = createFactory(ref, ctx); 115 cm = createManager(ref, ctx); 116 } catch (ResourceException | NamingException e) { 117 throw new RuntimeException(e); 118 } 119 return new NuxeoDataSource(mcf, cm); 120 } 121 122 protected ConnectionManagerWrapper createManager(Reference ref, Context ctx) throws ResourceException { 123 NuxeoConnectionManagerConfiguration config = NuxeoConnectionManagerFactory.getConfig(ref); 124 String className = ref.getClassName(); 125 config.setXAMode(XADataSource.class.getName().equals(className)); 126 return NuxeoContainer.initConnectionManager(config); 127 } 128 129 protected ManagedConnectionFactory createFactory(Reference ref, Context ctx) throws NamingException, 130 InvalidPropertyException { 131 String className = ref.getClassName(); 132 if (XADataSource.class.getName().equals(className)) { 133 String user = refAttribute(ref, "User", ""); 134 String password = refAttribute(ref, "Password", ""); 135 String name = refAttribute(ref, "dataSourceJNDI", null); 136 XADataSource ds = NuxeoContainer.lookup(name, XADataSource.class); 137 XADataSourceWrapper wrapper = new XADataSourceWrapper(ds); 138 wrapper.setUserName(user); 139 wrapper.setPassword(password); 140 return wrapper; 141 } 142 if (javax.sql.DataSource.class.getName().equals(className)) { 143 String user = refAttribute(ref, "username", ""); 144 if (user.isEmpty()) { 145 user = refAttribute(ref, "user", ""); 146 if (!user.isEmpty()) { 147 LogFactory.getLog(PooledDataSourceFactory.class).warn( 148 "wrong attribute 'user' in datasource descriptor, should use 'username' instead"); 149 } 150 } 151 String password = refAttribute(ref, "password", ""); 152 String dsname = refAttribute(ref, "dataSourceJNDI", ""); 153 if (!dsname.isEmpty()) { 154 javax.sql.DataSource ds = NuxeoContainer.lookup(dsname, DataSource.class); 155 LocalDataSourceWrapper wrapper = new LocalDataSourceWrapper(ds); 156 wrapper.setUserName(user); 157 wrapper.setPassword(password); 158 return wrapper; 159 } 160 String name = refAttribute(ref, "driverClassName", null); 161 String url = refAttribute(ref, "url", null); 162 String sqlExceptionSorter = refAttribute(ref, "sqlExceptionSorter", 163 DatasourceExceptionSorter.class.getName()); 164 boolean commitBeforeAutocommit = Boolean.valueOf(refAttribute(ref, "commitBeforeAutocommit", "true")).booleanValue(); 165 JdbcConnectionFactory factory = new JdbcConnectionFactory(); 166 factory.setDriver(name); 167 factory.setUserName(user); 168 factory.setPassword(password); 169 factory.setConnectionURL(url); 170 factory.setExceptionSorterClass(sqlExceptionSorter); 171 factory.setCommitBeforeAutocommit(commitBeforeAutocommit); 172 return factory; 173 } 174 throw new IllegalArgumentException("unsupported class " + className); 175 } 176 177 static class JdbcConnectionFactory implements UserPasswordManagedConnectionFactory, AutocommitSpecCompliant { 178 private static final long serialVersionUID = 4317141492511322929L; 179 private Driver driver; 180 private String url; 181 private String user; 182 private String password; 183 private ExceptionSorter exceptionSorter = new KnownSQLStateExceptionSorter(); 184 private boolean commitBeforeAutocommit = false; 185 186 private PrintWriter log; 187 188 @Override 189 public Object createConnectionFactory() throws ResourceException { 190 throw new NotSupportedException("ConnectionManager is required"); 191 } 192 193 @Override 194 public Object createConnectionFactory(ConnectionManager connectionManager) throws ResourceException { 195 return new TranqlDataSource(this, connectionManager); 196 } 197 198 @Override 199 public ManagedConnection createManagedConnection(Subject subject, ConnectionRequestInfo connectionRequestInfo) throws ResourceException { 200 201 class ManagedJDBCConnection extends AbstractManagedConnection<Connection, ConnectionHandle> { 202 final CredentialExtractor credentialExtractor; 203 final LocalTransactionImpl localTx; 204 final LocalTransactionImpl localClientTx; 205 final boolean commitBeforeAutoCommit; 206 207 Exception fatalError; 208 209 ManagedJDBCConnection(UserPasswordManagedConnectionFactory mcf, Connection physicalConnection, 210 CredentialExtractor credentialExtractor, ExceptionSorter exceptionSorter, boolean commitBeforeAutoCommit) { 211 super(mcf, physicalConnection, exceptionSorter); 212 this.credentialExtractor = credentialExtractor; 213 localTx = new LocalTransactionImpl(true); 214 localClientTx = new LocalTransactionImpl(false); 215 this.commitBeforeAutoCommit = commitBeforeAutoCommit; 216 } 217 218 @Override 219 public boolean matches(ManagedConnectionFactory mcf, Subject subject, ConnectionRequestInfo connectionRequestInfo) 220 throws ResourceAdapterInternalException { 221 return credentialExtractor.matches(subject, connectionRequestInfo, (UserPasswordManagedConnectionFactory) mcf); 222 } 223 224 @Override 225 public LocalTransaction getClientLocalTransaction() { 226 return localClientTx; 227 } 228 229 @Override 230 public LocalTransaction getLocalTransaction() throws ResourceException { 231 return localTx; 232 } 233 234 Connection physicalConnection() throws ResourceException { 235 return physicalConnection; 236 } 237 238 @Override 239 protected void localTransactionStart(boolean isSPI) throws ResourceException { 240 Connection c = physicalConnection(); 241 try { 242 c.setAutoCommit(false); 243 } catch (SQLException e) { 244 throw new LocalTransactionException("Unable to disable autoCommit", e); 245 } 246 super.localTransactionStart(isSPI); 247 } 248 249 @Override 250 protected void localTransactionCommit(boolean isSPI) throws ResourceException { 251 Connection c = physicalConnection(); 252 try { 253 if (commitBeforeAutoCommit) { 254 c.commit(); 255 } 256 } catch (SQLException e) { 257 try { 258 c.rollback(); 259 } catch (SQLException e1) { 260 if (log != null) { 261 e.printStackTrace(log); 262 } 263 } 264 throw new LocalTransactionException("Unable to commit", e); 265 } finally { 266 try { 267 c.setAutoCommit(true); 268 } catch (SQLException e) { 269 // don't rethrow inside finally 270 LogFactory.getLog(PooledDataSourceFactory.class) 271 .error("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(@SuppressWarnings("rawtypes") Set set, Subject subject, ConnectionRequestInfo connectionRequestInfo) 402 throws ResourceException { 403 for (@SuppressWarnings("unchecked") 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 } 520 521 protected String refAttribute(Reference ref, String key, String defvalue) { 522 RefAddr addr = ref.get(key); 523 if (addr == null) { 524 if (defvalue == null) { 525 throw new IllegalArgumentException(key + " address is mandatory"); 526 } 527 return defvalue; 528 } 529 return (String) addr.getContent(); 530 } 531 532}