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.common;
19
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.InetAddress;
26 import java.net.MalformedURLException;
27 import java.net.URL;
28 import java.net.UnknownHostException;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Properties;
36 import java.util.TreeMap;
37
38 import javax.servlet.FilterConfig;
39 import javax.servlet.ServletContext;
40
41 import net.bull.javamelody.Parameter;
42 import net.bull.javamelody.internal.model.TransportFormat;
43
44 /**
45  * Classe d'accès aux paramètres du monitoring.
46  * @author Emeric Vernat
47  */

48 public final class Parameters {
49     public static final String PARAMETER_SYSTEM_PREFIX = "javamelody.";
50     public static final File TEMPORARY_DIRECTORY = new File(System.getProperty("java.io.tmpdir"));
51     public static final String JAVA_VERSION = System.getProperty("java.version");
52     public static final String JAVAMELODY_VERSION = getJavaMelodyVersion();
53     // default monitoring-path is "/monitoring" in the http URL
54     private static final String DEFAULT_MONITORING_PATH = "/monitoring";
55     // résolution (ou pas) par défaut en s de stockage des valeurs dans les fichiers RRD
56     private static final int DEFAULT_RESOLUTION_SECONDS = 60;
57     // stockage des fichiers RRD de JRobin dans le répertoire temp/javamelody/<context> par défaut
58     private static final String DEFAULT_DIRECTORY = "javamelody";
59     // nom du fichier stockant les applications et leurs urls dans le répertoire de stockage
60     private static final String COLLECTOR_APPLICATIONS_FILENAME = "applications.properties";
61     private static final boolean PDF_ENABLED = computePdfEnabled();
62     private static Map<String, List<URL>> urlsByApplications;
63     private static Map<String, List<String>> applicationsByAggregationApplications;
64
65     private static FilterConfig filterConfig;
66     private static ServletContext servletContext;
67     private static String lastConnectUrl;
68     private static Properties lastConnectInfo;
69     private static boolean dnsLookupsDisabled;
70
71     private Parameters() {
72         super();
73     }
74
75     public static void initialize(FilterConfig config) {
76         filterConfig = config;
77         if (config != null) {
78             final ServletContext context = config.getServletContext();
79             initialize(context);
80         }
81     }
82
83     public static void initialize(ServletContext context) {
84         if ("1.6".compareTo(JAVA_VERSION) > 0) {
85             throw new IllegalStateException(
86                     "La version java doit être 1.6 au minimum et non " + JAVA_VERSION);
87         }
88         servletContext = context;
89
90         dnsLookupsDisabled = Parameter.DNS_LOOKUPS_DISABLED.getValueAsBoolean();
91     }
92
93     public static void initJdbcDriverParameters(String connectUrl, Properties connectInfo) {
94         lastConnectUrl = connectUrl;
95         lastConnectInfo = connectInfo;
96     }
97
98     /**
99      * @return Contexte de servlet de la webapp, soit celle monitorée ou soit celle de collecte.
100      */

101     public static ServletContext getServletContext() {
102         assert servletContext != null;
103         return servletContext;
104     }
105
106     public static String getLastConnectUrl() {
107         return lastConnectUrl;
108     }
109
110     public static Properties getLastConnectInfo() {
111         return lastConnectInfo;
112     }
113
114     /**
115      * @return Nom et urls des applications telles que paramétrées dans un serveur de collecte.
116      * @throws IOException e
117      */

118     public static Map<String, List<URL>> getCollectorUrlsByApplications() throws IOException {
119         if (urlsByApplications == null) {
120             readCollectorApplications();
121         }
122         return Collections.unmodifiableMap(urlsByApplications);
123     }
124
125     public static Map<String, List<String>> getApplicationsByAggregationApplication()
126             throws IOException {
127         if (applicationsByAggregationApplications == null) {
128             readCollectorApplications();
129         }
130         return Collections.unmodifiableMap(applicationsByAggregationApplications);
131     }
132
133     public static void addCollectorApplication(String application, List<URL> urls)
134             throws IOException {
135         assert application != null;
136         assert urls != null && !urls.isEmpty();
137         // initialisation si besoin
138         getCollectorUrlsByApplications();
139
140         urlsByApplications.put(application, urls);
141         writeCollectorApplications();
142     }
143
144     public static void addCollectorAggregationApplication(String aggregationApplication,
145             List<String> aggregatedApplications) throws IOException {
146         assert aggregationApplication != null;
147         assert aggregatedApplications != null && !aggregatedApplications.isEmpty();
148         // initialisation si besoin
149         getCollectorUrlsByApplications();
150
151         applicationsByAggregationApplications.put(aggregationApplication, aggregatedApplications);
152         writeCollectorApplications();
153     }
154
155     public static void removeCollectorApplication(String application) throws IOException {
156         assert application != null;
157         // initialisation si besoin
158         getCollectorUrlsByApplications();
159
160         if (urlsByApplications.containsKey(application)) {
161             urlsByApplications.remove(application);
162         } else {
163             applicationsByAggregationApplications.remove(application);
164         }
165         synchronizeAggregationApplications();
166         writeCollectorApplications();
167     }
168
169     private static void writeCollectorApplications() throws IOException {
170         final Properties properties = new Properties();
171         final String monitoringPath = getMonitoringPath();
172         for (final Map.Entry<String, List<URL>> entry : urlsByApplications.entrySet()) {
173             final List<URL> urls = entry.getValue();
174             assert urls != null && !urls.isEmpty();
175             final StringBuilder sb = new StringBuilder();
176             for (final URL url : urls) {
177                 final String urlString = url.toString();
178                 // on enlève le suffixe ajouté précédemment dans parseUrl
179                 final String webappUrl = urlString.substring(0,
180                         urlString.lastIndexOf(monitoringPath));
181                 if (webappUrl.indexOf(',') != -1) {
182                     throw new IOException("The URL should not contain a comma.");
183                 }
184                 sb.append(webappUrl).append(',');
185             }
186             sb.delete(sb.length() - 1, sb.length());
187             properties.put(entry.getKey(), sb.toString());
188         }
189         for (final Map.Entry<String, List<String>> entry : applicationsByAggregationApplications
190                 .entrySet()) {
191             final List<String> applications = entry.getValue();
192             final StringBuilder sb = new StringBuilder();
193             for (final String application : applications) {
194                 if (application.indexOf(',') != -1) {
195                     throw new IOException("The application name should not contain a comma.");
196                 }
197                 sb.append(application).append(',');
198             }
199             sb.delete(sb.length() - 1, sb.length());
200             properties.put(entry.getKey(), sb.toString());
201         }
202         final File collectorApplicationsFile = getCollectorApplicationsFile();
203         final File directory = collectorApplicationsFile.getParentFile();
204         if (!directory.mkdirs() && !directory.exists()) {
205             throw new IOException("JavaMelody directory can't be created: " + directory.getPath());
206         }
207         try (FileOutputStream output = new FileOutputStream(collectorApplicationsFile)) {
208             properties.store(output, "urls of the applications to monitor");
209         }
210     }
211
212     private static void readCollectorApplications() throws IOException {
213         // le fichier applications.properties contient les noms et les urls des applications à monitorer
214         // par ex.: recette=http://recette1:8080/myapp
215         // ou recette2=http://recette2:8080/myapp
216         // ou production=http://prod1:8080/myapp,http://prod2:8080/myapp
217         // ou aggregation=recette,recette2
218         // Dans une instance de Properties, les propriétés ne sont pas ordonnées,
219         // mais elles seront ordonnées lorsqu'elles seront mises dans cette TreeMap
220         final Map<String, List<URL>> applications = new TreeMap<>();
221         final Map<String, List<String>> aggregationApplications = new TreeMap<>();
222         final File file = getCollectorApplicationsFile();
223         if (file.exists()) {
224             final Properties properties = new Properties();
225             try (FileInputStream input = new FileInputStream(file)) {
226                 properties.load(input);
227             }
228             @SuppressWarnings("unchecked")
229             final List<String> propertyNames = (List<String>) Collections
230                     .list(properties.propertyNames());
231             for (final String property : propertyNames) {
232                 final String value = String.valueOf(properties.get(property));
233                 if (value.startsWith("http")) {
234                     applications.put(property, parseUrls(value));
235                 } else {
236                     aggregationApplications.put(property,
237                             new ArrayList<>(Arrays.asList(value.split(","))));
238                 }
239             }
240         }
241         urlsByApplications = applications;
242         applicationsByAggregationApplications = aggregationApplications;
243
244         synchronizeAggregationApplications();
245     }
246
247     private static void synchronizeAggregationApplications() {
248         for (final Iterator<List<String>> it1 = applicationsByAggregationApplications.values()
249                 .iterator(); it1.hasNext();) {
250             final List<String> aggregatedApplications = it1.next();
251             for (final Iterator<String> it2 = aggregatedApplications.iterator(); it2.hasNext();) {
252                 final String aggregatedApplication = it2.next();
253                 if (!urlsByApplications.containsKey(aggregatedApplication)
254                         && !applicationsByAggregationApplications
255                                 .containsKey(aggregatedApplication)) {
256                     // application aggrégée inconnue, on la supprime
257                     it2.remove();
258                 }
259             }
260             if (aggregatedApplications.isEmpty()) {
261                 // application d'aggrégation vide, on la supprime
262                 it1.remove();
263             }
264         }
265     }
266
267     public static File getCollectorApplicationsFile() {
268         return new File(getStorageDirectory(""), COLLECTOR_APPLICATIONS_FILENAME);
269     }
270
271     public static List<URL> parseUrls(String value) throws MalformedURLException {
272         // pour un cluster, le paramètre vaut "url1,url2"
273         final TransportFormat transportFormat;
274         if (Parameter.TRANSPORT_FORMAT.getValue() == null) {
275             transportFormat = TransportFormat.SERIALIZED;
276         } else {
277             transportFormat = TransportFormat
278                     .valueOfIgnoreCase(Parameter.TRANSPORT_FORMAT.getValue());
279         }
280         final String suffix = getMonitoringPath() + "?collector=stop&format="
281                 + transportFormat.getCode();
282
283         final String[] urlsArray = value.split(",");
284         final List<URL> urls = new ArrayList<>(urlsArray.length);
285         for (final String s : urlsArray) {
286             String s2 = s.trim();
287             while (s2.endsWith("/")) {
288                 s2 = s2.substring(0, s2.length() - 1);
289             }
290             final URL url = new URL(s2 + suffix);
291             urls.add(url);
292         }
293         return urls;
294     }
295
296     public static String getMonitoringPath() {
297         final String parameterValue = Parameter.MONITORING_PATH.getValue();
298         if (parameterValue == null) {
299             return DEFAULT_MONITORING_PATH;
300         }
301         return parameterValue;
302     }
303
304     /**
305      * @return nom réseau de la machine
306      */

307     public static String getHostName() {
308         if (dnsLookupsDisabled) {
309             return "localhost";
310         }
311
312         try {
313             return InetAddress.getLocalHost().getHostName();
314         } catch (final UnknownHostException ex) {
315             return "unknown";
316         }
317     }
318
319     /**
320      * @return adresse ip de la machine
321      */

322     public static String getHostAddress() {
323         if (dnsLookupsDisabled) {
324             return "127.0.0.1"// NOPMD
325         }
326
327         try {
328             return InetAddress.getLocalHost().getHostAddress();
329         } catch (final UnknownHostException ex) {
330             return "unknown";
331         }
332     }
333
334     /**
335      * @param fileName Nom du fichier de resource.
336      * @return Chemin complet d'une resource.
337      */

338     public static String getResourcePath(String fileName) {
339         return "/net/bull/javamelody/resource/" + fileName;
340     }
341
342     /**
343      * @return Résolution en secondes des courbes et période d'appels par le serveur de collecte le cas échéant.
344      */

345     public static int getResolutionSeconds() {
346         final String param = Parameter.RESOLUTION_SECONDS.getValue();
347         if (param != null) {
348             // lance une NumberFormatException si ce n'est pas un nombre
349             final int result = Integer.parseInt(param);
350             if (result <= 0) {
351                 throw new IllegalStateException(
352                         "The parameter resolution-seconds should be > 0 (between 60 and 600 recommended)");
353             }
354             return result;
355         }
356         return DEFAULT_RESOLUTION_SECONDS;
357     }
358
359     /**
360      * @param application Nom de l'application
361      * @return Répertoire de stockage des compteurs et des données pour les courbes.
362      */

363     public static File getStorageDirectory(String application) {
364         final String param = Parameter.STORAGE_DIRECTORY.getValue();
365         final String dir;
366         if (param == null) {
367             dir = DEFAULT_DIRECTORY;
368         } else {
369             dir = param;
370         }
371         // Si le nom du répertoire commence par '/' (ou "drive specifier" sur Windows),
372         // on considère que c'est un chemin absolu,
373         // sinon on considère que c'est un chemin relatif par rapport au répertoire temporaire
374         // ('temp' dans TOMCAT_HOME pour tomcat).
375         final String directory;
376         if (!dir.isEmpty() && new File(dir).isAbsolute()) {
377             directory = dir;
378         } else {
379             directory = TEMPORARY_DIRECTORY.getPath() + '/' + dir;
380         }
381         if (servletContext != null) {
382             return new File(directory + '/' + application);
383         }
384         return new File(directory);
385     }
386
387     /**
388      * Booléen selon que le paramètre no-database vaut true.
389      * @return boolean
390      */

391     public static boolean isNoDatabase() {
392         return Parameter.NO_DATABASE.getValueAsBoolean();
393     }
394
395     /**
396      * Booléen selon que le paramètre system-actions-enabled vaut true.
397      * @return boolean
398      */

399     public static boolean isSystemActionsEnabled() {
400         final String parameter = Parameter.SYSTEM_ACTIONS_ENABLED.getValue();
401         return parameter == null || Boolean.parseBoolean(parameter);
402     }
403
404     public static boolean isPdfEnabled() {
405         return PDF_ENABLED;
406     }
407
408     private static boolean computePdfEnabled() {
409         try {
410             Class.forName("com.lowagie.text.Document");
411             return true;
412         } catch (final ClassNotFoundException e) {
413             return false;
414         }
415     }
416
417     /**
418      * Retourne false si le paramètre displayed-counters n'a pas été défini
419      * ou si il contient le compteur dont le nom est paramètre,
420      * et retourne true sinon (c'est-à-dire si le paramètre displayed-counters est défini
421      * et si il ne contient pas le compteur dont le nom est paramètre).
422      * @param counterName Nom du compteur
423      * @return boolean
424      */

425     public static boolean isCounterHidden(String counterName) {
426         final String displayedCounters = Parameter.DISPLAYED_COUNTERS.getValue();
427         if (displayedCounters == null) {
428             return false;
429         }
430         for (final String displayedCounter : displayedCounters.split(",")) {
431             final String displayedCounterName = displayedCounter.trim();
432             if (counterName.equalsIgnoreCase(displayedCounterName)) {
433                 return false;
434             }
435         }
436         return true;
437     }
438
439     /**
440      * @return Nom de l'application courante et nom du sous-répertoire de stockage dans une application monitorée.
441      */

442     public static String getCurrentApplication() {
443         // use explicitly configured application name (if configured)
444         final String applicationName = Parameter.APPLICATION_NAME.getValue();
445         if (applicationName != null) {
446             return applicationName;
447         }
448         if (servletContext != null) {
449             // Le nom de l'application et donc le stockage des fichiers est dans le sous-répertoire
450             // ayant pour nom le contexte de la webapp et le nom du serveur
451             // pour pouvoir monitorer plusieurs webapps sur le même serveur et
452             // pour pouvoir stocker sur un répertoire partagé entre plusieurs serveurs
453             return getContextPath(servletContext) + '_' + getHostName();
454         }
455         return null;
456     }
457
458     public static String getContextPath(ServletContext context) {
459         // cette méthode retourne le contextPath de la webapp
460         // en utilisant ServletContext.getContextPath si servlet api 2.5
461         // ou en se débrouillant sinon
462         // (on n'a pas encore pour l'instant de request pour appeler HttpServletRequest.getContextPath)
463         if (context.getMajorVersion() == 2 && context.getMinorVersion() >= 5
464                 || context.getMajorVersion() > 2) {
465             // api servlet 2.5 (Java EE 5) minimum pour appeler ServletContext.getContextPath
466             return context.getContextPath();
467         }
468         final URL webXmlUrl;
469         try {
470             webXmlUrl = context.getResource("/WEB-INF/web.xml");
471         } catch (final MalformedURLException e) {
472             throw new IllegalStateException(e);
473         }
474         String contextPath = webXmlUrl.toExternalForm();
475         contextPath = contextPath.substring(0, contextPath.indexOf("/WEB-INF/web.xml"));
476         final int indexOfWar = contextPath.indexOf(".war");
477         if (indexOfWar > 0) {
478             contextPath = contextPath.substring(0, indexOfWar);
479         }
480         // tomcat peut renvoyer une url commençant pas "jndi:/localhost"
481         // (v5.5.28, webapp dans un répertoire)
482         if (contextPath.startsWith("jndi:/localhost")) {
483             contextPath = contextPath.substring("jndi:/localhost".length());
484         }
485         final int lastIndexOfSlash = contextPath.lastIndexOf('/');
486         if (lastIndexOfSlash != -1) {
487             contextPath = contextPath.substring(lastIndexOfSlash);
488         }
489         return contextPath;
490     }
491
492     private static String getJavaMelodyVersion() {
493         final InputStream inputStream = Parameters.class
494                 .getResourceAsStream("/JAVAMELODY-VERSION.properties");
495         if (inputStream == null) {
496             return null;
497         }
498
499         final Properties properties = new Properties();
500         try {
501             try {
502                 properties.load(inputStream);
503                 return properties.getProperty("version");
504             } finally {
505                 inputStream.close();
506             }
507         } catch (final IOException e) {
508             return e.toString();
509         }
510     }
511
512     /**
513      * Recherche la valeur d'un paramètre qui peut être défini par ordre de priorité croissant : <br/>
514      * - dans les paramètres d'initialisation du filtre (fichier web.xml dans la webapp) <br/>
515      * - dans les paramètres du contexte de la webapp avec le préfixe "javamelody." (fichier xml de contexte dans Tomcat) <br/>
516      * - dans les variables d'environnement du système d'exploitation avec le préfixe "javamelody." <br/>
517      * - dans les propriétés systèmes avec le préfixe "javamelody." (commande de lancement java).
518      * @param parameter Enum du paramètre
519      * @return valeur du paramètre ou null si pas de paramètre défini
520      */

521     public static String getParameterValue(Parameter parameter) {
522         assert parameter != null;
523         final String name = parameter.getCode();
524         return getParameterValueByName(name);
525     }
526
527     public static String getParameterValueByName(String parameterName) {
528         assert parameterName != null;
529         final String globalName = PARAMETER_SYSTEM_PREFIX + parameterName;
530         String result = System.getProperty(globalName);
531         if (result != null) {
532             return result;
533         }
534         if (servletContext != null) {
535             result = servletContext.getInitParameter(globalName);
536             if (result != null) {
537                 return result;
538             }
539             // issue 463: in a ServletContextListener, it's also possible to call servletContext.setAttribute("javamelody.log""true"); for example
540             final Object attribute = servletContext.getAttribute(globalName);
541             if (attribute instanceof String) {
542                 return (String) attribute;
543             }
544         }
545         if (filterConfig != null) {
546             return filterConfig.getInitParameter(parameterName);
547         }
548         return null;
549     }
550 }
551