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.io.Serializable;
22 import java.sql.SQLException;
23 import java.text.DateFormat;
24 import java.text.SimpleDateFormat;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.TreeMap;
33
34 import javax.management.JMException;
35 import javax.naming.NamingException;
36 import javax.servlet.http.HttpServletRequest;
37 import javax.servlet.http.HttpServletResponse;
38
39 import net.bull.javamelody.JdbcWrapper;
40 import net.bull.javamelody.SessionListener;
41 import net.bull.javamelody.internal.common.HttpParameter;
42 import net.bull.javamelody.internal.common.HttpPart;
43 import net.bull.javamelody.internal.common.I18N;
44 import net.bull.javamelody.internal.model.Action;
45 import net.bull.javamelody.internal.model.Collector;
46 import net.bull.javamelody.internal.model.Counter;
47 import net.bull.javamelody.internal.model.CounterRequest;
48 import net.bull.javamelody.internal.model.CounterRequestAggregation;
49 import net.bull.javamelody.internal.model.CounterRequestContext;
50 import net.bull.javamelody.internal.model.DatabaseInformations;
51 import net.bull.javamelody.internal.model.HsErrPid;
52 import net.bull.javamelody.internal.model.JRobin;
53 import net.bull.javamelody.internal.model.JavaInformations;
54 import net.bull.javamelody.internal.model.JndiBinding;
55 import net.bull.javamelody.internal.model.MBeans;
56 import net.bull.javamelody.internal.model.MavenArtifact;
57 import net.bull.javamelody.internal.model.Period;
58 import net.bull.javamelody.internal.model.ProcessInformations;
59 import net.bull.javamelody.internal.model.Range;
60 import net.bull.javamelody.internal.model.TransportFormat;
61 import net.bull.javamelody.internal.model.VirtualMachine;
62 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestAttribute;
63 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestHeader;
64 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestParameter;
65 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestPart;
66
67 /**
68  * Contrôleur au sens MVC pour la partie des données sérialisées.
69  * @author Emeric Vernat
70  */

