001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2021  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconlet.jmxbrowser;
020
021import freemarker.core.ParseException;
022import freemarker.template.MalformedTemplateNameException;
023import freemarker.template.Template;
024import freemarker.template.TemplateNotFoundException;
025import java.io.IOException;
026import java.io.Serializable;
027import java.lang.management.ManagementFactory;
028import java.math.BigDecimal;
029import java.math.BigInteger;
030import java.util.ArrayList;
031import java.util.Comparator;
032import java.util.Date;
033import java.util.HashSet;
034import java.util.Hashtable;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.TreeMap;
039import java.util.TreeSet;
040import javax.management.AttributeNotFoundException;
041import javax.management.InstanceNotFoundException;
042import javax.management.MBeanAttributeInfo;
043import javax.management.MBeanException;
044import javax.management.MBeanInfo;
045import javax.management.MBeanServer;
046import javax.management.MalformedObjectNameException;
047import javax.management.ObjectName;
048import javax.management.ReflectionException;
049import javax.management.RuntimeMBeanException;
050import org.jdrupes.json.JsonArray;
051import org.jgrapes.core.Channel;
052import org.jgrapes.core.Event;
053import org.jgrapes.core.Manager;
054import org.jgrapes.core.annotation.Handler;
055import org.jgrapes.webconsole.base.Conlet.RenderMode;
056import org.jgrapes.webconsole.base.ConsoleConnection;
057import org.jgrapes.webconsole.base.events.AddConletType;
058import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
059import org.jgrapes.webconsole.base.events.ConsoleReady;
060import org.jgrapes.webconsole.base.events.NotifyConletModel;
061import org.jgrapes.webconsole.base.events.NotifyConletView;
062import org.jgrapes.webconsole.base.events.RenderConlet;
063import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
064import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
065
066public class JmxBrowserConlet extends FreeMarkerConlet<Serializable> {
067
068    private static final Set<RenderMode> MODES = RenderMode.asSet(
069        RenderMode.Preview, RenderMode.View);
070
071    private static MBeanServer mbeanServer
072        = ManagementFactory.getPlatformMBeanServer();
073
074    /**
075     * Creates a new component with its channel set to the given channel.
076     * 
077     * @param componentChannel the channel that the component's handlers listen
078     *            on by default and that {@link Manager#fire(Event, Channel...)}
079     *            sends the event to
080     */
081    public JmxBrowserConlet(Channel componentChannel) {
082        super(componentChannel);
083    }
084
085    /**
086     * On {@link ConsoleReady}, fire the {@link AddConletType}.
087     *
088     * @param event the event
089     * @param channel the channel
090     * @throws TemplateNotFoundException the template not found exception
091     * @throws MalformedTemplateNameException the malformed template name
092     *             exception
093     * @throws ParseException the parse exception
094     * @throws IOException Signals that an I/O exception has occurred.
095     */
096    @Handler
097    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
098            throws TemplateNotFoundException, MalformedTemplateNameException,
099            ParseException, IOException {
100        // Add conlet resources to page
101        channel.respond(new AddConletType(type())
102            .addRenderMode(RenderMode.Preview).setDisplayNames(
103                localizations(channel.supportedLocales(), "conletName"))
104            .addScript(new ScriptResource()
105                .setScriptUri(event.renderSupport().conletResource(
106                    type(), "jmxbrowser.min.js"))
107                .setScriptType("module")));
108    }
109
110    @Override
111    protected Set<RenderMode> doRenderConlet(
112            RenderConletRequestBase<?> event, ConsoleConnection channel,
113            String conletId, Serializable conletState)
114            throws Exception {
115        Set<RenderMode> renderedAs = new HashSet<>();
116        if (event.renderAs().contains(RenderMode.Preview)) {
117            Template tpl = freemarkerConfig()
118                .getTemplate("JmxBrowser-preview.ftl.html");
119            channel.respond(new RenderConlet(type(), conletId,
120                processTemplate(event, tpl,
121                    fmModel(event, channel, conletId, conletState)))
122                        .setRenderAs(
123                            RenderMode.Preview.addModifiers(event.renderAs()))
124                        .setSupportedModes(MODES));
125            channel.respond(new NotifyConletView(type(),
126                conletId, "mbeansTree", genMBeansTree(),
127                "preview", true));
128            renderedAs.add(RenderMode.Preview);
129        }
130        if (event.renderAs().contains(RenderMode.View)) {
131            Template tpl
132                = freemarkerConfig().getTemplate("JmxBrowser-view.ftl.html");
133            channel.respond(new RenderConlet(type(), conletId,
134                processTemplate(event, tpl,
135                    fmModel(event, channel, conletId, conletState)))
136                        .setRenderAs(
137                            RenderMode.View.addModifiers(event.renderAs()))
138                        .setSupportedModes(MODES));
139            channel.respond(new NotifyConletView(type(),
140                conletId, "mbeansTree", genMBeansTree(),
141                "view", true));
142            renderedAs.add(RenderMode.View);
143        }
144        return renderedAs;
145    }
146
147    @Override
148    protected void doUpdateConletState(NotifyConletModel event,
149            ConsoleConnection channel, Serializable conletModel)
150            throws Exception {
151        event.stop();
152        if ("sendMBean".equals(event.method())) {
153            JsonArray segments = (JsonArray) event.params().get(0);
154            String domain = segments.asString(0);
155            @SuppressWarnings("PMD.ReplaceHashtableWithMap")
156            Hashtable<String, String> props = new Hashtable<>();
157            for (int i = 1; i < segments.size(); i++) {
158                String[] keyProp = segments.asString(i).split("=", 2);
159                props.put(keyProp[0], keyProp[1]);
160            }
161            Set<ObjectName> mbeanNames
162                = mbeanServer.queryNames(new ObjectName(domain, props), null);
163            if (mbeanNames.isEmpty()) {
164                return;
165            }
166            ObjectName mbeanName = mbeanNames.iterator().next();
167            MBeanInfo info = mbeanServer.getMBeanInfo(mbeanName);
168            channel.respond(new NotifyConletView(type(),
169                event.conletId(), "mbeanDetails",
170                new Object[] { genAttributesInfo(mbeanName, info), null }));
171        }
172    }
173
174    public static class NodeDTO {
175        public String segment;
176        public String label;
177        public Set<NodeDTO> children;
178
179        public NodeDTO(String segment, String label, Set<NodeDTO> children) {
180            this.segment = segment;
181            this.label = label;
182            this.children = children;
183        }
184
185        public NodeDTO(String segment, String label) {
186            this(segment, label,
187                new TreeSet<>(Comparator.comparing((node) -> node.label)));
188        }
189
190        public String getSegment() {
191            return segment;
192        }
193
194        public String getLabel() {
195            return label;
196        }
197
198        public Set<NodeDTO> getChildren() {
199            return children;
200        }
201
202        @Override
203        public int hashCode() {
204            final int prime = 31;
205            int result = 1;
206            result
207                = prime * result + ((segment == null) ? 0 : segment.hashCode());
208            return result;
209        }
210
211        @Override
212        public boolean equals(Object obj) {
213            if (this == obj) {
214                return true;
215            }
216            if (obj == null) {
217                return false;
218            }
219            if (getClass() != obj.getClass()) {
220                return false;
221            }
222            NodeDTO other = (NodeDTO) obj;
223            if (segment == null) {
224                if (other.segment != null) {
225                    return false;
226                }
227            } else if (!segment.equals(other.segment)) {
228                return false;
229            }
230            return true;
231        }
232    }
233
234    private List<NodeDTO> genMBeansTree() {
235        Map<String, NodeDTO> trees = new TreeMap<>();
236        Set<ObjectName> mbeanNames = mbeanServer.queryNames(null, null);
237        for (ObjectName mbn : mbeanNames) {
238            NodeDTO domainGroup = trees.computeIfAbsent(mbn.getDomain(),
239                key -> new NodeDTO(mbn.getDomain(), mbn.getDomain()));
240            appendToGroup(domainGroup, "type",
241                new Hashtable<>(mbn.getKeyPropertyList()), mbn);
242        }
243        List<NodeDTO> roots = new ArrayList<>(trees.values());
244        return roots;
245    }
246
247    private void appendToGroup(NodeDTO parent, String property,
248            Hashtable<String, String> propsLeft, ObjectName mbn) {
249        if (!propsLeft.keySet().contains(property)) {
250            try {
251                String left = ObjectName.getInstance("tmp", propsLeft)
252                    .getCanonicalKeyPropertyListString();
253                parent.children.add(new NodeDTO(left, left, null));
254            } catch (MalformedObjectNameException e) {
255                // Shoudn't happen
256            }
257            return;
258        }
259        Set<NodeDTO> candidates = parent.children;
260        String partition = property + "=" + propsLeft.get(property);
261        NodeDTO match = candidates.stream()
262            .filter(n -> n.segment.equals(partition)).findFirst()
263            .orElseGet(() -> {
264                NodeDTO node = new NodeDTO(partition, propsLeft.get(property));
265                candidates.add(node);
266                return node;
267            });
268        propsLeft.remove(property);
269        appendToGroup(match, property.equals("type") ? "name" : "", propsLeft,
270            mbn);
271    }
272
273    public static class AttributeDTO {
274        private String name;
275        private Object value;
276        private boolean writable;
277
278        public AttributeDTO(String name, Object value, boolean writable) {
279            super();
280            this.name = name;
281            this.value = value;
282            this.writable = writable;
283        }
284
285        public String getName() {
286            return name;
287        }
288
289        public Object getValue() {
290            return value;
291        }
292
293        public boolean getWritable() {
294            return writable;
295        }
296    }
297
298    private static Set<Class<?>> simpleTypes = new HashSet<>();
299
300    static {
301        simpleTypes.add(BigDecimal.class);
302        simpleTypes.add(BigInteger.class);
303        simpleTypes.add(Boolean.class);
304        simpleTypes.add(Byte.class);
305        simpleTypes.add(Character.class);
306        simpleTypes.add(Date.class);
307        simpleTypes.add(Double.class);
308        simpleTypes.add(Float.class);
309        simpleTypes.add(Integer.class);
310        simpleTypes.add(Long.class);
311        simpleTypes.add(ObjectName.class);
312        simpleTypes.add(Short.class);
313        simpleTypes.add(String.class);
314        simpleTypes.add(Void.class);
315    }
316
317    private List<AttributeDTO> genAttributesInfo(ObjectName mbeanName,
318            MBeanInfo info) {
319        List<AttributeDTO> result = new ArrayList<>();
320        for (MBeanAttributeInfo attr : info.getAttributes()) {
321            try {
322                Object value
323                    = mbeanServer.getAttribute(mbeanName, attr.getName());
324                result.add(new AttributeDTO(attr.getName(), value,
325                    attr.isWritable()));
326            } catch (InstanceNotFoundException | RuntimeMBeanException
327                    | AttributeNotFoundException | ReflectionException
328                    | MBeanException | IllegalArgumentException e) {
329                // Ignore (shouldn't happen)
330            }
331        }
332        return result;
333    }
334
335}