1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody;
19
20 import java.io.Serializable;
21 import java.lang.reflect.InvocationHandler;
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Method;
24 import java.lang.reflect.Proxy;
25 import java.sql.Connection;
26 import java.sql.Driver;
27 import java.sql.SQLException;
28 import java.sql.Statement;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.atomic.AtomicInteger;
36 import java.util.concurrent.atomic.AtomicLong;
37
38 import javax.naming.Context;
39 import javax.naming.NamingException;
40 import javax.servlet.ServletContext;
41 import javax.sql.DataSource;
42
43 import net.bull.javamelody.internal.common.LOG;
44 import net.bull.javamelody.internal.common.Parameters;
45 import net.bull.javamelody.internal.model.ConnectionInformations;
46 import net.bull.javamelody.internal.model.Counter;
47
48 /**
49  * Cette classe est utile pour construire des proxy de {@link DataSource}s ou de {@link Connection}s jdbc.<br>
50  * Et notamment elle rebinde dans l'annuaire JNDI la dataSource jdbc en la remplaçant
51  * par un proxy de monitoring.
52  * @author Emeric Vernat
53  */

54 public final class JdbcWrapper {
55     /**
56      * Instance singleton de JdbcWrapper (ici on ne connaît pas le ServletContext).
57      */

58     public static final JdbcWrapper SINGLETON = new JdbcWrapper(
59             new Counter(Counter.SQL_COUNTER_NAME, "db.png"));
60
61     // au lieu d'utiliser int avec des synchronized partout, on utilise AtomicInteger
62     static final AtomicInteger ACTIVE_CONNECTION_COUNT = new AtomicInteger();
63     static final AtomicInteger USED_CONNECTION_COUNT = new AtomicInteger();
64     static final AtomicLong TRANSACTION_COUNT = new AtomicLong();
65     static final AtomicInteger ACTIVE_THREAD_COUNT = new AtomicInteger();
66     static final AtomicInteger RUNNING_BUILD_COUNT = new AtomicInteger();
67     static final AtomicInteger BUILD_QUEUE_LENGTH = new AtomicInteger();
68     static final AtomicLong BUILD_QUEUE_WAITING_DURATIONS_SUM = new AtomicLong();
69     static final Map<Integer, ConnectionInformations> USED_CONNECTION_INFORMATIONS = new ConcurrentHashMap<>();
70
71     private static final int MAX_USED_CONNECTION_INFORMATIONS = 500;
72
73     // Cette variable sqlCounter conserve un état qui est global au filtre et à l'application (donc thread-safe).
74     private final Counter sqlCounter;
75     private ServletContext servletContext;
76     private boolean connectionInformationsEnabled;
77     private boolean jboss;
78     private boolean glassfish;
79     private boolean weblogic;
80
81     static final class ConnectionInformationsComparator
82             implements Comparator<ConnectionInformations>, Serializable {
83         private static final long serialVersionUID = 1L;
84
85         /** {@inheritDoc} */
86         @Override
87         public int compare(ConnectionInformations connection1, ConnectionInformations connection2) {
88             return connection1.getOpeningDate().compareTo(connection2.getOpeningDate());
89         }
90     }
91
92     /**
93      * Handler de proxy d'un {@link Statement} jdbc.
94      */

95     private class StatementInvocationHandler implements InvocationHandler {
96         // Rq : dans les proxy de DataSource, Connection et Statement,
97         // si la méthode appelée est java.sql.Wrapper.unwrap
98         // on invoque toujours unwrap sur l'objet initial pour qu'il retourne lui-même
99         // ou son objet wrappé. Par exemple, l'appel de unwrap sur le proxy d'un Statement
100         // retournera le Statement initial du serveur ou même du driver bdd (OracleStatement...)
101         // sans notre proxy pour pouvoir appeler les méthodes non standard du driver par ex.
102         private String requestName;
103         private final Statement statement;
104         private final Connection connection;
105
106         StatementInvocationHandler(String query, Statement statement, Connection connection) {
107             super();
108             assert statement != null;
109             assert connection != null;
110
111             this.requestName = query;
112             this.statement = statement;
113             this.connection = connection;
114         }
115
116         /** {@inheritDoc} */
117         @Override
118         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
119             // performance : on évite method.invoke pour equals & hashCode
120             final String methodName = method.getName();
121             if (isEqualsMethod(methodName, args)) {
122                 return statement.equals(args[0]);
123             } else if (isHashCodeMethod(methodName, args)) {
124                 return statement.hashCode();
125             } else if (methodName.startsWith("execute")) {
126                 if (isFirstArgAString(args)) {
127                     // la méthode est du type executeQuery(String), executeUpdate(String),
128                     // executeUpdate(String, ...) ou execute(String sql),
129                     // alors la requête sql est le premier argument (et pas query)
130                     requestName = (String) args[0];
131                 } else if (("executeBatch".equals(methodName)
132                         || "executeLargeBatch".equals(methodName)) && requestName != null
133                         && !requestName.startsWith("/* BATCH */ ")) {
134                     // if executeBatch, add a prefix in the request name to explain that
135                     // 1 batch "hit" is equivalent to several exec of the request in the db.
136
137                     // requestName may be null if executeBatch()
138                     // without prepareStatement(String) or addBatch(String)
139                     requestName = "/* BATCH */ " + requestName;
140                 }
141
142                 // si on n'a pas trouvé la requête, on prend "null"
143                 requestName = String.valueOf(requestName);
144
145                 return doExecute(requestName, statement, method, args);
146             } else if ("addBatch".equals(methodName) && isFirstArgAString(args)) {
147                 // Bien que déconseillée la méthode est addBatch(String),
148                 // la requête sql est alors le premier argument
149                 // (elle sera utilisée lors de l'appel à executeBatch())
150
151                 // Rq : on ne conserve que la dernière requête de addBatch.
152                 // Rq : si addBatch(String) est appelée, puis que executeUpdate(String)
153                 // la requête du batch est correctement ignorée ci-dessus.
154                 // Rq : si connection.prepareStatement(String).addBatch(String) puis executeUpdate()
155                 // sont appelées (et pas executeBatch()) alors la requête conservée est
156                 // faussement celle du batch mais l'application cloche grave.
157                 requestName = (String) args[0];
158             } else if ("getConnection".equals(methodName) && (args == null || args.length == 0)) {
159                 return connection;
160             }
161
162             // ce n'est pas une méthode executeXxx du Statement
163             return method.invoke(statement, args);
164         }
165
166         private boolean isFirstArgAString(Object[] args) {
167             return args != null && args.length > 0 && args[0] instanceof String;
168         }
169     }
170
171     /**
172      * Handler de proxy d'une {@link Connection} jdbc.
173      */

