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.internal.web; // NOPMD
19
20 import java.io.IOException;
21 import java.util.Collections;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28
29 import net.bull.javamelody.SessionListener;
30 import net.bull.javamelody.internal.common.HttpParameter;
31 import net.bull.javamelody.internal.common.HttpPart;
32 import net.bull.javamelody.internal.common.I18N;
33 import net.bull.javamelody.internal.model.Action;
34 import net.bull.javamelody.internal.model.Collector;
35 import net.bull.javamelody.internal.model.CollectorServer;
36 import net.bull.javamelody.internal.model.Counter;
37 import net.bull.javamelody.internal.model.CounterRequestContext;
38 import net.bull.javamelody.internal.model.DatabaseInformations;
39 import net.bull.javamelody.internal.model.HeapHistogram;
40 import net.bull.javamelody.internal.model.JavaInformations;
41 import net.bull.javamelody.internal.model.JndiBinding;
42 import net.bull.javamelody.internal.model.MBeanNode;
43 import net.bull.javamelody.internal.model.MBeans;
44 import net.bull.javamelody.internal.model.ProcessInformations;
45 import net.bull.javamelody.internal.model.Range;
46 import net.bull.javamelody.internal.model.SamplingProfiler.SampledMethod;
47 import net.bull.javamelody.internal.model.SessionInformations;
48 import net.bull.javamelody.internal.model.VirtualMachine;
49 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestAttribute;
50 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestParameter;
51 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestPart;
52 import net.bull.javamelody.internal.web.pdf.PdfOtherReport;
53 import net.bull.javamelody.internal.web.pdf.PdfReport;
54
55 /**
56  * Contrôleur au sens MVC de l'ihm de monitoring pour la partie pdf.
57  * @author Emeric Vernat
58  */

