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.model;
19
20 import java.lang.management.ManagementFactory;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Set;
31 import java.util.TreeMap;
32
33 import javax.management.Attribute;
34 import javax.management.InstanceNotFoundException;
35 import javax.management.JMException;
36 import javax.management.MBeanAttributeInfo;
37 import javax.management.MBeanInfo;
38 import javax.management.MBeanServer;
39 import javax.management.MBeanServerFactory;
40 import javax.management.ObjectName;
41 import javax.management.openmbean.CompositeData;
42 import javax.management.openmbean.TabularData;
43
44 import net.bull.javamelody.internal.common.I18N;
45 import net.bull.javamelody.internal.common.LOG;
46 import net.bull.javamelody.internal.model.MBeanNode.MBeanAttribute;
47
48 /**
49  * Objet récupérant une instance de {@link MBeanServer} lors de sa construction
50  * et permettant de récupérer différentes données sur les MBeans.
51  * @author Emeric Vernat
52  */

53 public final class MBeans {
54     /**
55      * Separator between mbeans attributes in the External API.
56      */

57     public static final char ATTRIBUTES_SEPARATOR = '|';
58
59     private static final String JAVA_LANG_MBEAN_DESCRIPTION = "Information on the management interface of the MBean";
60     private static final Comparator<MBeanNode> NODE_COMPARATOR = new Comparator<MBeanNode>() {
61         @Override
62         public int compare(MBeanNode o1, MBeanNode o2) {
63             return o1.getName() != null ? o1.getName().compareTo(o2.getName()) : 0;
64         }
65     };
66     private static final Comparator<MBeanAttribute> ATTRIBUTE_COMPARATOR = new Comparator<MBeanAttribute>() {
67         @Override
68         public int compare(MBeanAttribute o1, MBeanAttribute o2) {
69             return o1.getName().compareTo(o2.getName());
70         }
71     };
72     private final MBeanServer mbeanServer;
73
74     MBeans() {
75         this(getPlatformMBeanServer());
76     }
77
78     private MBeans(MBeanServer mbeanServer) {
79         super();
80         this.mbeanServer = mbeanServer;
81     }
82
83     Object getAttribute(ObjectName name, String attribute) throws JMException {
84         return mbeanServer.getAttribute(name, attribute);
85     }
86
87     public static List<MBeanNode> getAllMBeanNodes() throws JMException {
88         initJRockitMBeansIfNeeded();
89
90         final List<MBeanNode> result = new ArrayList<>();
91         final MBeanServer platformMBeanServer = getPlatformMBeanServer();
92         final MBeanNode platformNode = new MBeanNode("");
93         // MBeans pour la plateforme
94         final MBeans platformMBeans = new MBeans();
95         platformNode.getChildren().addAll(platformMBeans.getMBeanNodes());
96         result.add(platformNode);
97
98         // pour JBoss 5.0.x, les MBeans de JBoss sont dans un autre MBeanServer
99         for (final MBeanServer mbeanServer : getMBeanServers()) {
100             if (!mbeanServer.equals(platformMBeanServer)) {
101                 final MBeanNode node = new MBeanNode(mbeanServer.getDefaultDomain());
102                 final MBeans mbeans = new MBeans(mbeanServer);
103                 node.getChildren().addAll(mbeans.getMBeanNodes());
104                 result.add(node);
105             }
106         }
107         return result;
108     }
109
110     private static void initJRockitMBeansIfNeeded() {
111         // si jrockit, on initialise les MBeans spécifiques jrockit lors de la première demande
112         if (System.getProperty("java.vendor").contains("BEA")) {
113             try {
114                 // initialisation des MBeans jrockit comme indiqué dans http://blogs.oracle.com/hirt/jrockit/
115                 try {
116                     getPlatformMBeanServer().getMBeanInfo(
117                             new ObjectName("bea.jrockit.management:type=JRockitConsole"));
118                 } catch (final InstanceNotFoundException e1) {
119                     getPlatformMBeanServer().createMBean("bea.jrockit.management.JRockitConsole",
120                             null);
121                     LOG.debug("JRockit MBeans initialized");
122                 }
123             } catch (final JMException e) {
124                 throw new IllegalStateException(e);
125             }
126         }
127     }
128
129     private List<MBeanNode> getMBeanNodes() throws JMException {
130         final List<MBeanNode> result = new ArrayList<>();
131         final Set<ObjectName> names = mbeanServer.queryNames(nullnull);
132         for (final ObjectName name : names) {
133             final String domain = name.getDomain();
134             if ("jboss.deployment".equals(domain)) {
135                 // la partie "jboss.deployment" dans JBoss (5.0.x) est plutôt inutile et trop lourde
136                 continue;
137             }
138             MBeanNode domainNode = getMBeanNodeFromList(result, domain);
139             if (domainNode == null) {
140                 domainNode = new MBeanNode(domain);
141                 result.add(domainNode);
142             }
143             final String keyPropertyListString = name.getKeyPropertyListString();
144             final String firstPropertyValue;
145             final int indexOf = keyPropertyListString.indexOf('=');
146             if (indexOf == -1) {
147                 // n'arrive probablement pas, mais au cas où
148                 firstPropertyValue = null;
149             } else {
150                 firstPropertyValue = name
151                         .getKeyProperty(keyPropertyListString.substring(0, indexOf));
152             }
153             MBeanNode firstPropertyNode = getMBeanNodeFromList(domainNode.getChildren(),
154                     firstPropertyValue);
155             if (firstPropertyNode == null) {
156                 firstPropertyNode = new MBeanNode(firstPropertyValue);
157                 domainNode.getChildren().add(firstPropertyNode);
158             }
159             try {
160                 final MBeanNode mbean = getMBeanNode(name);
161                 firstPropertyNode.getChildren().add(mbean);
162             } catch (final IllegalStateException e) {
163                 // for JBoss EAP 6 (#757)
164                 continue;
165             }
166         }
167         sortMBeanNodes(result);
168         return result;
169     }
170
171     private void sortMBeanNodes(List<MBeanNode> nodes) {
172         if (nodes.size() > 1) {
173             Collections.sort(nodes, NODE_COMPARATOR);
174         }
175
176         for (final MBeanNode node : nodes) {
177             final List<MBeanNode> children = node.getChildren();
178             if (children != null) {
179                 sortMBeanNodes(children);
180             }
181             final List<MBeanAttribute> attributes = node.getAttributes();
182             if (attributes != null && attributes.size() > 1) {
183                 Collections.sort(attributes, ATTRIBUTE_COMPARATOR);
184             }
185         }
186     }
187
188     private static MBeanNode getMBeanNodeFromList(List<MBeanNode> list, String name) {
189         for (final MBeanNode node : list) {
190             if (node.getName().equals(name)) {
191                 return node;
192             }
193         }
194         return null;
195     }
196
197     private MBeanNode getMBeanNode(ObjectName name) throws JMException {
198         final String mbeanName = name.toString();
199         final MBeanInfo mbeanInfo = mbeanServer.getMBeanInfo(name);
200         final String description = formatDescription(mbeanInfo.getDescription());
201         final MBeanAttributeInfo[] attributeInfos = mbeanInfo.getAttributes();
202         final List<MBeanAttribute> attributes = getAttributes(name, attributeInfos);
203         // les attributs seront triés par ordre alphabétique dans getMBeanNodes
204         return new MBeanNode(mbeanName, description, attributes);
205     }
206
207     private List<MBeanAttribute> getAttributes(ObjectName name,
208             MBeanAttributeInfo[] attributeInfos) {
209         final List<String> attributeNames = new ArrayList<>(attributeInfos.length);
210         for (final MBeanAttributeInfo attribute : attributeInfos) {
211             // on ne veut pas afficher l'attribut password, jamais
212             // (notamment, dans users tomcat ou dans datasources tomcat)
213             if (attribute.isReadable() && !"password".equalsIgnoreCase(attribute.getName())) {
214                 attributeNames.add(attribute.getName());
215             }
216         }
217         final String[] attributeNamesArray = attributeNames.toArray(new String[0]);
218         final List<MBeanAttribute> result = new ArrayList<>();
219         try {
220             // issue 116: asList sur mbeanServer.getAttributes(name, attributeNamesArray) n'existe qu'en java 1.6
221             final List<Object> attributes = mbeanServer.getAttributes(name, attributeNamesArray);
222             for (final Object object : attributes) {
223                 final Attribute attribute = (Attribute) object;
224                 final Object value = convertValueIfNeeded(attribute.getValue());
225                 final String attributeDescription = getAttributeDescription(attribute.getName(),
226                         attributeInfos);
227                 final String formattedAttributeValue = formatAttributeValue(value);
228                 final MBeanAttribute mbeanAttribute = new MBeanAttribute(attribute.getName(),
229                         attributeDescription, formattedAttributeValue);
230                 result.add(mbeanAttribute);
231             }
232         } catch (final Exception e) {
233             // issue 201: do not stop to render MBeans tree when exception in mbeanServer.getAttributes
234             final MBeanAttribute mbeanAttribute = new MBeanAttribute("exception"null,
235                     e.toString());
236             result.add(mbeanAttribute);
237         }
238         return result;
239     }
240
241     private String formatAttributeValue(Object attributeValue) {
242         try {
243             if (attributeValue instanceof List) {
244                 final StringBuilder sb = new StringBuilder();
245                 sb.append('[');
246                 boolean first = true;
247                 for (final Object value : (List<?>) attributeValue) {
248                     if (first) {
249                         first = false;
250                     } else {
251                         sb.append(",\n");
252                     }
253                     if (attributeValue instanceof Number) {
254                         sb.append(I18N.createIntegerFormat().format(attributeValue));
255                     } else {
256                         sb.append(value);
257                     }
258                 }
259                 sb.append(']');
260                 return sb.toString();
261             } else if (attributeValue instanceof Map) {
262                 @SuppressWarnings("unchecked")
263                 final Map<Object, Object> map = (Map<Object, Object>) attributeValue;
264                 final LinkedHashMap<Object, Object> mapToString = new LinkedHashMap<>();
265                 for (final Entry<Object, Object> e : map.entrySet()) {
266                     final Object v = e.getValue();
267                     if (v instanceof Number) {
268                         mapToString.put(e.getKey(), I18N.createIntegerFormat().format(v));
269                     } else {
270                         mapToString.put(e.getKey(), attributeValue);
271                     }
272                 }
273                 return mapToString.toString();
274             } else if (attributeValue instanceof Number) {
275                 return I18N.createIntegerFormat().format(attributeValue);
276             }
277             return String.valueOf(attributeValue);
278         } catch (final Exception e) {
279             return e.toString();
280         }
281     }
282
283     private String formatDescription(String description) {
284         // les descriptions des MBeans de java.lang n'apportent aucune information utile
285         if (description == null || JAVA_LANG_MBEAN_DESCRIPTION.equals(description)) {
286             return null;
287         }
288         int indexOf = description.indexOf("  ");
289         if (indexOf != -1) {
290             // certaines descriptions de MBeans ou d'attributs dans Tomcat 6 et 7 contiennent de nombreux espaces qui se suivent
291             final StringBuilder sb = new StringBuilder(description);
292             while (indexOf != -1) {
293                 sb.deleteCharAt(indexOf);
294                 indexOf = sb.indexOf("  ");
295             }
296             return sb.toString();
297         }
298         return description;
299     }
300
301     private Object convertValueIfNeeded(Object value) {
302         if (value instanceof CompositeData) {
303             final CompositeData data = (CompositeData) value;
304             final Map<String, Object> values = new TreeMap<>();
305             for (final String key : data.getCompositeType().keySet()) {
306                 values.put(key, convertValueIfNeeded(data.get(key)));
307             }
308             return values;
309         } else if (value instanceof CompositeData[]) {
310             final List<Object> list = new ArrayList<>();
311             for (final CompositeData data : (CompositeData[]) value) {
312                 list.add(convertValueIfNeeded(data));
313             }
314             return list;
315         } else if (value instanceof Object[]) {
316             return Arrays.asList((Object[]) value);
317         } else if (value instanceof TabularData) {
318             final TabularData tabularData = (TabularData) value;
319             return convertValueIfNeeded(tabularData.values());
320         } else if (value instanceof Collection) {
321             final List<Object> list = new ArrayList<>();
322             for (final Object data : (Collection<?>) value) {
323                 list.add(convertValueIfNeeded(data));
324             }
325             return list;
326         }
327         return convertJRockitValueIfNeeded(value);
328     }
329
330     private static Object convertJRockitValueIfNeeded(Object value) {
331         if (value instanceof double[]) {
332             // pour jrockit MBeans
333             final List<Double> list = new ArrayList<>();
334             for (final double data : (double[]) value) {
335                 list.add(data);
336             }
337             return list;
338         } else if (value instanceof int[]) {
339             // pour jrockit MBeans
340             final List<Integer> list = new ArrayList<>();
341             for (final int data : (int[]) value) {
342                 list.add(data);
343             }
344             return list;
345         }
346         return value;
347     }
348
349     private static List<Object> getConvertedAttributes(List<String> mbeanAttributes) {
350         initJRockitMBeansIfNeeded();
351
352         final List<Object> result = new ArrayList<>();
353         final List<MBeanServer> mBeanServers = getMBeanServers();
354         for (final String mbeansAttribute : mbeanAttributes) {
355             final int lastIndexOfPoint = mbeansAttribute.lastIndexOf('.');
356             if (lastIndexOfPoint <= 0) {
357                 throw new IllegalArgumentException(mbeansAttribute);
358             }
359             final String name = mbeansAttribute.substring(0, lastIndexOfPoint);
360             final String attribute = mbeansAttribute.substring(lastIndexOfPoint + 1);
361             // on ne veut pas afficher l'attribut password, jamais
362             // (notamment, dans users tomcat ou dans datasources tomcat)
363             if ("password".equalsIgnoreCase(attribute)) {
364                 throw new IllegalArgumentException(name + '.' + attribute);
365             }
366             InstanceNotFoundException instanceNotFoundException = null;
367             for (final MBeanServer mbeanServer : mBeanServers) {
368                 try {
369                     final MBeans mbeans = new MBeans(mbeanServer);
370                     final Object jmxValue = mbeans.convertValueIfNeeded(
371                             mbeans.getAttribute(new ObjectName(name), attribute));
372                     result.add(jmxValue);
373                     instanceNotFoundException = null;
374                     // ObjectName trouvé dans ce MBeanServer, inutile de chercher dans les suivants
375                     // où il n'est d'ailleurs pas
376                     break;
377                 } catch (final InstanceNotFoundException e) {
378                     // ObjectName non trouvé dans ce MBeanServer, donc on cherche dans le suivant
379                     // (nécessaire pour JBoss 5.0.x)
380                     instanceNotFoundException = e;
381                     continue;
382                 } catch (final JMException e) {
383                     throw new IllegalArgumentException(name + '.' + attribute, e);
384                 }
385             }
386             if (instanceNotFoundException != null) {
387                 throw new IllegalArgumentException(name + '.' + attribute,
388                         instanceNotFoundException);
389             }
390         }
391         return result;
392     }
393
394     public static String getConvertedAttributes(String jmxValueParameter) {
395         final List<String> mbeanAttributes = Arrays
396                 .asList(jmxValueParameter.split("[" + ATTRIBUTES_SEPARATOR + ']'));
397         final List<Object> jmxValues = getConvertedAttributes(mbeanAttributes);
398         final StringBuilder sb = new StringBuilder();
399         boolean first = true;
400         for (final Object jmxValue : jmxValues) {
401             if (first) {
402                 first = false;
403             } else {
404                 sb.append(ATTRIBUTES_SEPARATOR);
405             }
406             sb.append(jmxValue);
407         }
408         return sb.toString();
409     }
410
411     private String getAttributeDescription(String name, MBeanAttributeInfo[] attributeInfos) {
412         for (final MBeanAttributeInfo attributeInfo : attributeInfos) {
413             if (name.equals(attributeInfo.getName())) {
414                 // certaines descriptions d'attributs comme les NamingResources dans Tomcat 7 contiennent aussi des espaces qui se suivent
415                 final String attributeDescription = formatDescription(
416                         attributeInfo.getDescription());
417                 if (attributeDescription == null || name.equals(attributeDescription)
418                         || attributeDescription.isEmpty()) {
419                     // les attributs des MBeans de java.lang ont des descriptions égales aux noms,
420                     // ce sont des descriptions inutiles
421                     return null;
422                 }
423                 return attributeDescription;
424             }
425         }
426         return null;
427     }
428
429     /**
430      * Retourne le javax.management.MBeanServer de la plateforme.
431      * @return MBeanServer
432      */

433     public static MBeanServer getPlatformMBeanServer() {
434         return ManagementFactory.getPlatformMBeanServer();
435         // alternative (sauf pour Jenkins slaves):
436         //        final List<MBeanServer> mBeanServers = MBeanServerFactory.findMBeanServer(null);
437         //        if (!mBeanServers.isEmpty()) {
438         //            // il existe déjà un MBeanServer créé précédemment par Tomcat ou bien ci-dessous
439         //            return mBeanServers.get(0);
440         //        }
441         //        final MBeanServer server = MBeanServerFactory.createMBeanServer();
442         //        return server;
443     }
444
445     /**
446      * Retourne la liste de tous les {@link javax.management.MBeanServer}.
447      * @return List
448      */

449     private static List<MBeanServer> getMBeanServers() {
450         // par exemple avec JBoss 5.0.x, il y a un MBeanServer de la plateforme (defaultDomain null)
451         // et un MBeanServer de JBoss (defaultDomain "jboss")
452         return MBeanServerFactory.findMBeanServer(null);
453     }
454 }
455