174     private class ConnectionInvocationHandler implements InvocationHandler {
175         private final Connection connection;
176         private boolean alreadyClosed;
177
178         ConnectionInvocationHandler(Connection connection) {
179             super();
180             assert connection != null;
181             this.connection = connection;
182         }
183
184         void init() {
185             // on limite la taille pour éviter une éventuelle saturation mémoire
186             if (isConnectionInformationsEnabled()
187                     && USED_CONNECTION_INFORMATIONS.size() < MAX_USED_CONNECTION_INFORMATIONS) {
188                 USED_CONNECTION_INFORMATIONS.put(
189                         ConnectionInformations.getUniqueIdOfConnection(connection),
190                         new ConnectionInformations());
191             }
192             USED_CONNECTION_COUNT.incrementAndGet();
193             TRANSACTION_COUNT.incrementAndGet();
194         }
195
196         /** {@inheritDoc} */
197         @Override
198         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
199             // performance : on évite method.invoke pour equals & hashCode
200             final String methodName = method.getName();
201             if (isEqualsMethod(methodName, args)) {
202                 return areConnectionsEquals(args[0]);
203             } else if (isHashCodeMethod(methodName, args)) {
204                 return connection.hashCode();
205             }
206             try {
207                 Object result = method.invoke(connection, args);
208                 if (result instanceof Statement) {
209                     final String requestName;
210                     if ("prepareStatement".equals(methodName) || "prepareCall".equals(methodName)) {
211                         // la méthode est du type prepareStatement(String) ou prepareCall(String),
212                         // alors la requête sql est le premier argument
213                         requestName = (String) args[0];
214                     } else {
215                         requestName = null;
216                     }
217                     result = createStatementProxy(requestName, (Statement) result,
218                             (Connection) proxy);
219                 }
220                 return result;
221             } finally {
222                 if ("close".equals(methodName) && !alreadyClosed) {
223                     USED_CONNECTION_COUNT.decrementAndGet();
224                     USED_CONNECTION_INFORMATIONS
225                             .remove(ConnectionInformations.getUniqueIdOfConnection(connection));
226                     alreadyClosed = true;
227                 }
228             }
229         }
230
231         private boolean areConnectionsEquals(Object object) {
232             // Special case if what we're being passed is one of our proxies (specifically a connection proxy)
233             // This way the equals call is truely transparent for our proxies (cf issue 78)
234             if (Proxy.isProxyClass(object.getClass())) {
235                 final InvocationHandler invocationHandler = Proxy.getInvocationHandler(object);
236                 if (invocationHandler instanceof DelegatingInvocationHandler) {
237                     final DelegatingInvocationHandler d = (DelegatingInvocationHandler) invocationHandler;
238                     if (d.getDelegate() instanceof ConnectionInvocationHandler) {
239                         final ConnectionInvocationHandler c = (ConnectionInvocationHandler) d
240                                 .getDelegate();
241                         return connection.equals(c.connection);
242                     }
243                 }
244             }
245             return connection.equals(object);
246         }
247     }
248
249     private static class ConnectionManagerInvocationHandler
250             extends AbstractInvocationHandler<Object> {
251         // classe sérialisable pour glassfish v2.1.1, issue 229: Exception in NamingManagerImpl copyMutableObject()
252         private static final long serialVersionUID = 1L;
253
254         ConnectionManagerInvocationHandler(Object javaxConnectionManager) {
255             super(javaxConnectionManager);
256         }
257
258         @Override
259         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
260             final Object result = method.invoke(getProxiedObject(), args);
261             if (result instanceof Connection) {
262                 return SINGLETON
263                         .createConnectionProxyOrRewrapIfJBossOrGlassfish((Connection) result);
264             }
265             return result;
266         }
267     }
268
269     private abstract static class AbstractInvocationHandler<T>
270             implements InvocationHandler, Serializable {
271         private static final long serialVersionUID = 1L;
272
273         @SuppressWarnings("all")
274         private final T proxiedObject;
275
276         AbstractInvocationHandler(T proxiedObject) {
277             super();
278             this.proxiedObject = proxiedObject;
279         }
280
281         T getProxiedObject() {
282             return proxiedObject;
283         }
284     }
285
286     // ce handler désencapsule les InvocationTargetException des proxy
287     private static class DelegatingInvocationHandler implements InvocationHandler, Serializable {
288         // classe sérialisable pour MonitoringProxy
289         private static final long serialVersionUID = 7515240588169084785L;
290         @SuppressWarnings("all")
291         private final InvocationHandler delegate;
292
293         DelegatingInvocationHandler(InvocationHandler delegate) {
294             super();
295             this.delegate = delegate;
296         }
297
298         InvocationHandler getDelegate() {
299             return delegate;
300         }
301
302         /** {@inheritDoc} */
303         @Override
304         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
305             try {
306                 return delegate.invoke(proxy, method, args);
307             } catch (final InvocationTargetException e) {
308                 if (e.getTargetException() != null) {
309                     throw e.getTargetException();
310                 }
311                 throw e;
312             }
313         }
314     }
315
316     private JdbcWrapper(Counter sqlCounter) {
317         super();
318         assert sqlCounter != null;
319         this.sqlCounter = sqlCounter;
320         // servletContext reste null pour l'instant
321         this.servletContext = null;
322         connectionInformationsEnabled = Parameters.isSystemActionsEnabled()
323                 && !Parameters.isNoDatabase();
324     }
325
326     void initServletContext(ServletContext context) {
327         assert context != null;
328         this.servletContext = context;
329         final String serverInfo = servletContext.getServerInfo();
330         jboss = serverInfo.contains("JBoss") || serverInfo.contains("WildFly");
331         glassfish = serverInfo.contains("GlassFish")
332                 || serverInfo.contains("Sun Java System Application Server")
333                 || serverInfo.contains("Payara");
334         weblogic = serverInfo.contains("WebLogic");
335         connectionInformationsEnabled = Parameters.isSystemActionsEnabled()
336                 && !Parameters.isNoDatabase();
337     }
338
339     public static int getUsedConnectionCount() {
340         return USED_CONNECTION_COUNT.get();
341     }
342
343     public static int getActiveConnectionCount() {
344         return ACTIVE_CONNECTION_COUNT.get();
345     }
346
347     public static long getTransactionCount() {
348         return TRANSACTION_COUNT.get();
349     }
350
351     public static int getActiveThreadCount() {
352         return ACTIVE_THREAD_COUNT.get();
353     }
354
355     public static int getRunningBuildCount() {
356         return RUNNING_BUILD_COUNT.get();
357     }
358
359     public static int getBuildQueueLength() {
360         return BUILD_QUEUE_LENGTH.get();
361     }
362
363     public static long getBuildQueueWaitingDurationsSum() {
364         return BUILD_QUEUE_WAITING_DURATIONS_SUM.get();
365     }
366
367     public static List<ConnectionInformations> getConnectionInformationsList() {
368         final List<ConnectionInformations> result = new ArrayList<>(
369                 USED_CONNECTION_INFORMATIONS.values());
370         Collections.sort(result, new ConnectionInformationsComparator());
371         return Collections.unmodifiableList(result);
372     }
373
374     public Counter getSqlCounter() {
375         return sqlCounter;
376     }
377
378     boolean isConnectionInformationsEnabled() {
379         return connectionInformationsEnabled;
380     }
381
382     public static int getMaxConnectionCount() {
383         return JdbcWrapperHelper.getMaxConnectionCount();
384     }
385
386     public static Map<String, Map<String, Object>> getBasicDataSourceProperties() {
387         return JdbcWrapperHelper.getBasicDataSourceProperties();
388     }
389
390     public static Map<String, DataSource> getJndiAndSpringDataSources() throws NamingException {
391         return JdbcWrapperHelper.getJndiAndSpringDataSources();
392     }
393
394     /**
395      * Enregistre une {@link DataSource} ne venant pas de JNDI.
396      * @param name String
397      * @param dataSource DataSource
398      */

