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    }