71 public class SerializableController {
72     private static final String RANGE_KEY = "range";
73     private static final String JAVA_INFORMATIONS_LIST_KEY = "javaInformationsList";
74     private static final String MESSAGE_FOR_REPORT_KEY = "messageForReport";
75     private static final RequestToMethodMapper<SerializableController> REQUEST_TO_METHOD_MAPPER = new RequestToMethodMapper<>(
76             SerializableController.class);
77     private final Collector collector;
78
79     public SerializableController(Collector collector) {
80         super();
81         assert collector != null;
82         this.collector = collector;
83     }
84
85     void doSerializable(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
86             Serializable serializable) throws IOException {
87         // l'appelant (un serveur d'agrégation par exemple) peut appeler
88         // la page monitoring avec un format "serialized" ou "xml" en paramètre
89         // pour avoir les données au format sérialisé java ou xml
90         final String format = HttpParameter.FORMAT.getParameterFrom(httpRequest);
91         final TransportFormat transportFormat = TransportFormat.valueOfIgnoreCase(format);
92         // checkDependencies avant setContentType pour afficher correctement les erreurs
93         transportFormat.checkDependencies();
94         httpResponse.setContentType(transportFormat.getMimeType());
95         final String fileName = "JavaMelody_" + getApplication().replace(' ', '_').replace("/""")
96                 + '_' + I18N.getCurrentDate().replace('/', '_') + '.' + transportFormat.getCode();
97         final String contentDisposition = "inline;filename=" + fileName;
98         // encoding des CRLF pour http://en.wikipedia.org/wiki/HTTP_response_splitting
99         httpResponse.addHeader("Content-Disposition",
100                 contentDisposition.replace('\n', '_').replace('\r', '_'));
101
102         transportFormat.writeSerializableTo(serializable, httpResponse.getOutputStream());
103     }
104
105     public Serializable createSerializable(HttpServletRequest httpRequest,
106             List<JavaInformations> javaInformationsList, String messageForReport)
107             throws IOException {
108         final Range range = getRangeForSerializable(httpRequest);
109         if (HttpParameter.PART.getParameterFrom(httpRequest) != null) {
110             httpRequest.setAttribute(JAVA_INFORMATIONS_LIST_KEY, javaInformationsList);
111             httpRequest.setAttribute(RANGE_KEY, range);
112             httpRequest.setAttribute(MESSAGE_FOR_REPORT_KEY, messageForReport);
113
114             return (Serializable) REQUEST_TO_METHOD_MAPPER.invokeAndReturn(httpRequest, this);
115         } else if (HttpParameter.JMX_VALUE.getParameterFrom(httpRequest) != null) {
116             // par sécurité
117             Action.checkSystemActionsEnabled();
118             final String jmxValue = HttpParameter.JMX_VALUE.getParameterFrom(httpRequest);
119             return MBeans.getConvertedAttributes(jmxValue);
120         }
121
122         return createDefaultSerializable(javaInformationsList, range, messageForReport);
123     }
124
125     @RequestPart(HttpPart.THREADS)
126     Serializable createThreadsSerializable(
127             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList) {
128         return new ArrayList<>(javaInformationsList.get(0).getThreadInformationsList());
129     }
130
131     @RequestPart(HttpPart.COUNTER_SUMMARY_PER_CLASS)
132     Serializable createCounterSummaryPerClassSerializable(@RequestAttribute(RANGE_KEY) Range range,
133             @RequestParameter(HttpParameter.COUNTER) String counterName,
134             @RequestParameter(HttpParameter.GRAPH) String requestId) throws IOException {
135         final Counter counter = collector.getRangeCounter(range, counterName).clone();
136         final List<CounterRequest> requestList = new CounterRequestAggregation(counter)
137                 .getRequestsAggregatedOrFilteredByClassName(requestId);
138         return new ArrayList<>(requestList);
139     }
140
141     @RequestPart(HttpPart.CURRENT_REQUESTS)
142     Serializable createCurrentRequestsSerializable(
143             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList) {
144         final Map<JavaInformations, List<CounterRequestContext>> result = new HashMap<>();
145         result.put(javaInformationsList.get(0), getCurrentRequests());
146         return (Serializable) result;
147     }
148
149     @RequestPart(HttpPart.DEFAULT_WITH_CURRENT_REQUESTS)
150     @SuppressWarnings("unchecked")
151     Serializable createDefaultWithCurrentRequestsSerializable(
152             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList,
153             @RequestAttribute(MESSAGE_FOR_REPORT_KEY) String messageForReport,
154             @RequestAttribute(RANGE_KEY) Range range) throws IOException {
155         final List<Serializable> result = new ArrayList<>(
156                 (List<Serializable>) createDefaultSerializable(javaInformationsList, range,
157                         messageForReport));
158         result.addAll(getCurrentRequests());
159         return (Serializable) result;
160     }
161
162     @RequestPart(HttpPart.JVM)
163     Serializable createJvmSerializable(
164             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList) {
165         return new ArrayList<>(javaInformationsList);
166     }
167
168     @RequestPart(HttpPart.SESSIONS)
169     Serializable createSessionsSerializable(
170             @RequestParameter(HttpParameter.SESSION_ID) String sessionId) {
171         // par sécurité
172         Action.checkSystemActionsEnabled();
173         if (sessionId == null) {
174             return new ArrayList<>(SessionListener.getAllSessionsInformations());
175         }
176         return SessionListener.getSessionInformationsBySessionId(sessionId);
177     }
178
179     @RequestPart(HttpPart.HOTSPOTS)
180     Serializable createHotspotsSerializable() {
181         // par sécurité
182         Action.checkSystemActionsEnabled();
183         return new ArrayList<>(collector.getHotspots());
184     }
185
186     @RequestPart(HttpPart.HEAP_HISTO)
187     Serializable createHeapHistoSerializable() throws Exception { // NOPMD
188         // par sécurité
189         Action.checkSystemActionsEnabled();
190         return VirtualMachine.createHeapHistogram();
191     }
192
193     @RequestPart(HttpPart.PROCESSES)
194     Serializable createProcessesSerializable() throws IOException {
195         // par sécurité
196         Action.checkSystemActionsEnabled();
197         return new ArrayList<>(ProcessInformations.buildProcessInformations());
198     }
199
200     @RequestPart(HttpPart.JNDI)
201     Serializable createJndiSerializable(@RequestParameter(HttpParameter.PATH) String path)
202             throws NamingException {
203         // par sécurité
204         Action.checkSystemActionsEnabled();
205         return new ArrayList<>(JndiBinding.listBindings(path));
206     }
207
208     @RequestPart(HttpPart.MBEANS)
209     Serializable createMBeansSerializable() throws JMException {
210         // par sécurité
211         Action.checkSystemActionsEnabled();
212         return new ArrayList<>(MBeans.getAllMBeanNodes());
213     }
214
215     @RequestPart(HttpPart.DEPENDENCIES)
216     Serializable createDependenciesSerializable() throws IOException {
217         // par sécurité
218         Action.checkSystemActionsEnabled();
219         final Map<String, MavenArtifact> webappDependencies = MavenArtifact.getWebappDependencies();
220         for (final MavenArtifact dependency : webappDependencies.values()) {
221             if (dependency != null) {
222                 // preload licenses with parent of dependency when needed
223                 dependency.getLicenseUrlsByName();
224             }
225         }
226         return new TreeMap<>(webappDependencies);
227     }
228
229     @RequestPart(HttpPart.LAST_VALUE)
230     Serializable createLastValueSerializable(@RequestParameter(HttpParameter.GRAPH) String graph)
231             throws IOException {
232         if (graph != null) {
233             final JRobin jrobin = collector.getJRobin(graph);
234             final double lastValue;
235             if (jrobin == null) {
236                 lastValue = -1;
237             } else {
238                 lastValue = jrobin.getLastValue();
239             }
240             return lastValue;
241         }
242         final Collection<JRobin> jrobins = collector.getDisplayedCounterJRobins();
243         final Map<String, Double> lastValues = new LinkedHashMap<>(jrobins.size());
244         for (final JRobin jrobin : jrobins) {
245             lastValues.put(jrobin.getName(), jrobin.getLastValue());
246         }
247         return (Serializable) lastValues;
248     }
249
250     @RequestPart(HttpPart.DATABASE)
251     Serializable createDatabaseSerializable(
252             @RequestParameter(HttpParameter.REQUEST) String requestIndex)
253             throws SQLException, NamingException {
254         // par sécurité
255         Action.checkSystemActionsEnabled();
256         final int index = DatabaseInformations.parseRequestIndex(requestIndex);
257         return new DatabaseInformations(index);
258     }
259
260     @RequestPart(HttpPart.CONNECTIONS)
261     Serializable createConnectionsSerializable() {
262         // par sécurité
263         Action.checkSystemActionsEnabled();
264         return new ArrayList<>(JdbcWrapper.getConnectionInformationsList());
265     }
266
267     @RequestPart(HttpPart.WEBAPP_VERSIONS)
268     Serializable createWebappVersionsSerializable() {
269         return new LinkedHashMap<>(collector.getDatesByWebappVersions());
270     }
271
272     @RequestPart(HttpPart.GRAPH)
273     Serializable getCounterRequestById(@RequestParameter(HttpParameter.GRAPH) String requestId,
274             @RequestAttribute(RANGE_KEY) Range range) throws IOException {
275         for (final Counter counter : collector.getCounters()) {
276             if (counter.isRequestIdFromThisCounter(requestId)) {
277                 final Counter rangeCounter = collector.getRangeCounter(range, counter.getName())
278                         .clone();
279                 for (final CounterRequest request : rangeCounter.getRequests()) {
280                     if (requestId.equals(request.getId())) {
281                         return request;
282                     }
283                 }
284             }
285         }
286         // non trouvé
287         return null;
288     }
289
290     @RequestPart(HttpPart.JROBINS)
291     Serializable getJRobinsImages(@RequestAttribute(RANGE_KEY) Range range,
292             @RequestParameter(HttpParameter.WIDTH) String width,
293             @RequestParameter(HttpParameter.HEIGHT) String height,
294             @RequestParameter(HttpParameter.GRAPH) String graphName) throws IOException {
295         // pour UI Swing
296         final int myWidth = Integer.parseInt(width);
297         final int myHeight = Integer.parseInt(height);
298         if (graphName != null) {
299             final JRobin jrobin = collector.getJRobin(graphName);
300             if (jrobin != null) {
301                 return jrobin.graph(range, myWidth, myHeight);
302             }
303             return null;
304         }
305         final Collection<JRobin> jrobins = collector.getDisplayedCounterJRobins();
306         return (Serializable) convertJRobinsToImages(jrobins, range, myWidth, myHeight);
307     }
308
309     @RequestPart(HttpPart.OTHER_JROBINS)
310     Serializable getOtherJRobinsImages(@RequestAttribute(RANGE_KEY) Range range,
311             @RequestParameter(HttpParameter.WIDTH) String width,
312             @RequestParameter(HttpParameter.HEIGHT) String height) throws IOException {
313         // pour UI Swing
314         final Collection<JRobin> jrobins = collector.getDisplayedOtherJRobins();
315         return (Serializable) convertJRobinsToImages(jrobins, range, Integer.parseInt(width),
316                 Integer.parseInt(height));
317     }
318
319     @RequestPart(HttpPart.EXPLAIN_PLAN)
320     Serializable createExplainPlanSerializableFor(@RequestHeader("request") String sqlRequest) {
321         // pour UI Swing
322         assert sqlRequest != null;
323         try {
324             // retourne le plan d'exécution ou null si la base de données ne le permet pas (ie non Oracle)
325             return DatabaseInformations.explainPlanFor(sqlRequest);
326         } catch (final Exception ex) {
327             return ex.toString();
328         }
329     }
330
331     private List<CounterRequestContext> getCurrentRequests() {
332         final List<Counter> counters = collector.getCounters();
333         final List<Counter> newCounters = new ArrayList<>();
334         for (final Counter counter : counters) {
335             final Counter cloneLight = new Counter(counter.getName(), counter.getStorageName(),
336                     counter.getIconName(), counter.getChildCounterName());
337             newCounters.add(cloneLight);
338         }
339
340         // note: ces contextes ont été clonés dans getRootCurrentContexts(newCounters) par getOrderedRootCurrentContexts()
341         return collector.getRootCurrentContexts(newCounters);
342     }
343
344     @RequestPart(HttpPart.CRASHES)
345     Serializable createCrashesSerializable(
346             @RequestAttribute(JAVA_INFORMATIONS_LIST_KEY) List<JavaInformations> javaInformationsList) {
347         return new ArrayList<>(HsErrPid.getHsErrPidList(javaInformationsList));
348     }
349
350     private Map<String, byte[]> convertJRobinsToImages(Collection<JRobin> jrobins, Range range,
351             int width, int height) throws IOException {
352         final Map<String, byte[]> images = new LinkedHashMap<>(jrobins.size());
353         for (final JRobin jrobin : jrobins) {
354             final byte[] image = jrobin.graph(range, width, height);
355             images.put(jrobin.getName(), image);
356         }
357         return images;
358     }
359
360     public Serializable createDefaultSerializable(List<JavaInformations> javaInformationsList,
361             Range range, String messageForReport) throws IOException {
362         final List<Counter> counters = collector.getRangeCounters(range);
363         final List<Serializable> serialized = new ArrayList<>(
364                 counters.size() + javaInformationsList.size());
365         // on clone les counters avant de les sérialiser pour ne pas avoir de problèmes de concurrences d'accès
366         for (final Counter counter : counters) {
367             serialized.add(counter.clone());
368         }
369         serialized.addAll(javaInformationsList);
370         if (messageForReport != null) {
371             serialized.add(messageForReport);
372         }
373         return (Serializable) serialized;
374     }
375
376     public Range getRangeForSerializable(HttpServletRequest httpRequest) {
377         final Range range;
378         final String period = HttpParameter.PERIOD.getParameterFrom(httpRequest);
379         if (period == null) {
380             // période tout par défaut pour Serializable, notamment pour le serveur de collecte
381             range = Period.TOUT.getRange();
382         } else {
383             final DateFormat dateFormat;
384             final String pattern = HttpParameter.PATTERN.getParameterFrom(httpRequest);
385             if (pattern == null) {
386                 dateFormat = I18N.createDateFormat();
387             } else {
388                 dateFormat = new SimpleDateFormat(pattern, Locale.US);
389             }
390             range = Range.parse(period, dateFormat);
391         }
392         return range;
393     }
394
395     private String getApplication() {
396         return collector.getApplication();
397     }
398 }
399