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}