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().isEmpty() ? 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 sw) {
171         svltCount++;
172 
173         // Get Request Count (bridge between tomcat 10 and 11 using int vs long
174         Object requestCount = null;
175         try {
176           requestCount = MethodUtils.invokeMethod(sw, "getRequestCount");
177           if (requestCount instanceof Long result) {
178             // tomcat 11+
179             reqCount += result;
180           } else {
181             // tomcat 10
182             reqCount += (int) requestCount;
183           }
184         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
185           logger.error("Unable to find getRequestCount", e);
186         }
187 
188         // Get Error Count (bridge between tomcat 10 and 11 using int vs long
189         try {
190           Object errorCount = MethodUtils.invokeMethod(sw, "getErrorCount");
191           if (errorCount instanceof Long result) {
192             // Tomcat 11+
193             errCount += result;
194           } else {
195             // Tomcat 10
196             errCount += (int) errorCount;
197           }
198         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
199           logger.error("Unable to find getErrorCount", e);
200         }
201 
202         procTime += sw.getProcessingTime();
203         if (requestCount != null) {
204           if (requestCount instanceof Long result && result > 0) {
205             // Tomcat 11+
206             minTime = Math.min(minTime, sw.getMinTime());
207           } else if (requestCount instanceof Integer result && result > 0) {
208             // Tomcat 10
209             minTime = Math.min(minTime, sw.getMinTime());
210           }
211         }
212         maxTime = Math.max(maxTime, sw.getMaxTime());
213       }
214     }
215     app.setServletCount(svltCount);
216     app.setRequestCount(reqCount);
217     app.setErrorCount(errCount);
218     app.setProcessingTime(procTime);
219     app.setMinTime(minTime == Long.MAX_VALUE ? 0 : minTime);
220     app.setMaxTime(maxTime);
221   }
222 
223   /**
224    * Gets the application data source usage scores.
225    *
226    * @param context the context
227    * @param resolver the resolver
228    * @param containerWrapper the container wrapper
229    *
230    * @return the application data source usage scores
231    */
232   public static int[] getApplicationDataSourceUsageScores(Context context,
233       ResourceResolver resolver, ContainerWrapperBean containerWrapper) {
234 
235     logger.debug("Calculating datasource usage score");
236 
237     int[] scores = {0, 0};
238     List<ApplicationResource> appResources;
239     try {
240       appResources = resolver.getApplicationResources(context, containerWrapper);
241     } catch (NamingException e) {
242       throw new RuntimeException(e);
243     }
244     for (ApplicationResource appResource : appResources) {
245       if (appResource.getDataSourceInfo() != null) {
246         scores[0] = Math.max(scores[0], appResource.getDataSourceInfo().getBusyScore());
247         scores[1] = Math.max(scores[1], appResource.getDataSourceInfo().getEstablishedScore());
248       }
249     }
250     return scores;
251   }
252 
253   /**
254    * Gets the application session.
255    *
256    * @param session the session
257    * @param calcSize the calc size
258    * @param addAttributes the add attributes
259    *
260    * @return the application session
261    */
262   public static ApplicationSession getApplicationSession(Session session, boolean calcSize,
263       boolean addAttributes) {
264 
265     ApplicationSession sbean = null;
266     if (session != null && session.isValid()) {
267       sbean = new ApplicationSession();
268 
269       sbean.setId(session.getId());
270       sbean.setCreationTime(new Date(session.getCreationTime()));
271       sbean.setLastAccessTime(new Date(session.getLastAccessedTime()));
272       sbean.setMaxIdleTime(session.getMaxInactiveInterval() * 1000);
273       sbean.setManagerType(session.getManager().getClass().getName());
274       sbean.setInfo(session.getClass().getSimpleName());
275 
276       boolean sessionSerializable = true;
277       int attributeCount = 0;
278       long size = 0;
279 
280       HttpSession httpSession = session.getSession();
281       Set<Object> processedObjects = new HashSet<>(1000);
282 
283       // Exclude references back to the session itself
284       processedObjects.add(httpSession);
285       try {
286         for (String name : Collections.list(httpSession.getAttributeNames())) {
287           Object obj = httpSession.getAttribute(name);
288           sessionSerializable = sessionSerializable && obj instanceof Serializable;
289 
290           long objSize = 0;
291           if (calcSize) {
292             try {
293               objSize += Instruments.sizeOf(name, processedObjects);
294               objSize += Instruments.sizeOf(obj, processedObjects);
295             } catch (Exception ex) {
296               logger.error("Cannot estimate size of attribute '{}'", name, ex);
297             }
298           }
299 
300           if (addAttributes) {
301             Attribute saBean = new Attribute();
302             saBean.setName(name);
303             saBean.setType(ClassUtils.getQualifiedName(obj.getClass()));
304             saBean.setValue(obj);
305             saBean.setSize(objSize);
306             saBean.setSerializable(obj instanceof Serializable);
307             sbean.addAttribute(saBean);
308           }
309           attributeCount++;
310           size += objSize;
311         }
312         String lastAccessedIp =
313             (String) httpSession.getAttribute(ApplicationSession.LAST_ACCESSED_BY_IP);
314         if (lastAccessedIp != null) {
315           sbean.setLastAccessedIp(lastAccessedIp);
316           sbean.setLastAccessedIpLocale(
317               (Locale) httpSession.getAttribute(ApplicationSession.LAST_ACCESSED_LOCALE));
318         }
319 
320       } catch (IllegalStateException e) {
321         logger.info("Session appears to be invalidated, ignore");
322         logger.trace("", e);
323       }
324 
325       sbean.setObjectCount(attributeCount);
326       sbean.setSize(size);
327       sbean.setSerializable(sessionSerializable);
328     }
329 
330     return sbean;
331   }
332 
333   /**
334    * Gets the application attributes.
335    *
336    * @param context the context
337    *
338    * @return the application attributes
339    */
340   public static List<Attribute> getApplicationAttributes(Context context) {
341     List<Attribute> attrs = new ArrayList<>();
342     ServletContext servletCtx = context.getServletContext();
343     for (String attrName : Collections.list(servletCtx.getAttributeNames())) {
344       Object attrValue = servletCtx.getAttribute(attrName);
345 
346       Attribute attr = new Attribute();
347       attr.setName(attrName);
348       attr.setValue(attrValue);
349       attr.setType(ClassUtils.getQualifiedName(attrValue.getClass()));
350       attrs.add(attr);
351     }
352     return attrs;
353   }
354 
355   /**
356    * Gets the application init params.
357    *
358    * @param context the context
359    * @param containerWrapper the container wrapper
360    *
361    * @return the application init params
362    */
363   public static List<ApplicationParam> getApplicationInitParams(Context context,
364       ContainerWrapperBean containerWrapper) {
365 
366     return containerWrapper.getTomcatContainer().getApplicationInitParams(context);
367   }
368 
369   /**
370    * Gets the application servlet.
371    *
372    * @param context the context
373    * @param servletName the servlet name
374    *
375    * @return the application servlet
376    */
377   public static ServletInfo getApplicationServlet(Context context, String servletName) {
378     Container container = context.findChild(servletName);
379 
380     if (container instanceof Wrapper wrapper) {
381       return getServletInfo(wrapper, context.getName());
382     }
383     return null;
384   }
385 
386   /**
387    * Gets the servlet info.
388    *
389    * @param wrapper the wrapper
390    * @param contextName the context name
391    *
392    * @return the servlet info
393    */
394   private static ServletInfo getServletInfo(Wrapper wrapper, String contextName) {
395     ServletInfo si = new ServletInfo();
396     si.setApplicationName(!contextName.isEmpty() ? contextName : "/");
397     si.setServletName(wrapper.getName());
398     si.setServletClass(wrapper.getServletClass());
399     si.setAvailable(!wrapper.isUnavailable());
400     si.setLoadOnStartup(wrapper.getLoadOnStartup());
401     si.setRunAs(wrapper.getRunAs());
402     si.getMappings().addAll(Arrays.asList(wrapper.findMappings()));
403     if (wrapper instanceof StandardWrapper sw) {
404       si.setAllocationCount(sw.getCountAllocated());
405 
406       // Get Error Count (bridge between tomcat 10 and 11 using int vs long
407       try {
408         Object errorCount = MethodUtils.invokeMethod(sw, "getErrorCount");
409         if (errorCount instanceof Long result) {
410           // Tomcat 11+
411           si.setErrorCount(result);
412         } else {
413           // Tomcat 10
414           si.setErrorCount((int) errorCount);
415         }
416       } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
417         logger.error("Unable to find getErrorCount", e);
418       }
419 
420       si.setLoadTime(sw.getLoadTime());
421       si.setMaxTime(sw.getMaxTime());
422       si.setMinTime(sw.getMinTime() == Long.MAX_VALUE ? 0 : sw.getMinTime());
423       si.setProcessingTime(sw.getProcessingTime());
424 
425       // Get Request Count (bridge between tomcat 10 and 11 using int vs long
426       try {
427         Object requestCount = MethodUtils.invokeMethod(sw, "getRequestCount");
428         if (requestCount instanceof Long result) {
429           // Tomcat 11+
430           si.setRequestCount(result);
431         } else {
432           // Tomcat 10
433           si.setRequestCount((int) requestCount);
434         }
435       } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
436         logger.error("Unable to find getRequestCount", e);
437       }
438 
439     }
440     return si;
441   }
442 
443   /**
444    * Gets the application servlets.
445    *
446    * @param context the context
447    *
448    * @return the application servlets
449    */
450   public static List<ServletInfo> getApplicationServlets(Context context) {
451     Container[] cns = context.findChildren();
452     List<ServletInfo> servlets = new ArrayList<>(cns.length);
453     for (Container container : cns) {
454       if (container instanceof Wrapper wrapper) {
455         servlets.add(getServletInfo(wrapper, context.getName()));
456       }
457     }
458     return servlets;
459   }
460 
461   /**
462    * Gets the application servlet maps.
463    *
464    * @param context the context
465    *
466    * @return the application servlet maps
467    */
468   public static List<ServletMapping> getApplicationServletMaps(Context context) {
469     String[] sms = context.findServletMappings();
470     List<ServletMapping> servletMaps = new ArrayList<>(sms.length);
471     for (String servletMapping : sms) {
472       if (servletMapping != null) {
473         String sn = context.findServletMapping(servletMapping);
474         if (sn != null) {
475           ServletMapping sm = new ServletMapping();
476           sm.setApplicationName(!context.getName().isEmpty() ? context.getName() : "/");
477           sm.setUrl(servletMapping);
478           sm.setServletName(sn);
479           Container container = context.findChild(sn);
480           if (container instanceof Wrapper wrapper) {
481             sm.setServletClass(wrapper.getServletClass());
482             sm.setAvailable(!wrapper.isUnavailable());
483           }
484           servletMaps.add(sm);
485         }
486       }
487     }
488     return servletMaps;
489   }
490 
491   /**
492    * Gets the application filters.
493    *
494    * @param context the context
495    * @param containerWrapper the container wrapper
496    *
497    * @return the application filters
498    */
499   public static List<FilterInfo> getApplicationFilters(Context context,
500       ContainerWrapperBean containerWrapper) {
501     return containerWrapper.getTomcatContainer().getApplicationFilters(context);
502   }
503 
504 }