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
105         StatementInvocationHandler(String query, Statement statement) {
106             super();
107             assert statement != null;
108
109             this.requestName = query;
110             this.statement = statement;
111         }
112
113         /** {@inheritDoc} */
114         @Override
115         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
116             // performance : on évite method.invoke pour equals & hashCode
117             final String methodName = method.getName();
118             if (isEqualsMethod(methodName, args)) {
119                 return statement.equals(args[0]);
120             } else if (isHashCodeMethod(methodName, args)) {
121                 return statement.hashCode();
122             } else if (methodName.startsWith("execute")) {
123                 if (isFirstArgAString(args)) {
124                     // la méthode est du type executeQuery(String), executeUpdate(String),
125                     // executeUpdate(String, ...) ou execute(String sql),
126                     // alors la requête sql est le premier argument (et pas query)
127                     requestName = (String) args[0];
128                 } else if (("executeBatch".equals(methodName)
129                         || "executeLargeBatch".equals(methodName)) && requestName != null
130                         && !requestName.startsWith("/* BATCH */ ")) {
131                     // if executeBatch, add a prefix in the request name to explain that
132                     // 1 batch "hit" is equivalent to several exec of the request in the db.
133
134                     // requestName may be null if executeBatch()
135                     // without prepareStatement(String) or addBatch(String)
136                     requestName = "/* BATCH */ " + requestName;
137                 }
138
139                 // si on n'a pas trouvé la requête, on prend "null"
140                 requestName = String.valueOf(requestName);
141
142                 return doExecute(requestName, statement, method, args);
143             } else if ("addBatch".equals(methodName) && isFirstArgAString(args)) {
144                 // Bien que déconseillée la méthode est addBatch(String),
145                 // la requête sql est alors le premier argument
146                 // (elle sera utilisée lors de l'appel à executeBatch())
147
148                 // Rq : on ne conserve que la dernière requête de addBatch.
149                 // Rq : si addBatch(String) est appelée, puis que executeUpdate(String)
150                 // la requête du batch est correctement ignorée ci-dessus.
151                 // Rq : si connection.prepareStatement(String).addBatch(String) puis executeUpdate()
152                 // sont appelées (et pas executeBatch()) alors la requête conservée est
153                 // faussement celle du batch mais l'application cloche grave.
154                 requestName = (String) args[0];
155             }
156
157             // ce n'est pas une méthode executeXxx du Statement
158             return method.invoke(statement, args);
159         }
160
161         private boolean isFirstArgAString(Object[] args) {
162             return args != null && args.length > 0 && args[0] instanceof String;
163         }
164     }
165
166     /**
167      * Handler de proxy d'une {@link Connection} jdbc.
168      */

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

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

784     public DataSource createDataSourceProxy(DataSource dataSource) {
785         return createDataSourceProxy(null, dataSource);
786     }
787
788     /**
789      * Crée un proxy d'une {@link DataSource} jdbc.
790      * @param name String
791      * @param dataSource DataSource
792      * @return DataSource
793      */

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

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