View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe.tools;
12  
13  import jakarta.servlet.ServletContext;
14  import jakarta.servlet.http.HttpSession;
15  
16  import java.io.Serializable;
17  import java.lang.reflect.InvocationTargetException;
18  import java.util.ArrayList;
19  import java.util.Arrays;
20  import java.util.Collections;
21  import java.util.Date;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Set;
26  
27  import javax.naming.NamingException;
28  
29  import org.apache.catalina.Container;
30  import org.apache.catalina.Context;
31  import org.apache.catalina.Session;
32  import org.apache.catalina.Wrapper;
33  import org.apache.catalina.core.StandardWrapper;
34  import org.apache.commons.lang3.reflect.MethodUtils;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  import org.springframework.util.ClassUtils;
38  
39  import psiprobe.beans.ContainerWrapperBean;
40  import psiprobe.beans.ResourceResolver;
41  import psiprobe.model.Application;
42  import psiprobe.model.ApplicationParam;
43  import psiprobe.model.ApplicationResource;
44  import psiprobe.model.ApplicationSession;
45  import psiprobe.model.Attribute;
46  import psiprobe.model.FilterInfo;
47  import psiprobe.model.ServletInfo;
48  import psiprobe.model.ServletMapping;
49  
50  /**
51   * The Class ApplicationUtils.
52   */
53  public final class ApplicationUtils {
54  
55    /** The Constant logger. */
56    private static final Logger logger = LoggerFactory.getLogger(ApplicationUtils.class);
57  
58    /**
59     * Prevent Instantiation.
60     */
61    private ApplicationUtils() {
62      // Prevent Instantiation
63    }
64  
65    /**
66     * Gets the application.
67     *
68     * @param context the context
69     * @param containerWrapper the container wrapper
70     *
71     * @return the application
72     */
73    public static Application getApplication(Context context, ContainerWrapperBean containerWrapper) {
74      return getApplication(context, null, false, containerWrapper);
75    }
76  
77    /**
78     * Creates Application instance from Tomcat Context object. If ResourceResolver is passed the
79     * method will also collect additional information about the application such as session count,
80     * session attribute count, application attribute count, servlet count, servlet stats summary and
81     * datasource usage summary. Collecting additional information can be CPU intensive and time
82     * consuming so this should be avoided unless absolutely required. Some datasource implementations
83     * (c3p0) are known to be prone to internal deadlocks, so this method can also hang is datasource
84     * usage stats is to be collected.
85     *
86     * @param context the context from which to create the Application
87     * @param resourceResolver the resolver to use for resources associated with the given context
88     * @param calcSize flag which controls whether to calculate session size
89     * @param containerWrapper the wrapper for the context's root containing server
90     *
91     * @return Application object
92     */
93    public static Application getApplication(Context context, ResourceResolver resourceResolver,
94        boolean calcSize, ContainerWrapperBean containerWrapper) {
95  
96      // ContainerWrapperBean containerWrapper
97      logger.debug("Querying webapp: {}", context.getName());
98  
99      Application app = new Application();
100     app.setName(context.getName().length() > 0 ? context.getName() : "/");
101     app.setDocBase(context.getDocBase());
102     app.setDisplayName(context.getDisplayName());
103 
104     app.setAvailable(containerWrapper.getTomcatContainer().getAvailable(context));
105     app.setDistributable(context.getDistributable());
106     app.setSessionTimeout(context.getSessionTimeout());
107     app.setServletVersion(context.getServletContext().getMajorVersion() + "."
108         + context.getServletContext().getMinorVersion());
109 
110     if (resourceResolver != null) {
111       logger.debug("counting servlet attributes");
112 
113       app.setContextAttributeCount(
114           Collections.list(context.getServletContext().getAttributeNames()).size());
115 
116       if (app.isAvailable()) {
117         logger.debug("collecting session information");
118 
119         app.setSessionCount(context.getManager().findSessions().length);
120 
121         boolean serializable = true;
122         long sessionAttributeCount = 0;
123         long size = 0;
124 
125         for (Session session : context.getManager().findSessions()) {
126           ApplicationSession appSession = getApplicationSession(session, calcSize, false);
127           if (appSession != null) {
128             sessionAttributeCount += appSession.getObjectCount();
129             serializable = serializable && appSession.isSerializable();
130             size += appSession.getSize();
131           }
132         }
133         app.setSerializable(serializable);
134         app.setSessionAttributeCount(sessionAttributeCount);
135         app.setSize(size);
136       }
137 
138       logger.debug("aggregating servlet stats");
139 
140       collectApplicationServletStats(context, app);
141 
142       if (resourceResolver.supportsPrivateResources() && app.isAvailable()) {
143         int[] scores =
144             getApplicationDataSourceUsageScores(context, resourceResolver, containerWrapper);
145         app.setDataSourceBusyScore(scores[0]);
146         app.setDataSourceEstablishedScore(scores[1]);
147       }
148     }
149 
150     return app;
151   }
152 
153   /**
154    * Calculates Sum of requestCount, errorCount and processingTime for all servlets for the given
155    * application. It also works out minimum value of minTime and maximum value for maxTime for all
156    * servlets.
157    *
158    * @param context the context whose stats will be collected
159    * @param app the application in which to store the collected stats
160    */
161   public static void collectApplicationServletStats(Context context, Application app) {
162     int svltCount = 0;
163     long reqCount = 0;
164     long errCount = 0;
165     long procTime = 0;
166     long minTime = Long.MAX_VALUE;
167     long maxTime = 0;
168 
169     for (Container container : context.findChildren()) {
170       if (container instanceof StandardWrapper) {
171         StandardWrapper sw = (StandardWrapper) container;
172         svltCount++;
173 
174         // Get Request Count (bridge between tomcat 10 and 11 using int vs long
175         Object requestCount = null;
176         try {
177           requestCount = MethodUtils.invokeMethod(sw, "getRequestCount");
178           if (requestCount instanceof Long) {
179             // tomcat 11+
180             reqCount += (long) requestCount;
181           } else {
182             // tomcat 10
183             reqCount += (int) requestCount;
184           }
185         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
186           logger.error("Unable to find getRequestCount", e);
187         }
188 
189         // Get Error Count (bridge between tomcat 10 and 11 using int vs long
190         try {
191           Object errorCount = MethodUtils.invokeMethod(sw, "getErrorCount");
192           if (errorCount instanceof Long) {
193             // Tomcat 11+
194             errCount += (long) errorCount;
195           } else {
196             // Tomcat 10
197             errCount += (int) errorCount;
198           }
199         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
200           logger.error("Unable to find getErrorCount", e);
201         }
202 
203         procTime += sw.getProcessingTime();
204         if (requestCount != null) {
205           if (requestCount instanceof Long && (long) requestCount > 0) {
206             // Tomcat 11+
207             minTime = Math.min(minTime, sw.getMinTime());
208           } else if (requestCount instanceof Integer && (int) requestCount > 0) {
209             // Tomcat 10
210             minTime = Math.min(minTime, sw.getMinTime());
211           }
212         }
213         maxTime = Math.max(maxTime, sw.getMaxTime());
214       }
215     }
216     app.setServletCount(svltCount);
217     app.setRequestCount(reqCount);
218     app.setErrorCount(errCount);
219     app.setProcessingTime(procTime);
220     app.setMinTime(minTime == Long.MAX_VALUE ? 0 : minTime);
221     app.setMaxTime(maxTime);
222   }
223 
224   /**
225    * Gets the application data source usage scores.
226    *
227    * @param context the context
228    * @param resolver the resolver
229    * @param containerWrapper the container wrapper
230    *
231    * @return the application data source usage scores
232    */
233   public static int[] getApplicationDataSourceUsageScores(Context context,
234       ResourceResolver resolver, ContainerWrapperBean containerWrapper) {
235 
236     logger.debug("Calculating datasource usage score");
237 
238     int[] scores = {0, 0};
239     List<ApplicationResource> appResources;
240     try {
241       appResources = resolver.getApplicationResources(context, containerWrapper);
242     } catch (NamingException e) {
243       throw new RuntimeException(e);
244     }
245     for (ApplicationResource appResource : appResources) {
246       if (appResource.getDataSourceInfo() != null) {
247         scores[0] = Math.max(scores[0], appResource.getDataSourceInfo().getBusyScore());
248         scores[1] = Math.max(scores[1], appResource.getDataSourceInfo().getEstablishedScore());
249       }
250     }
251     return scores;
252   }
253 
254   /**
255    * Gets the application session.
256    *
257    * @param session the session
258    * @param calcSize the calc size
259    * @param addAttributes the add attributes
260    *
261    * @return the application session
262    */
263   public static ApplicationSession getApplicationSession(Session session, boolean calcSize,
264       boolean addAttributes) {
265 
266     ApplicationSession sbean = null;
267     if (session != null && session.isValid()) {
268       sbean = new ApplicationSession();
269 
270       sbean.setId(session.getId());
271       sbean.setCreationTime(new Date(session.getCreationTime()));
272       sbean.setLastAccessTime(new Date(session.getLastAccessedTime()));
273       sbean.setMaxIdleTime(session.getMaxInactiveInterval() * 1000);
274       sbean.setManagerType(session.getManager().getClass().getName());
275       sbean.setInfo(session.getClass().getSimpleName());
276 
277       boolean sessionSerializable = true;
278       int attributeCount = 0;
279       long size = 0;
280 
281       HttpSession httpSession = session.getSession();
282       Set<Object> processedObjects = new HashSet<>(1000);
283 
284       // Exclude references back to the session itself
285       processedObjects.add(httpSession);
286       try {
287         for (String name : Collections.list(httpSession.getAttributeNames())) {
288           Object obj = httpSession.getAttribute(name);
289           sessionSerializable = sessionSerializable && obj instanceof Serializable;
290 
291           long objSize = 0;
292           if (calcSize) {
293             try {
294               objSize += Instruments.sizeOf(name, processedObjects);
295               objSize += Instruments.sizeOf(obj, processedObjects);
296             } catch (Exception ex) {
297               logger.error("Cannot estimate size of attribute '{}'", name, ex);
298             }
299           }
300 
301           if (addAttributes) {
302             Attribute saBean = new Attribute();
303             saBean.setName(name);
304             saBean.setType(ClassUtils.getQualifiedName(obj.getClass()));
305             saBean.setValue(obj);
306             saBean.setSize(objSize);
307             saBean.setSerializable(obj instanceof Serializable);
308             sbean.addAttribute(saBean);
309           }
310           attributeCount++;
311           size += objSize;
312         }
313         String lastAccessedIp =
314             (String) httpSession.getAttribute(ApplicationSession.LAST_ACCESSED_BY_IP);
315         if (lastAccessedIp != null) {
316           sbean.setLastAccessedIp(lastAccessedIp);
317           sbean.setLastAccessedIpLocale(
318               (Locale) httpSession.getAttribute(ApplicationSession.LAST_ACCESSED_LOCALE));
319         }
320 
321       } catch (IllegalStateException e) {
322         logger.info("Session appears to be invalidated, ignore");
323         logger.trace("", e);
324       }
325 
326       sbean.setObjectCount(attributeCount);
327       sbean.setSize(size);
328       sbean.setSerializable(sessionSerializable);
329     }
330 
331     return sbean;
332   }
333 
334   /**
335    * Gets the application attributes.
336    *
337    * @param context the context
338    *
339    * @return the application attributes
340    */
341   public static List<Attribute> getApplicationAttributes(Context context) {
342     List<Attribute> attrs = new ArrayList<>();
343     ServletContext servletCtx = context.getServletContext();
344     for (String attrName : Collections.list(servletCtx.getAttributeNames())) {
345       Object attrValue = servletCtx.getAttribute(attrName);
346 
347       Attribute attr = new Attribute();
348       attr.setName(attrName);
349       attr.setValue(attrValue);
350       attr.setType(ClassUtils.getQualifiedName(attrValue.getClass()));
351       attrs.add(attr);
352     }
353     return attrs;
354   }
355 
356   /**
357    * Gets the application init params.
358    *
359    * @param context the context
360    * @param containerWrapper the container wrapper
361    *
362    * @return the application init params
363    */
364   public static List<ApplicationParam> getApplicationInitParams(Context context,
365       ContainerWrapperBean containerWrapper) {
366 
367     return containerWrapper.getTomcatContainer().getApplicationInitParams(context);
368   }
369 
370   /**
371    * Gets the application servlet.
372    *
373    * @param context the context
374    * @param servletName the servlet name
375    *
376    * @return the application servlet
377    */
378   public static ServletInfo getApplicationServlet(Context context, String servletName) {
379     Container container = context.findChild(servletName);
380 
381     if (container instanceof Wrapper) {
382       Wrapper wrapper = (Wrapper) container;
383       return getServletInfo(wrapper, context.getName());
384     }
385     return null;
386   }
387 
388   /**
389    * Gets the servlet info.
390    *
391    * @param wrapper the wrapper
392    * @param contextName the context name
393    *
394    * @return the servlet info
395    */
396   private static ServletInfo getServletInfo(Wrapper wrapper, String contextName) {
397     ServletInfo si = new ServletInfo();
398     si.setApplicationName(contextName.length() > 0 ? contextName : "/");
399     si.setServletName(wrapper.getName());
400     si.setServletClass(wrapper.getServletClass());
401     si.setAvailable(!wrapper.isUnavailable());
402     si.setLoadOnStartup(wrapper.getLoadOnStartup());
403     si.setRunAs(wrapper.getRunAs());
404     si.getMappings().addAll(Arrays.asList(wrapper.findMappings()));
405     if (wrapper instanceof StandardWrapper) {
406       StandardWrapper sw = (StandardWrapper) wrapper;
407       si.setAllocationCount(sw.getCountAllocated());
408 
409       // Get Error Count (bridge between tomcat 10 and 11 using int vs long
410       try {
411         Object errorCount = MethodUtils.invokeMethod(sw, "getErrorCount");
412         if (errorCount instanceof Long) {
413           // Tomcat 11+
414           si.setErrorCount((long) errorCount);
415         } else {
416           // Tomcat 10
417           si.setErrorCount((int) errorCount);
418         }
419       } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
420         logger.error("Unable to find getErrorCount", e);
421       }
422 
423       si.setLoadTime(sw.getLoadTime());
424       si.setMaxTime(sw.getMaxTime());
425       si.setMinTime(sw.getMinTime() == Long.MAX_VALUE ? 0 : sw.getMinTime());
426       si.setProcessingTime(sw.getProcessingTime());
427 
428       // Get Request Count (bridge between tomcat 10 and 11 using int vs long
429       try {
430         Object requestCount = MethodUtils.invokeMethod(sw, "getRequestCount");
431         if (requestCount instanceof Long) {
432           // Tomcat 11+
433           si.setRequestCount((long) requestCount);
434         } else {
435           // Tomcat 10
436           si.setRequestCount((int) requestCount);
437         }
438       } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
439         logger.error("Unable to find getRequestCount", e);
440       }
441 
442     }
443     return si;
444   }
445 
446   /**
447    * Gets the application servlets.
448    *
449    * @param context the context
450    *
451    * @return the application servlets
452    */
453   public static List<ServletInfo> getApplicationServlets(Context context) {
454     Container[] cns = context.findChildren();
455     List<ServletInfo> servlets = new ArrayList<>(cns.length);
456     for (Container container : cns) {
457       if (container instanceof Wrapper) {
458         Wrapper wrapper = (Wrapper) container;
459         servlets.add(getServletInfo(wrapper, context.getName()));
460       }
461     }
462     return servlets;
463   }
464 
465   /**
466    * Gets the application servlet maps.
467    *
468    * @param context the context
469    *
470    * @return the application servlet maps
471    */
472   public static List<ServletMapping> getApplicationServletMaps(Context context) {
473     String[] sms = context.findServletMappings();
474     List<ServletMapping> servletMaps = new ArrayList<>(sms.length);
475     for (String servletMapping : sms) {
476       if (servletMapping != null) {
477         String sn = context.findServletMapping(servletMapping);
478         if (sn != null) {
479           ServletMapping sm = new ServletMapping();
480           sm.setApplicationName(context.getName().length() > 0 ? context.getName() : "/");
481           sm.setUrl(servletMapping);
482           sm.setServletName(sn);
483           Container container = context.findChild(sn);
484           if (container instanceof Wrapper) {
485             Wrapper wrapper = (Wrapper) container;
486             sm.setServletClass(wrapper.getServletClass());
487             sm.setAvailable(!wrapper.isUnavailable());
488           }
489           servletMaps.add(sm);
490         }
491       }
492     }
493     return servletMaps;
494   }
495 
496   /**
497    * Gets the application filters.
498    *
499    * @param context the context
500    * @param containerWrapper the container wrapper
501    *
502    * @return the application filters
503    */
504   public static List<FilterInfo> getApplicationFilters(Context context,
505       ContainerWrapperBean containerWrapper) {
506     return containerWrapper.getTomcatContainer().getApplicationFilters(context);
507   }
508 
509 }