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 throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e); 270 } 271 } 272 super.localTransactionCommit(isSPI); 273 } 274 275 @Override 276 protected void localTransactionRollback(boolean isSPI) throws ResourceException { 277 Connection c = physicalConnection; 278 try { 279 c.rollback(); 280 } catch (SQLException e) { 281 throw new LocalTransactionException("Unable to rollback", e); 282 } 283 super.localTransactionRollback(isSPI); 284 try { 285 c.setAutoCommit(true); 286 } catch (SQLException e) { 287 throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e); 288 } 289 } 290 291 @Override 292 public XAResource getXAResource() throws ResourceException { 293 throw new NotSupportedException("XAResource not available from a LocalTransaction connection"); 294 } 295 296 @Override 297 protected void closePhysicalConnection() throws ResourceException { 298 Connection c = physicalConnection; 299 try { 300 c.close(); 301 } catch (SQLException e) { 302 throw new ResourceAdapterInternalException("Error attempting to destroy managed connection", e); 303 } 304 } 305 306 @Override 307 public ManagedConnectionMetaData getMetaData() throws ResourceException { 308 throw new NotSupportedException("no metadata available yet"); 309 } 310 311 @Override 312 public void connectionError(Exception e) { 313 if (fatalError != null) { 314 return; 315 } 316 317 if (isFatal(e)) { 318 fatalError = e; 319 if (exceptionSorter.rollbackOnFatalException()) { 320 if (TransactionHelper.isTransactionActive()) { 321 // will roll-back at tx end through #localTransactionRollback 322 TransactionHelper.setTransactionRollbackOnly(); 323 } else { 324 attemptRollback(); 325 } 326 } 327 } 328 } 329 330 @Override 331 public void cleanup() throws ResourceException { 332 super.cleanup(); 333 if (fatalError != null) { 334 ResourceException error = new ResourceException(String.format("fatal error occurred on %s, destroying", this), fatalError); 335 LogFactory.getLog(ManagedJDBCConnection.class).warn(error.getMessage(), error.getCause()); 336 throw error; 337 } 338 } 339 340 protected boolean isFatal(Exception e) { 341 if (exceptionSorter.isExceptionFatal(e)) { 342 return true; 343 } 344 try { 345 return !physicalConnection.isValid(10); 346 } catch (SQLException cause) { 347 return false; // could not state 348 } catch (LinkageError cause) { 349 return false; // not compliant JDBC4 driver 350 } 351 } 352 353 @Override 354 protected void attemptRollback() { 355 try { 356 physicalConnection.rollback(); 357 } catch (SQLException e) { 358 // ignore.... presumably the connection is actually dead 359 } 360 } 361 362 @Override 363 public String toString() { 364 return super.toString() + ". jdbc=" + physicalConnection; 365 } 366 } 367 368 CredentialExtractor credentialExtractor = new CredentialExtractor(subject, connectionRequestInfo, this); 369 Connection sqlConnection = getPhysicalConnection(subject, credentialExtractor); 370 return new ManagedJDBCConnection(this, sqlConnection, credentialExtractor, exceptionSorter, commitBeforeAutocommit); 371 } 372 373 protected Connection getPhysicalConnection(Subject subject, CredentialExtractor credentialExtractor) throws ResourceException { 374 try { 375 if (!driver.acceptsURL(url)) { 376 throw new ResourceAdapterInternalException("JDBC Driver cannot handle url: " + url); 377 } 378 } catch (SQLException e) { 379 throw new ResourceAdapterInternalException("JDBC Driver rejected url: " + url); 380 } 381 382 Properties info = new Properties(); 383 String user = credentialExtractor.getUserName(); 384 if (user != null) { 385 info.setProperty("user", user); 386 } 387 String password = credentialExtractor.getPassword(); 388 if (password != null) { 389 info.setProperty("password", password); 390 } 391 try { 392 return driver.connect(url, info); 393 } catch (SQLException e) { 394 throw new ResourceAllocationException("Unable to obtain physical connection to " + url, e); 395 } 396 } 397 398 @Override 399 public ManagedConnection matchManagedConnections(@SuppressWarnings("rawtypes") Set set, Subject subject, ConnectionRequestInfo connectionRequestInfo) 400 throws ResourceException { 401 for (@SuppressWarnings("unchecked") Iterator<Object> i = set.iterator(); i.hasNext();) { 402 Object o = i.next(); 403 if (o instanceof ManagedConnectionHandle) { 404 ManagedConnectionHandle<?,?> mc = (ManagedConnectionHandle<?,?>) o; 405 if (mc.matches(this, subject, connectionRequestInfo)) { 406 return mc; 407 } 408 } 409 } 410 return null; 411 } 412 413 @Override 414 public PrintWriter getLogWriter() { 415 return log; 416 } 417 418 @Override 419 public void setLogWriter(PrintWriter log) { 420 this.log = log; 421 } 422 423 void setDriver(String driver) throws InvalidPropertyException { 424 if (driver == null || driver.length() == 0) { 425 throw new InvalidPropertyException("Empty driver class name"); 426 } 427 try { 428 @SuppressWarnings("unchecked") 429 Class<Driver> driverClass = (Class<Driver>) Class.forName(driver); 430 this.driver = driverClass.newInstance(); 431 } catch (ClassNotFoundException e) { 432 throw new InvalidPropertyException("Unable to load driver class: " + driver, e); 433 } catch (InstantiationException e) { 434 throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e); 435 } catch (IllegalAccessException e) { 436 throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e); 437 } catch (ClassCastException e) { 438 throw new InvalidPropertyException("Class is not a " + Driver.class.getName() + ": " + driver, e); 439 } 440 } 441 442 void setConnectionURL(String url) throws InvalidPropertyException { 443 if (url == null || url.length() == 0) { 444 throw new InvalidPropertyException("Empty connection URL"); 445 } 446 this.url = url; 447 } 448 449 @Override 450 public String getUserName() { 451 return user; 452 } 453 454 void setUserName(String user) { 455 this.user = user; 456 } 457 458 @Override 459 public String getPassword() { 460 return password; 461 } 462 463 void setPassword(String password) { 464 this.password = password; 465 } 466 467 @Override 468 public Boolean isCommitBeforeAutocommit() { 469 return Boolean.valueOf(commitBeforeAutocommit); 470 } 471 472 void setCommitBeforeAutocommit(Boolean commitBeforeAutocommit) { 473 this.commitBeforeAutocommit = commitBeforeAutocommit != null && commitBeforeAutocommit.booleanValue(); 474 } 475 476 void setExceptionSorterClass(String className) throws InvalidPropertyException { 477 if (className == null || className.length() == 0) { 478 throw new InvalidPropertyException("Empty class name"); 479 } 480 try { 481 @SuppressWarnings("unchecked") 482 Class<ExceptionSorter> clazz = (Class<ExceptionSorter>) Class.forName(className); 483 exceptionSorter = clazz.newInstance(); 484 } catch (ClassNotFoundException e) { 485 throw new InvalidPropertyException("Unable to load class: " + className, e); 486 } catch (IllegalAccessException e) { 487 throw new InvalidPropertyException("Unable to instantiate class: " + className, e); 488 } catch (InstantiationException e) { 489 throw new InvalidPropertyException("Unable to instantiate class: " + className, e); 490 } catch (ClassCastException e) { 491 throw new InvalidPropertyException("Class is not a " + ExceptionSorter.class.getName() + ": " + driver, e); 492 } 493 } 494 495 @Override 496 public boolean equals(Object obj) { 497 if (obj == this) { 498 return true; 499 } 500 if (obj instanceof JdbcConnectionFactory) { 501 JdbcConnectionFactory other = (JdbcConnectionFactory) obj; 502 return url == other.url || url != null && url.equals(other.url); 503 } 504 return false; 505 } 506 507 @Override 508 public int hashCode() { 509 return url == null ? 0 : url.hashCode(); 510 } 511 512 @Override 513 public String toString() { 514 return "Pooled JDBC Driver Connection Factory [" + user + "@" + url + "]"; 515 } 516 517 } 518 519 protected String refAttribute(Reference ref, String key, String defvalue) { 520 RefAddr addr = ref.get(key); 521 if (addr == null) { 522 if (defvalue == null) { 523 throw new IllegalArgumentException(key + " address is mandatory"); 524 } 525 return defvalue; 526 } 527 return (String) addr.getContent(); 528 } 529 530}