View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.monitoring.reporting.web.plugin.jmx;
18  
19  import org.apache.commons.codec.binary.Base64;
20  import org.apache.commons.monitoring.MonitoringException;
21  import org.apache.commons.monitoring.configuration.Configuration;
22  import org.apache.commons.monitoring.reporting.web.handler.api.Regex;
23  import org.apache.commons.monitoring.reporting.web.handler.api.Template;
24  import org.apache.commons.monitoring.reporting.web.template.MapBuilder;
25  import org.apache.commons.monitoring.util.ClassLoaders;
26  
27  import javax.management.MBeanAttributeInfo;
28  import javax.management.MBeanInfo;
29  import javax.management.MBeanOperationInfo;
30  import javax.management.MBeanParameterInfo;
31  import javax.management.MBeanServerConnection;
32  import javax.management.ObjectInstance;
33  import javax.management.ObjectName;
34  import javax.management.openmbean.CompositeData;
35  import javax.management.openmbean.TabularData;
36  import java.io.IOException;
37  import java.lang.management.ManagementFactory;
38  import java.lang.reflect.Array;
39  import java.lang.reflect.Field;
40  import java.util.Arrays;
41  import java.util.Collection;
42  import java.util.HashMap;
43  import java.util.LinkedList;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Set;
47  
48  public class JMXEndpoints {
49      private static final boolean METHOD_INVOCATION_ALLOWED = Configuration.is(Configuration.COMMONS_MONITORING_PREFIX + "jmx.method.allowed", true);
50  
51      private static final Map<String, Class<?>> WRAPPERS = new HashMap<String, Class<?>>();
52  
53      static {
54          for (final Class<?> c : Arrays.<Class<?>>asList(Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class, Boolean.class )) {
55              try {
56                  final Field f = c.getField("TYPE");
57                  Class<?> p = (Class<?>) f.get(null);
58                  WRAPPERS.put(p.getName(), c);
59              } catch (Exception e) {
60                  throw new AssertionError(e);
61              }
62          }
63      }
64  
65      protected final MBeanServerConnection server;
66  
67      public JMXEndpoints() {
68          this.server = ManagementFactory.getPlatformMBeanServer();
69      }
70  
71      @Regex("")
72      public Template home() throws IOException {
73          return new Template("jmx/main.vm", new MapBuilder<String, Object>().set("jmxTree", buildJmxTree()).build());
74      }
75  
76      @Regex("/operation/([^/]*)/([^/]*)/(.*)")
77      public String invokeOperation(final String objectNameBase64, final String method, final String[] parameters) {
78          if (!METHOD_INVOCATION_ALLOWED) {
79              throw new MonitoringException("Method invocation not allowed");
80          }
81  
82          try {
83              final ObjectName name = new ObjectName(new String(Base64.decodeBase64(objectNameBase64)));
84              final MBeanInfo info = server.getMBeanInfo(name);
85              for (final MBeanOperationInfo op : info.getOperations()) {
86                  if (op.getName().equals(method)) {
87                      final MBeanParameterInfo[] signature = op.getSignature();
88                      final String[] sign = new String[signature.length];
89                      for (int i = 0; i < sign.length; i++) {
90                          sign[i] = signature[i].getType();
91                      }
92                      final Object result = server.invoke(name, method, convertParams(signature, parameters), sign);
93                      return "<div>Method was invoked and returned:</div>" + value(result);
94                  }
95              }
96          } catch (final Exception e) {
97              return "<div class=\"alert alert-error\">\n" +
98                  "\n" + e.getMessage() + "\n" +
99                  "</div>";
100         }
101 
102         return "<div class=\"alert alert-error\">Operation" + method + " not found.</div>";
103     }
104 
105     @Regex("/([^/]*)")
106     public Template mbean(final String objectNameBase64) {
107         try {
108             final ObjectName name = new ObjectName(new String(Base64.decodeBase64(objectNameBase64)));
109             final MBeanInfo info = server.getMBeanInfo(name);
110             return new Template("templates/jmx/mbean.vm",
111                 new MapBuilder<String, Object>()
112                     .set("objectname", name.toString())
113                     .set("objectnameHash", Base64.encodeBase64String(name.toString().getBytes()))
114                     .set("classname", info.getClassName())
115                     .set("description", value(info.getDescription()))
116                     .set("attributes", attributes(name, info))
117                     .set("operations", operations(info))
118                     .build(), false);
119         } catch (final Exception e) {
120             throw new MonitoringException(e);
121         }
122     }
123 
124     private JMXNode buildJmxTree() throws IOException {
125         final JMXNode root = new JMXNode("/");
126 
127         for (final ObjectInstance instance : server.queryMBeans(null, null)) {
128             final ObjectName objectName = instance.getObjectName();
129             JMXNode.addNode(root, objectName.getDomain(), objectName.getKeyPropertyListString());
130         }
131 
132         return root;
133     }
134 
135     private Object[] convertParams(final MBeanParameterInfo[] signature, final String[] params) {
136         if (params == null) {
137             return null;
138         }
139 
140         final Object[] convertedParams = new Object[signature.length];
141         for (int i = 0; i < signature.length; i++) {
142             if (i < params.length) {
143                 convertedParams[i] = convert(signature[i].getType(), params[i]);
144             } else {
145                 convertedParams[i] = null;
146             }
147         }
148         return convertedParams;
149     }
150 
151     public static Object convert(final String type, final String value) {
152         try {
153             if (WRAPPERS.containsKey(type)) {
154                 if (type.equals(Character.TYPE.getName())) {
155                     return value.charAt(0);
156                 } else {
157                     return tryStringConstructor(type, value);
158                 }
159             }
160 
161             if (type.equals(Character.class.getName())) {
162                 return value.charAt(0);
163             }
164 
165             if (Number.class.isAssignableFrom(ClassLoaders.current().loadClass(type))) {
166                 return toNumber(value);
167             }
168 
169             if (value == null || value.equals("null")) {
170                 return null;
171             }
172 
173             return tryStringConstructor(type, value);
174         } catch (final Exception e) {
175             throw new MonitoringException(e);
176         }
177     }
178 
179     private static Number toNumber(final String value) throws NumberFormatException {
180         // first the user can force the conversion
181         final char lastChar = Character.toLowerCase(value.charAt(value.length() - 1));
182         if (lastChar == 'd') {
183             return Double.valueOf(value.substring(0, value.length() - 1));
184         }
185         if (lastChar == 'l') {
186             return Long.valueOf(value.substring(0, value.length() - 1));
187         }
188         if (lastChar == 'f') {
189             return Float.valueOf(value.substring(0, value.length() - 1));
190         }
191 
192         // try all conversions in cascade until it works
193         for (final Class<?> clazz : new Class<?>[] { Integer.class, Long.class, Double.class }) {
194             try {
195                 return Number.class.cast(clazz.getMethod("valueOf").invoke(null, value));
196             } catch (final Exception e) {
197                 // no-op
198             }
199         }
200 
201         throw new MonitoringException(value + " is not a number");
202     }
203 
204     private static Object tryStringConstructor(String type, final String value) throws Exception {
205         return ClassLoaders.current().loadClass(type).getConstructor(String.class).newInstance(value);
206     }
207 
208     private Collection<MBeanOperation> operations(final MBeanInfo info) {
209         final Collection<MBeanOperation> operations = new LinkedList<MBeanOperation>();
210         for (final MBeanOperationInfo operationInfo : info.getOperations()) {
211             final MBeanOperation mBeanOperation = new MBeanOperation(operationInfo.getName(), operationInfo.getReturnType());
212             for (final MBeanParameterInfo param : operationInfo.getSignature()) {
213                 mBeanOperation.getParameters().add(new MBeanParameter(param.getName(), param.getType()));
214             }
215             operations.add(mBeanOperation);
216         }
217         return operations;
218     }
219 
220     private Collection<MBeanAttribute> attributes(final ObjectName name, final MBeanInfo info) {
221         final Collection<MBeanAttribute> list = new LinkedList<MBeanAttribute>();
222         for (final MBeanAttributeInfo attribute : info.getAttributes()) {
223             Object value;
224             try {
225                 value = server.getAttribute(name, attribute.getName());
226             } catch (final Exception e) {
227                 value = "<div class=\"alert-error\">" + e.getMessage() + "</div>";
228             }
229             list.add(new MBeanAttribute(attribute.getName(), attribute.getType(), attribute.getDescription(), value(value)));
230         }
231         return list;
232     }
233 
234     private static String value(final Object value) {
235         try {
236             if (value == null) {
237                 return "";
238             }
239 
240             if (value.getClass().isArray()) {
241                 final int length = Array.getLength(value);
242                 if (length == 0) {
243                     return "";
244                 }
245 
246                 final StringBuilder builder = new StringBuilder().append("<ul>");
247                 for (int i = 0; i < length; i++) {
248                     builder.append("<li>").append(value(Array.get(value, i))).append("</li>");
249                 }
250                 builder.append("</ul>");
251                 return builder.toString();
252             }
253 
254             if (Collection.class.isInstance(value)) {
255                 final StringBuilder builder = new StringBuilder().append("<ul>");
256                 for (final Object o : Collection.class.cast(value)) {
257                     builder.append("<li>").append(value(o)).append("</li>");
258                 }
259                 builder.append("</ul>");
260                 return builder.toString();
261             }
262 
263             if (TabularData.class.isInstance(value)) {
264                 final TabularData td = TabularData.class.cast(value);
265                 final StringBuilder builder = new StringBuilder().append("<table class=\"table table-condensed\">");
266                 for (final Object type : td.keySet()) {
267                     final List<?> values = (List<?>) type;
268                     final CompositeData data = td.get(values.toArray(new Object[values.size()]));
269                     builder.append("<tr>");
270                     final Set<String> dataKeys = data.getCompositeType().keySet();
271                     for (final String k : data.getCompositeType().keySet()) {
272                         builder.append("<td>").append(value(data.get(k))).append("</td>");
273                     }
274                     builder.append("</tr>");
275                 }
276                 builder.append("</table>");
277 
278                 return builder.toString();
279             }
280 
281             if (CompositeData.class.isInstance(value)) {
282                 final CompositeData cd = CompositeData.class.cast(value);
283                 final Set<String> keys = cd.getCompositeType().keySet();
284 
285                 final StringBuilder builder = new StringBuilder().append("<table class=\"table table-condensed\">");
286                 for (final String type : keys) {
287                     builder.append("<tr><td>").append(type).append("</td><td>").append(value(cd.get(type))).append("</td></tr>");
288                 }
289                 builder.append("</table>");
290 
291                 return builder.toString();
292 
293             }
294 
295             if (Map.class.isInstance(value)) {
296                 final Map<?, ?> map = Map.class.cast(value);
297 
298                 final StringBuilder builder = new StringBuilder().append("<table class=\"table table-condensed\">");
299                 for (final Map.Entry<?, ?> entry : map.entrySet()) {
300                     builder.append("<tr><tr>").append(value(entry.getKey())).append("</td><td>").append(value(entry.getValue())).append("</td></tr>");
301                 }
302                 builder.append("</table>");
303 
304                 return builder.toString();
305 
306             }
307 
308             return value.toString();
309         } catch (final Exception e) {
310             throw new MonitoringException(e);
311         }
312     }
313 
314     public static class MBeanAttribute {
315         private final String name;
316         private final String type;
317         private final String description;
318         private final String value;
319 
320         public MBeanAttribute(final String name, final String type, final String description, final String value) {
321             this.name = name;
322             this.type = type;
323             this.value = value;
324             if (description != null) {
325                 this.description = description;
326             } else {
327                 this.description = "No description";
328             }
329         }
330 
331         public String getName() {
332             return name;
333         }
334 
335         public String getType() {
336             return type;
337         }
338 
339         public String getDescription() {
340             return description;
341         }
342 
343         public String getValue() {
344             return value;
345         }
346     }
347 
348     public static class MBeanOperation {
349         private final String name;
350         private final String returnType;
351         private final Collection<MBeanParameter> parameters = new LinkedList<MBeanParameter>();
352 
353         public MBeanOperation(final String name, final String returnType) {
354             this.name = name;
355             this.returnType = returnType;
356         }
357 
358         public String getName() {
359             return name;
360         }
361 
362         public String getReturnType() {
363             return returnType;
364         }
365 
366         public Collection<MBeanParameter> getParameters() {
367             return parameters;
368         }
369     }
370 
371     public static class MBeanParameter {
372         private final String name;
373         private final String type;
374 
375         public MBeanParameter(final String name, final String type) {
376             this.name = name;
377             this.type = type.replace("java.lang.", "");
378         }
379 
380         public String getName() {
381             return name;
382         }
383 
384         public String getType() {
385             return type;
386         }
387     }
388 }