399     public static void registerSpringDataSource(String name, DataSource dataSource) {
400         JdbcWrapperHelper.registerSpringDataSource(name, dataSource);
401     }
402
403     Object doExecute(String requestName, Statement statement, Method method, Object[] args)
404             throws IllegalAccessException, InvocationTargetException {
405         assert requestName != null;
406         assert statement != null;
407         assert method != null;
408
409         // on ignore les requêtes explain exécutées par DatabaseInformations
410         if (!sqlCounter.isDisplayed() || requestName.startsWith("explain ")) {
411             ACTIVE_CONNECTION_COUNT.incrementAndGet();
412             try {
413                 return method.invoke(statement, args);
414             } finally {
415                 ACTIVE_CONNECTION_COUNT.decrementAndGet();
416             }
417         }
418
419         final long start = System.currentTimeMillis();
420         boolean systemError = true;
421         try {
422             ACTIVE_CONNECTION_COUNT.incrementAndGet();
423
424             // note perf: selon un paramètre current-sql(/requests)-disabled,
425             // on pourrait ici ne pas binder un nouveau contexte à chaque requête sql
426             sqlCounter.bindContext(requestName, requestName, null, -1, -1);
427
428             final Object result = method.invoke(statement, args);
429             systemError = false;
430             return result;
431         } catch (final InvocationTargetException e) {
432             if (e.getCause() instanceof SQLException) {
433                 final int errorCode = ((SQLException) e.getCause()).getErrorCode();
434                 if (errorCode >= 20000 && errorCode < 30000) {
435                     // Dans Oracle par exemple, les erreurs 20000 à 30000 sont standardisées
436                     // comme étant des erreurs lancées par l'application dans des procédures stockées
437                     // pour être traitées comme des erreurs de saisies ou comme des règles de gestion.
438                     // Ce ne sont donc pas des erreurs systèmes.
439                     systemError = false;
440                 }
441             }
442             throw e;
443         } finally {
444             // Rq : on n'utilise pas la création du statement et l'appel à la méthode close du statement
445             // comme début et fin d'une connexion active, car en fonction de l'application
446             // la méthode close du statement peut ne jamais être appelée
447             // (par exemple, seule la méthode close de la connection peut être appelée ce qui ferme aussi le statement)
448             // Rq : pas de temps cpu pour les requêtes sql car c'est 0 ou quasiment 0
449             ACTIVE_CONNECTION_COUNT.decrementAndGet();
450             final long duration = Math.max(System.currentTimeMillis() - start, 0);
451             sqlCounter.addRequest(requestName, duration, -1, -1, systemError, -1);
452         }
453     }
454
455     boolean rebindDataSources() {
456         boolean ok;
457         // on cherche une datasource avec InitialContext pour afficher nom et version bdd + nom et version driver jdbc
458         // (le nom de la dataSource recherchée dans JNDI est du genre jdbc/Xxx qui est le nom standard d'une DataSource)
459         try {
460             final boolean rewrapDataSources = Parameter.REWRAP_DATASOURCES.getValueAsBoolean();
461             if (rewrapDataSources || Parameter.DATASOURCES.getValue() != null) {
462                 // on annule le rebinding ou rewrapping éventuellement faits avant par SessionListener
463                 // si datasources ou rewrap-datasources est défini dans le filter
464                 stop();
465             }
466             final Map<String, DataSource> jndiDataSources = JdbcWrapperHelper.getJndiDataSources();
467             LOG.debug("datasources found in JNDI: " + jndiDataSources.keySet());
468             for (final Map.Entry<String, DataSource> entry : jndiDataSources.entrySet()) {
469                 final String jndiName = entry.getKey();
470                 final DataSource dataSource = entry.getValue();
471                 try {
472                     if (rewrapDataSources || isServerNeedsRewrap(jndiName)) {
473                         rewrapDataSource(jndiName, dataSource);
474                         JdbcWrapperHelper.registerRewrappedDataSource(jndiName, dataSource);
475                     } else if (!isProxyAlready(dataSource)) {
476                         // si dataSource est déjà un proxy, il ne faut pas faire un proxy d'un proxy ni un rebinding
477                         final DataSource dataSourceProxy = createDataSourceProxy(jndiName,
478                                 dataSource);
479                         JdbcWrapperHelper.rebindDataSource(servletContext, jndiName, dataSource,
480                                 dataSourceProxy);
481                         LOG.debug("datasource rebinded: " + jndiName + " from class "
482                                 + dataSource.getClass().getName() + " to class "
483                                 + dataSourceProxy.getClass().getName());
484                     }
485                 } catch (final Throwable t) { // NOPMD
486                     // ça n'a pas marché, tant pis pour celle-ci qui semble invalide, mais continuons avec les autres
487                     LOG.debug("rebinding datasource " + jndiName + " failed, skipping it", t);
488                 }
489             }
490             ok = true;
491         } catch (final Throwable t) { // NOPMD
492             // ça n'a pas marché, tant pis
493             LOG.debug("rebinding datasources failed, skipping", t);
494             ok = false;
495         }
496         return ok;
497     }
498
499     private void rewrapDataSource(String jndiName, DataSource dataSource)
500             throws IllegalAccessException {
501         final String dataSourceClassName = dataSource.getClass().getName();
502         LOG.debug("Datasource needs rewrap: " + jndiName + " of class " + dataSourceClassName);
503         final String dataSourceRewrappedMessage = "Datasource rewrapped: " + jndiName;
504         if (isJBossOrGlassfishDataSource(dataSourceClassName)) {
505             // JBOSS: le rebind de la datasource dans le JNDI JBoss est possible mais ne
506             // fonctionne pas (car tous les lookup renverraient alors une instance de
507             // MarshalledValuePair ou une instance javax.naming.Reference selon comment cela
508             // est fait), donc on modifie directement l'instance de WrapperDataSource déjà
509             // présente dans le JNDI.
510             // GLASSFISH: le contexte JNDI commençant par "java:" est en lecture seule
511             // dans glassfish (comme dit dans la spec et comme implémenté dans
512             // http://kickjava.com/src/com/sun/enterprise/naming/java/javaURLContext.java.htm),
513             // donc on modifie directement l'instance de DataSource40 déjà présente dans le
514             // JNDI.
515             // Par "chance", la classe org.jboss.resource.adapter.jdbc.WrapperDataSource et
516             // la super-classe de com.sun.gjc.spi.jdbc40.DataSource40 contiennent toutes les
517             // deux un attribut de nom "cm" et de type javax.resource.spi.ConnectionManager
518             // dont on veut faire un proxy.
519             Object javaxConnectionManager = JdbcWrapperHelper.getFieldValue(dataSource, "cm");
520             javaxConnectionManager = createJavaxConnectionManagerProxy(javaxConnectionManager);
521             JdbcWrapperHelper.setFieldValue(dataSource, "cm", javaxConnectionManager);
522             LOG.debug(dataSourceRewrappedMessage);
523         } else if (isWildfly9DataSource(dataSourceClassName)) {
524             Object delegateDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
525             delegateDataSource = createDataSourceProxy((DataSource) delegateDataSource);
526             JdbcWrapperHelper.setFieldValue(dataSource, "delegate", delegateDataSource);
527             LOG.debug(dataSourceRewrappedMessage);
528         } else if (weblogic
529                 && "weblogic.jdbc.common.internal.RmiDataSource".equals(dataSourceClassName)) {
530             // WEBLOGIC: le contexte JNDI est en lecture seule donc on modifie directement
531             // l'instance de RmiDataSource déjà présente dans le JNDI.
532             rewrapWebLogicDataSource(dataSource);
533             LOG.debug(dataSourceRewrappedMessage);
534         } else if (isDbcpDataSource(dataSourceClassName)) {
535             // JIRA dans Tomcat: la dataSource a déjà été mise en cache par org.ofbiz.core.entity.transaction.JNDIFactory
536             // à l'initialisation de com.atlassian.jira.startup.JiraStartupChecklistContextListener
537             // donc on modifie directement l'instance de BasicDataSource déjà présente dans le JNDI.
538             // Et dans certains JIRA la datasource est bien une instance de org.apache.commons.dbcp.BasicDataSource
539             // cf http://groups.google.com/group/javamelody/browse_thread/thread/da8336b908f1e3bd/6cf3048f1f11866e?show_docid=6cf3048f1f11866e
540
541             // et aussi rewrap pour tomee/openejb (cf issue 104),
542             rewrapBasicDataSource(dataSource);
543             LOG.debug(dataSourceRewrappedMessage);
544         } else if ("org.apache.openejb.resource.jdbc.managed.local.ManagedDataSource"
545                 .equals(dataSourceClassName)) {
546             // rewrap pour tomee/openejb plus récents (cf issue 104),
547             rewrapTomEEDataSource(dataSource);
548             LOG.debug(dataSourceRewrappedMessage);
549         } else {
550             LOG.info("Datasource can't be rewrapped: " + jndiName + " of class "
551                     + dataSourceClassName);
552         }
553     }
554
555     private boolean isServerNeedsRewrap(String jndiName) {
556         return glassfish || jboss || weblogic || jndiName.contains("openejb");
557     }
558
559     private boolean isDbcpDataSource(String dataSourceClassName) {
560         return "org.apache.tomcat.dbcp.dbcp.BasicDataSource".equals(dataSourceClassName)
561                 || "org.apache.tomcat.dbcp.dbcp2.BasicDataSource".equals(dataSourceClassName)
562                 || "org.apache.commons.dbcp.BasicDataSource".equals(dataSourceClassName)
563                 || "org.apache.commons.dbcp2.BasicDataSource".equals(dataSourceClassName)
564                 || "org.apache.openejb.resource.jdbc.BasicManagedDataSource"
565                         .equals(dataSourceClassName)
566                 || "org.apache.openejb.resource.jdbc.BasicDataSource".equals(dataSourceClassName);
567     }
568
569     private boolean isJBossOrGlassfishDataSource(String dataSourceClassName) {
570         return jboss
571                 && "org.jboss.resource.adapter.jdbc.WrapperDataSource".equals(dataSourceClassName)
572                 || jboss && "org.jboss.jca.adapters.jdbc.WrapperDataSource"
573                         .equals(dataSourceClassName)
574                 || glassfish && "com.sun.gjc.spi.jdbc40.DataSource40".equals(dataSourceClassName);
575     }
576
577     private boolean isWildfly9DataSource(String dataSourceClassName) {
578         return jboss && "org.jboss.as.connector.subsystems.datasources.WildFlyDataSource"
579                 .equals(dataSourceClassName);
580     }
581
582     private void rewrapWebLogicDataSource(DataSource dataSource) throws IllegalAccessException {
583         if (JdbcWrapperHelper.hasField(dataSource, "delegate")) {
584             // issue #916, for weblogic 12.2.1.4.0
585             final Object delegate = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
586             rewrapWebLogicDataSource((DataSource) delegate);
587         } else {
588             Object jdbcCtx = JdbcWrapperHelper.getFieldValue(dataSource, "jdbcCtx");
589             if (jdbcCtx != null) {
590                 jdbcCtx = createContextProxy((Context) jdbcCtx);
591                 JdbcWrapperHelper.setFieldValue(dataSource, "jdbcCtx", jdbcCtx);
592             }
593             Object driverInstance = JdbcWrapperHelper.getFieldValue(dataSource, "driverInstance");
594             if (driverInstance != null) {
595                 driverInstance = createDriverProxy((Driver) driverInstance);
596                 JdbcWrapperHelper.setFieldValue(dataSource, "driverInstance", driverInstance);
597             }
598         }
599     }
600
601     private void rewrapBasicDataSource(DataSource dataSource) throws IllegalAccessException {
602         // on récupère une connection avant de la refermer,
603         // car sinon la datasource interne n'est pas encore créée
604         // et le rewrap ne peut pas fonctionner
605         try {
606             dataSource.getConnection().close();
607         } catch (final Exception e) {
608             LOG.debug(e.toString());
609             // ce n'est pas grave s'il y a une exception, par exemple parce que la base n'est pas disponible,
610             // car l'essentiel est de créer la datasource
611         }
612         Object innerDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "dataSource");
613         if (innerDataSource != null) {
614             innerDataSource = createDataSourceProxy((DataSource) innerDataSource);
615             JdbcWrapperHelper.setFieldValue(dataSource, "dataSource", innerDataSource);
616         }
617     }
618
619     private void rewrapTomEEDataSource(DataSource dataSource) throws IllegalAccessException {
620         // on récupère une connection avant de la refermer,
621         // car sinon la datasource interne n'est pas encore créée
622         // et le rewrap ne peut pas fonctionner
623         try {
624             dataSource.getConnection().close();
625         } catch (final Exception e) {
626             LOG.debug(e.toString());
627             // ce n'est pas grave s'il y a une exception, par exemple parce que la base n'est pas disponible,
628             // car l'essentiel est de créer la datasource
629         }
630         Object innerDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
631         if (innerDataSource != null) {
632             innerDataSource = createDataSourceProxy((DataSource) innerDataSource);
633             JdbcWrapperHelper.setFieldValue(dataSource, "delegate", innerDataSource);
634         }
635     }
636
637     boolean stop() {
638         boolean ok;
639         try {
640             JdbcWrapperHelper.rebindInitialDataSources(servletContext);
641
642             // si jboss, glassfish ou weblogic avec datasource, on désencapsule aussi les objets wrappés
643             final Map<String, DataSource> rewrappedDataSources = JdbcWrapperHelper
644                     .getRewrappedDataSources();
645             for (final Map.Entry<String, DataSource> entry : rewrappedDataSources.entrySet()) {
646                 final String jndiName = entry.getKey();
647                 final DataSource dataSource = entry.getValue();
648                 unwrapDataSource(jndiName, dataSource);
649             }
650             rewrappedDataSources.clear();
651
652             JdbcWrapperHelper.clearProxyCache();
653
654             ok = true;
655         } catch (final Throwable t) { // NOPMD
656             // ça n'a pas marché, tant pis
657             LOG.debug("rebinding initial datasources failed, skipping", t);
658             ok = false;
659         }
660         return ok;
661     }
662
663     private void unwrapDataSource(String jndiName, DataSource dataSource)
664             throws IllegalAccessException {
665         final String dataSourceClassName = dataSource.getClass().getName();
666         LOG.debug("Datasource needs unwrap: " + jndiName + " of class " + dataSourceClassName);
667         final String dataSourceUnwrappedMessage = "Datasource unwrapped: " + jndiName;
668         if (isJBossOrGlassfishDataSource(dataSourceClassName)) {
669             unwrap(dataSource, "cm", dataSourceUnwrappedMessage);
670         } else if (isWildfly9DataSource(dataSourceClassName)) {
671             unwrap(dataSource, "delegate", dataSourceUnwrappedMessage);
672         } else if (weblogic
673                 && "weblogic.jdbc.common.internal.RmiDataSource".equals(dataSourceClassName)) {
674             if (JdbcWrapperHelper.hasField(dataSource, "delegate")) {
675                 // followup on issue #916, for weblogic 12.2.1.4.0
676                 final Object delegate = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
677                 unwrap(delegate, "jdbcCtx", dataSourceUnwrappedMessage);
678                 unwrap(delegate, "driverInstance", dataSourceUnwrappedMessage);
679             } else {
680                 unwrap(dataSource, "jdbcCtx", dataSourceUnwrappedMessage);
681                 unwrap(dataSource, "driverInstance", dataSourceUnwrappedMessage);
682             }
683         } else if (isDbcpDataSource(dataSourceClassName)) {
684             unwrap(dataSource, "dataSource", dataSourceUnwrappedMessage);
685         }
686     }
687
688     private void unwrap(Object parentObject, String fieldName, String unwrappedMessage)
689             throws IllegalAccessException {
690         final Object proxy = JdbcWrapperHelper.getFieldValue(parentObject, fieldName);
691         if (proxy != null && Proxy.isProxyClass(proxy.getClass())) {
692             InvocationHandler invocationHandler = Proxy.getInvocationHandler(proxy);
693             if (invocationHandler instanceof DelegatingInvocationHandler) {
694                 invocationHandler = ((DelegatingInvocationHandler) invocationHandler).getDelegate();
695                 if (invocationHandler instanceof AbstractInvocationHandler) {
696                     final Object proxiedObject = ((AbstractInvocationHandler<?>) invocationHandler)
697                             .getProxiedObject();
698                     JdbcWrapperHelper.setFieldValue(parentObject, fieldName, proxiedObject);
699                     LOG.debug(unwrappedMessage);
700                 }
701             }
702         }
703     }
704
705     Context createContextProxy(final Context context) {
706         assert context != null;
707         final InvocationHandler invocationHandler = new AbstractInvocationHandler<Context>(
708                 context) {
709             private static final long serialVersionUID = 1L;
710
711             /** {@inheritDoc} */
712             @Override
713             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
714                 Object result = method.invoke(context, args);
715                 if (result instanceof DataSource) {
716                     result = createDataSourceProxy((DataSource) result);
717                 }
718                 return result;
719             }
720         };
721         return createProxy(context, invocationHandler);
722     }
723
724     // pour weblogic
725     private Driver createDriverProxy(final Driver driver) {
726         assert driver != null;
727         final InvocationHandler invocationHandler = new AbstractInvocationHandler<Driver>(driver) {
728             private static final long serialVersionUID = 1L;
729
730             /** {@inheritDoc} */
731             @Override
732             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
733                 Object result = method.invoke(driver, args);
734                 if (result instanceof Connection) {
735                     result = createConnectionProxy((Connection) result);
736                 }
737                 return result;
738             }
739         };
740         return createProxy(driver, invocationHandler);
741     }
742
743     // pour jboss ou glassfish
744     private Object createJavaxConnectionManagerProxy(Object javaxConnectionManager) {
745         assert javaxConnectionManager != null;
746         final InvocationHandler invocationHandler = new ConnectionManagerInvocationHandler(
747                 javaxConnectionManager);
748         return createProxy(javaxConnectionManager, invocationHandler);
749     }
750
751     void rewrapConnection(Connection connection) throws IllegalAccessException {
752         assert connection != null;
753         if (jboss && connection.getClass().getSimpleName().startsWith("WrappedConnection")) {
754             // pour jboss,
755             // result instance de WrappedConnectionJDK6 ou WrappedConnectionJDK5
756             // (attribut "mc" sur classe parente)
757             final Object baseWrapperManagedConnection = JdbcWrapperHelper.getFieldValue(connection,
758                     "mc");
759             final String conFieldName = "con";
760             Connection con = (Connection) JdbcWrapperHelper
761                     .getFieldValue(baseWrapperManagedConnection, conFieldName);
762             // on teste isProxyAlready ici pour raison de perf
763             if (!isProxyAlready(con)) {
764                 con = createConnectionProxy(con);
765                 JdbcWrapperHelper.setFieldValue(baseWrapperManagedConnection, conFieldName, con);
766             }
767         } else if (glassfish && ("com.sun.gjc.spi.jdbc40.ConnectionHolder40"
768                 .equals(connection.getClass().getName())
769                 || "com.sun.gjc.spi.jdbc40.ConnectionWrapper40"
770                         .equals(connection.getClass().getName())
771                 || "com.sun.gjc.spi.jdbc40.ProfiledConnectionWrapper40"
772                         .equals(connection.getClass().getName()))) {
773             // pour glassfish,
774             // result instance de com.sun.gjc.spi.jdbc40.ConnectionHolder40
775             // ou com.sun.gjc.spi.jdbc40.ConnectionWrapper40 selon message dans users' group
776             // (attribut "con" sur classe parente)
777             final String conFieldName = "con";
778             Connection con = (Connection) JdbcWrapperHelper.getFieldValue(connection, conFieldName);
779             // on teste isProxyAlready ici pour raison de perf
780             if (!isProxyAlready(con)) {
781                 con = createConnectionProxy(con);
782                 JdbcWrapperHelper.setFieldValue(connection, conFieldName, con);
783             }
784         }
785     }
786
787     /**
788      * Crée un proxy d'une {@link DataSource} jdbc.
789      * @param dataSource DataSource
790      * @return DataSource
791      */

