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}