59 class PdfController {
60     private static final String RANGE_KEY = "range";
61     private static final String JAVA_INFORMATIONS_LIST_KEY = "javaInformationsList";
62     private static final RequestToMethodMapper<PdfController> REQUEST_TO_METHOD_MAPPER = new RequestToMethodMapper<>(
63             PdfController.class);
64     private final HttpCookieManager httpCookieManager = new HttpCookieManager();
65     private final Collector collector;
66     private final CollectorServer collectorServer;
67     private PdfOtherReport pdfOtherReport;
68
69     PdfController(Collector collector, CollectorServer collectorServer) {
70         super();
71         assert collector != null;
72         this.collector = collector;
73         this.collectorServer = collectorServer;
74     }
75
76     void doPdf(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
77             List<JavaInformations> javaInformationsList) throws IOException {
78         addPdfContentTypeAndDisposition(httpRequest, httpResponse);
79         try {
80             final String part = HttpParameter.PART.getParameterFrom(httpRequest);
81             final Range range = httpCookieManager.getRange(httpRequest, httpResponse);
82             if (PdfReport.shouldUseEnglishInsteadOfUkrainian()) {
83                 I18N.bindLocale(Locale.ENGLISH);
84             }
85             if (part == null) {
86                 if (!isFromCollectorServer() && !collector.isStopped()) {
87                     // avant de faire l'affichage on fait une collecte,  pour que les courbes
88                     // et les compteurs par jour soit à jour avec les dernières requêtes
89                     // sauf si c'est un serveur de collecte
90                     // ou si la page de monitoring d'une webapp monitorée par un serveur de collecte est appelée par erreur
91                     collector.collectLocalContextWithoutErrors();
92                 }
93
94                 final PdfReport pdfReport = new PdfReport(collector, isFromCollectorServer(),
95                         javaInformationsList, range, httpResponse.getOutputStream());
96                 pdfReport.toPdf();
97             } else {
98                 this.pdfOtherReport = new PdfOtherReport(getApplication(),
99                         httpResponse.getOutputStream());
100                 // range et javaInformationsList sont passés en attribut de la requête avec @RequestAttribute
101                 // au lieu d'être en paramètre de new PdfOtherReport
102                 httpRequest.setAttribute(RANGE_KEY, range);
103                 httpRequest.setAttribute(JAVA_INFORMATIONS_LIST_KEY, javaInformationsList);
104                 REQUEST_TO_METHOD_MAPPER.invoke(httpRequest, this);
105             }
106         } finally {
107             httpResponse.getOutputStream().flush();
108         }
109     }
110
111     @RequestPart(HttpPart.CURRENT_REQUESTS)
112     void doCurrentRequests(
113             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList,
114             @RequestAttribute(RANGE_KEY) Range range) throws IOException {
115         final List<Counter> counters = collector.getRangeCountersToBeDisplayed(range);
116         final Map<JavaInformations, List<CounterRequestContext>> currentRequests;
117         if (!isFromCollectorServer()) {
118             // on est dans l'application monitorée
119             assert collectorServer == null;
120             assert javaInformationsList.size() == 1;
121             final JavaInformations javaInformations = javaInformationsList.get(0);
122             final List<CounterRequestContext> rootCurrentContexts = collector
123                     .getRootCurrentContexts(counters);
124             currentRequests = Collections.singletonMap(javaInformations, rootCurrentContexts);
125         } else {
126             currentRequests = collectorServer.collectCurrentRequests(collector.getApplication());
127         }
128         final long timeOfSnapshot = System.currentTimeMillis();
129         pdfOtherReport.writeAllCurrentRequestsAsPart(currentRequests, collector, counters,
130                 timeOfSnapshot);
131     }
132
133     @RequestPart(HttpPart.THREADS)
134     void doThreads(
135             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList)
136             throws IOException {
137         pdfOtherReport.writeThreads(javaInformationsList);
138     }
139
140     @RequestPart(HttpPart.RUNTIME_DEPENDENCIES)
141     void doRuntimeDependencies(@RequestAttribute(RANGE_KEY) Range range,
142             @RequestParameter(HttpParameter.COUNTER) String counterName) throws IOException {
143         final Counter counter = collector.getRangeCounter(range, counterName);
144         pdfOtherReport.writeRuntimeDependencies(counter, range);
145     }
146
147     @RequestPart(HttpPart.COUNTER_SUMMARY_PER_CLASS)
148     void doCounterSummaryPerClass(@RequestAttribute(RANGE_KEY) Range range,
149             @RequestParameter(HttpParameter.GRAPH) String requestId,
150             @RequestParameter(HttpParameter.COUNTER) String counterName) throws IOException {
151         final Counter counter = collector.getRangeCounter(range, counterName);
152         pdfOtherReport.writeCounterSummaryPerClass(collector, counter, requestId, range);
153     }
154
155     @RequestPart(HttpPart.GRAPH)
156     void doRequestAndGraphDetail(@RequestAttribute(RANGE_KEY) Range range,
157             @RequestParameter(HttpParameter.GRAPH) String requestId) throws IOException {
158         pdfOtherReport.writeRequestAndGraphDetail(collector, collectorServer, range, requestId);
159     }
160
161     @RequestPart(HttpPart.SESSIONS)
162     void doSessions() throws IOException {
163         // par sécurité
164         Action.checkSystemActionsEnabled();
165         final List<SessionInformations> sessionsInformations;
166         if (!isFromCollectorServer()) {
167             sessionsInformations = SessionListener.getAllSessionsInformations();
168         } else {
169             sessionsInformations = collectorServer.collectSessionInformations(getApplication(),
170                     null);
171         }
172         pdfOtherReport.writeSessionInformations(sessionsInformations);
173     }
174
175     @RequestPart(HttpPart.HOTSPOTS)
176     void doHotspots() throws IOException {
177         // par sécurité
178         Action.checkSystemActionsEnabled();
179         if (!isFromCollectorServer()) {
180             final List<SampledMethod> hotspots = collector.getHotspots();
181             pdfOtherReport.writeHotspots(hotspots);
182         } else {
183             final List<SampledMethod> hotspots = collectorServer.collectHotspots(getApplication());
184             pdfOtherReport.writeHotspots(hotspots);
185         }
186     }
187
188     @RequestPart(HttpPart.PROCESSES)
189     void doProcesses() throws IOException {
190         // par sécurité
191         Action.checkSystemActionsEnabled();
192         if (!isFromCollectorServer()) {
193             final List<ProcessInformations> processInformations = ProcessInformations
194                     .buildProcessInformations();
195             pdfOtherReport.writeProcessInformations(processInformations);
196         } else {
197             final Map<String, List<ProcessInformations>> processesByTitle = collectorServer
198                     .collectProcessInformations(getApplication());
199             pdfOtherReport.writeProcessInformations(processesByTitle);
200         }
201     }
202
203     @RequestPart(HttpPart.DATABASE)
204     void doDatabase(@RequestParameter(HttpParameter.REQUEST) String requestIndex) throws Exception { // NOPMD
205         // par sécurité
206         Action.checkSystemActionsEnabled();
207         final int index = DatabaseInformations.parseRequestIndex(requestIndex);
208         final DatabaseInformations databaseInformations;
209         if (!isFromCollectorServer()) {
210             databaseInformations = new DatabaseInformations(index);
211         } else {
212             databaseInformations = collectorServer.collectDatabaseInformations(getApplication(),
213                     index);
214         }
215         pdfOtherReport.writeDatabaseInformations(databaseInformations);
216     }
217
218     @RequestPart(HttpPart.JNDI)
219     void doJndi(@RequestParameter(HttpParameter.PATH) String path) throws Exception { // NOPMD
220         // par sécurité
221         Action.checkSystemActionsEnabled();
222         final List<JndiBinding> jndiBindings;
223         if (!isFromCollectorServer()) {
224             jndiBindings = JndiBinding.listBindings(path);
225         } else {
226             jndiBindings = collectorServer.collectJndiBindings(getApplication(), path);
227         }
228         pdfOtherReport.writeJndi(jndiBindings, JndiBinding.normalizePath(path));
229     }
230
231     @RequestPart(HttpPart.MBEANS)
232     void doMBeans() throws Exception { // NOPMD
233         // par sécurité
234         Action.checkSystemActionsEnabled();
235         if (!isFromCollectorServer()) {
236             final List<MBeanNode> nodes = MBeans.getAllMBeanNodes();
237             pdfOtherReport.writeMBeans(nodes);
238         } else {
239             final Map<String, List<MBeanNode>> allMBeans = collectorServer
240                     .collectMBeans(getApplication());
241             pdfOtherReport.writeMBeans(allMBeans);
242         }
243     }
244
245     @RequestPart(HttpPart.HEAP_HISTO)
246     void doHeapHisto() throws Exception { // NOPMD
247         // par sécurité
248         Action.checkSystemActionsEnabled();
249         final HeapHistogram heapHistogram;
250         if (!isFromCollectorServer()) {
251             heapHistogram = VirtualMachine.createHeapHistogram();
252         } else {
253             heapHistogram = collectorServer.collectHeapHistogram(getApplication());
254         }
255         pdfOtherReport.writeHeapHistogram(heapHistogram);
256     }
257
258     void addPdfContentTypeAndDisposition(HttpServletRequest httpRequest,
259             HttpServletResponse httpResponse) {
260         httpResponse.setContentType("application/pdf");
261         final String contentDisposition = encodeFileNameToContentDisposition(httpRequest,
262                 PdfReport.getFileName(getApplication()));
263         // encoding des CRLF pour http://en.wikipedia.org/wiki/HTTP_response_splitting
264         httpResponse.addHeader("Content-Disposition",
265                 contentDisposition.replace('\n', '_').replace('\r', '_'));
266     }
267
268     private String getApplication() {
269         return collector.getApplication();
270     }
271
272     private boolean isFromCollectorServer() {
273         return collectorServer != null;
274     }
275
276     /**
277      * Encode un nom de fichier avec des % pour Content-Disposition, avec téléchargement.
278      * (US-ASCII + Encode-Word : http://www.ietf.org/rfc/rfc2183.txt, http://www.ietf.org/rfc/rfc2231.txt
279      * sauf en MS IE qui ne supporte pas cet encodage et qui n'en a pas besoin)
280      * @param httpRequest HttpServletRequest
281      * @param fileName String
282      * @return String
283      */

284     private static String encodeFileNameToContentDisposition(HttpServletRequest httpRequest,
285             String fileName) {
286         assert fileName != null;
287         final String userAgent = httpRequest.getHeader("user-agent");
288         if (userAgent != null && userAgent.contains("MSIE")) {
289             return "attachment;filename=" + fileName;
290         }
291         return encodeFileNameToStandardContentDisposition(fileName);
292     }
293
294     private static String encodeFileNameToStandardContentDisposition(String fileName) {
295         final int length = fileName.length();
296         final StringBuilder sb = new StringBuilder(length + length / 4);
297         // attachment et non inline pour proposer l'enregistrement (sauf IE6)
298         // et non l'affichage direct dans le navigateur
299         sb.append("attachment;filename=\"");
300         char c;
301         for (int i = 0; i < length; i++) {
302             c = fileName.charAt(i);
303             if (isEncodingNotNeeded(c)) {
304                 sb.append(c);
305             } else {
306                 sb.append('%');
307                 if (c < 16) {
308                     sb.append('0');
309                 }
310                 sb.append(Integer.toHexString(c));
311             }
312         }
313         sb.append('"');
314         return sb.toString();
315     }
316
317     private static boolean isEncodingNotNeeded(char c) {
318         return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '.'
319                 || c == '_';
320     }
321 }
322