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}