792     public DataSource createDataSourceProxy(DataSource dataSource) {
793         return createDataSourceProxy(null, dataSource);
794     }
795
796     /**
797      * Crée un proxy d'une {@link DataSource} jdbc.
798      * @param name String
799      * @param dataSource DataSource
800      * @return DataSource
801      */

802     public DataSource createDataSourceProxy(String name, final DataSource dataSource) {
803         assert dataSource != null;
804         JdbcWrapperHelper.pullDataSourceProperties(name, dataSource);
805         final InvocationHandler invocationHandler = new AbstractInvocationHandler<DataSource>(
806                 dataSource) {
807             private static final long serialVersionUID = 1L;
808
809             /** {@inheritDoc} */
810             @Override
811             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
812                 Object result = method.invoke(dataSource, args);
813                 if (result instanceof Connection) {
814                     result = createConnectionProxy((Connection) result);
815                 }
816                 return result;
817             }
818         };
819         return createProxy(dataSource, invocationHandler);
820     }
821
822     Connection createConnectionProxyOrRewrapIfJBossOrGlassfish(Connection connection)
823             throws IllegalAccessException {
824         if (jboss || glassfish) {
825             rewrapConnection(connection);
826             return connection;
827         }
828         return createConnectionProxy(connection);
829     }
830
831     /**
832      * Crée un proxy d'une {@link Connection} jdbc.
833      * @param connection Connection
834      * @return Connection
835      */

