001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.monitoring.reporting.web.plugin.jmx;
018
019 import org.apache.commons.codec.binary.Base64;
020 import org.apache.commons.monitoring.MonitoringException;
021 import org.apache.commons.monitoring.configuration.Configuration;
022 import org.apache.commons.monitoring.reporting.web.handler.api.Regex;
023 import org.apache.commons.monitoring.reporting.web.handler.api.Template;
024 import org.apache.commons.monitoring.reporting.web.template.MapBuilder;
025 import org.apache.commons.monitoring.util.ClassLoaders;
026
027 import javax.management.MBeanAttributeInfo;
028 import javax.management.MBeanInfo;
029 import javax.management.MBeanOperationInfo;
030 import javax.management.MBeanParameterInfo;
031 import javax.management.MBeanServerConnection;
032 import javax.management.ObjectInstance;
033 import javax.management.ObjectName;
034 import javax.management.openmbean.CompositeData;
035 import javax.management.openmbean.TabularData;
036 import java.io.IOException;
037 import java.lang.management.ManagementFactory;
038 import java.lang.reflect.Array;
039 import java.lang.reflect.Field;
040 import java.util.Arrays;
041 import java.util.Collection;
042 import java.util.HashMap;
043 import java.util.LinkedList;
044 import java.util.List;
045 import java.util.Map;
046 import java.util.Set;
047
048 public class JMXEndpoints {
049 private static final boolean METHOD_INVOCATION_ALLOWED = Configuration.is(Configuration.COMMONS_MONITORING_PREFIX + "jmx.method.allowed", true);
050
051 private static final Map<String, Class<?>> WRAPPERS = new HashMap<String, Class<?>>();
052
053 static {
054 for (final Class<?> c : Arrays.<Class<?>>asList(Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class, Boolean.class )) {
055 try {
056 final Field f = c.getField("TYPE");
057 Class<?> p = (Class<?>) f.get(null);
058 WRAPPERS.put(p.getName(), c);
059 } catch (Exception e) {
060 throw new AssertionError(e);
061 }
062 }
063 }
064
065 protected final MBeanServerConnection server;
066
067 public JMXEndpoints() {
068 this.server = ManagementFactory.getPlatformMBeanServer();
069 }
070
071 @Regex("")
072 public Template home() throws IOException {
073 return new Template("jmx/main.vm", new MapBuilder<String, Object>().set("jmxTree", buildJmxTree()).build());
074 }
075
076 @Regex("/operation/([^/]*)/([^/]*)/(.*)")
077 public String invokeOperation(final String objectNameBase64, final String method, final String[] parameters) {
078 if (!METHOD_INVOCATION_ALLOWED) {
079 throw new MonitoringException("Method invocation not allowed");
080 }
081
082 try {
083 final ObjectName name = new ObjectName(new String(Base64.decodeBase64(objectNameBase64)));
084 final MBeanInfo info = server.getMBeanInfo(name);
085 for (final MBeanOperationInfo op : info.getOperations()) {
086 if (op.getName().equals(method)) {
087 final MBeanParameterInfo[] signature = op.getSignature();
088 final String[] sign = new String[signature.length];
089 for (int i = 0; i < sign.length; i++) {
090 sign[i] = signature[i].getType();
091 }
092 final Object result = server.invoke(name, method, convertParams(signature, parameters), sign);
093 return "<div>Method was invoked and returned:</div>" + value(result);
094 }
095 }
096 } catch (final Exception e) {
097 return "<div class=\"alert alert-error\">\n" +
098 "\n" + e.getMessage() + "\n" +
099 "</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 }