836     public Connection createConnectionProxy(Connection connection) {
837         assert connection != null;
838         // même si le counter sql n'est pas affiché on crée un proxy de la connexion
839         // pour avoir les graphiques USED_CONNECTION_COUNT et ACTIVE_CONNECTION_COUNT (cf issue 160)
840         if (isMonitoringDisabled()) {
841             return connection;
842         }
843         final ConnectionInvocationHandler invocationHandler = new ConnectionInvocationHandler(
844                 connection);
845         final Connection result = createProxy(connection, invocationHandler);
846         if (result != connection) {
847             invocationHandler.init();
848         }
849         return result;
850     }
851
852     boolean isSqlMonitoringDisabled() {
853         return isMonitoringDisabled() || !sqlCounter.isDisplayed();
854     }
855
856     private static boolean isMonitoringDisabled() {
857         // on doit réévaluer ici le paramètre, car au départ le servletContext
858         // n'est pas forcément défini si c'est un driver jdbc sans dataSource
859         return Parameter.DISABLED.getValueAsBoolean();
860     }
861
862     Statement createStatementProxy(String query, Statement statement, Connection connection) {
863         assert statement != null;
864         // Si un proxy de connexion a été créé dans un driver jdbc et que par la suite le
865         // servletContext a un paramètre désactivant le monitoring, alors ce n'est pas grave
866         // les requêtes sql seront simplement agrégées dans le counter pour cette connexion
867         // jusqu'à sa fermeture (pour éviter ce détail, il suffirait simplement d'utiliser une
868         // dataSource jdbc et pas un driver).
869         // Rq : on ne réévalue pas le paramètre ici pour raison de performances sur la recherche
870         // dans les paramètres du système, du contexte et du filtre alors que dans 99.999999999%
871         // des exécutions il n'y a pas le paramètre.
872         final InvocationHandler invocationHandler = new StatementInvocationHandler(query, statement,
873                 connection);
874         return createProxy(statement, invocationHandler);
875     }
876
877     static boolean isEqualsMethod(Object methodName, Object[] args) {
878         // == for perf (strings interned: == is ok)
879         return "equals" == methodName && args != null && args.length == 1; // NOPMD
880     }
881
882     static boolean isHashCodeMethod(Object methodName, Object[] args) {
883         // == for perf (strings interned: == is ok)
884         return "hashCode" == methodName && (args == null || args.length == 0); // NOPMD
885     }
886
887     static <T> T createProxy(T object, InvocationHandler invocationHandler) {
888         return createProxy(object, invocationHandler, null);
889     }
890
891     static <T> T createProxy(T object, InvocationHandler invocationHandler,
892             List<Class<?>> interfaces) {
893         if (isProxyAlready(object)) {
894             // si l'objet est déjà un proxy créé pas nous, initialisé par exemple
895             // depuis SessionListener ou MonitoringInitialContextFactory,
896             // alors il ne faut pas faire un proxy du proxy
897             return object;
898         }
899         final InvocationHandler ih = new DelegatingInvocationHandler(invocationHandler);
900         return JdbcWrapperHelper.createProxy(object, ih, interfaces);
901     }
902
903     static boolean isProxyAlready(Object object) {
904         return Proxy.isProxyClass(object.getClass()) && Proxy.getInvocationHandler(object)
905                 .getClass().getName().equals(DelegatingInvocationHandler.class.getName());
906         // utilisation de Proxy.getInvocationHandler(object).getClass().getName().equals(DelegatingInvocationHandler.class.getName())
907         // et non de Proxy.getInvocationHandler(object) instanceof DelegatingInvocationHandler
908         // pour issue 97 (classLoaders différents pour les classes DelegatingInvocationHandler)
909     }
910
911 }
912