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.jdrupes.json.JsonBeanEncoder;
052import org.jgrapes.core.Channel;
053import org.jgrapes.core.Event;
054import org.jgrapes.core.Manager;
055import org.jgrapes.core.annotation.Handler;
056import org.jgrapes.webconsole.base.Conlet.RenderMode;
057import org.jgrapes.webconsole.base.ConsoleConnection;
058import org.jgrapes.webconsole.base.events.AddConletType;
059import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
060import org.jgrapes.webconsole.base.events.ConsoleReady;
061import org.jgrapes.webconsole.base.events.NotifyConletModel;
062import org.jgrapes.webconsole.base.events.NotifyConletView;
063import org.jgrapes.webconsole.base.events.RenderConlet;
064import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
065import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
066
067import com.fasterxml.jackson.databind.ObjectMapper;
068
069public class JmxBrowserConlet extends FreeMarkerConlet<Serializable> {
070
071    /** The mapper. */
072    @SuppressWarnings("PMD.FieldNamingConventions")
073    protected static final ObjectMapper mapper = new ObjectMapper();
074    private static final Set<RenderMode> MODES = RenderMode.asSet(
075        RenderMode.Preview, RenderMode.View);
076
077    private static MBeanServer mbeanServer
078        = ManagementFactory.getPlatformMBeanServer();
079
080    /**
081     * Creates a new component with its channel set to the given channel.
082     * 
083     * @param componentChannel the channel that the component's handlers listen
084     *            on by default and that {@link Manager#fire(Event, Channel...)}
085     *            sends the event to
086     */
087    public JmxBrowserConlet(Channel componentChannel) {
088        super(componentChannel);
089    }
090
091    /**
092     * On {@link ConsoleReady}, fire the {@link AddConletType}.
093     *
094     * @param event the event
095     * @param channel the channel
096     * @throws TemplateNotFoundException the template not found exception
097     * @throws MalformedTemplateNameException the malformed template name
098     *             exception
099     * @throws ParseException the parse exception
100     * @throws IOException Signals that an I/O exception has occurred.
101     */
102    @Handler
103    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
104            throws TemplateNotFoundException, MalformedTemplateNameException,
105            ParseException, IOException {
106        // Add conlet resources to page
107        channel.respond(new AddConletType(type())
108            .addRenderMode(RenderMode.Preview).setDisplayNames(
109                localizations(channel.supportedLocales(), "conletName"))
110            .addScript(new ScriptResource()
111                .setScriptUri(event.renderSupport().conletResource(
112                    type(), "JmxBrowser-functions.js"))
113                .setScriptType("module")));
114    }
115
116    @Override
117    protected Set<RenderMode> doRenderConlet(
118            RenderConletRequestBase<?> event, ConsoleConnection channel,
119            String conletId, Serializable conletState)
120            throws Exception {
121        Set<RenderMode> renderedAs = new HashSet<>();
122        if (event.renderAs().contains(RenderMode.Preview)) {
123            Template tpl = freemarkerConfig()
124                .getTemplate("JmxBrowser-preview.ftl.html");
125            channel.respond(new RenderConlet(type(), conletId,
126                processTemplate(event, tpl,
127                    fmModel(event, channel, conletId, conletState)))
128                        .setRenderAs(
129                            RenderMode.Preview.addModifiers(event.renderAs()))
130                        .setSupportedModes(MODES));
131            channel.respond(new NotifyConletView(type(),
132                conletId, "mbeansTree", genMBeansTree(),
133                "preview", true));
134            renderedAs.add(RenderMode.Preview);
135        }
136        if (event.renderAs().contains(RenderMode.View)) {
137            Template tpl
138                = freemarkerConfig().getTemplate("JmxBrowser-view.ftl.html");
139            channel.respond(new RenderConlet(type(), conletId,
140                processTemplate(event, tpl,
141                    fmModel(event, channel, conletId, conletState)))
142                        .setRenderAs(
143                            RenderMode.View.addModifiers(event.renderAs()))
144                        .setSupportedModes(MODES));
145            channel.respond(new NotifyConletView(type(),
146                conletId, "mbeansTree", genMBeansTree(),
147                "view", true));
148            renderedAs.add(RenderMode.View);
149        }
150        return renderedAs;
151    }
152
153    @Override
154    protected void doUpdateConletState(NotifyConletModel event,
155            ConsoleConnection channel, Serializable conletModel)
156            throws Exception {
157        event.stop();
158        if ("sendMBean".equals(event.method())) {
159            List<String> segments = event.param(0);
160            String domain = segments.get(0);
161            @SuppressWarnings("PMD.ReplaceHashtableWithMap")
162            Hashtable<String, String> props = new Hashtable<>();
163            for (int i = 1; i < segments.size(); i++) {
164                String[] keyProp = segments.get(i).split("=", 2);
165                props.put(keyProp[0], keyProp[1]);
166            }
167            Set<ObjectName> mbeanNames
168                = mbeanServer.queryNames(new ObjectName(domain, props), null);
169            if (mbeanNames.isEmpty()) {
170                return;
171            }
172            ObjectName mbeanName = mbeanNames.iterator().next();
173            MBeanInfo info = mbeanServer.getMBeanInfo(mbeanName);
174            var json = JsonBeanEncoder.create()
175                .writeObject(genAttributesInfo(mbeanName, info)).toJson();
176            Object model = mapper.readValue(json, Object.class);
177            channel.respond(new NotifyConletView(type(),
178                event.conletId(), "mbeanDetails",
179                new Object[] { model, null }));
180        }
181    }
182
183    public static class NodeDTO {
184        public String segment;
185        public String label;
186        public Set<NodeDTO> children;
187
188        public NodeDTO(String segment, String label, Set<NodeDTO> children) {
189            this.segment = segment;
190            this.label = label;
191            this.children = children;
192        }
193
194        public NodeDTO(String segment, String label) {
195            this(segment, label,
196                new TreeSet<>(Comparator.comparing((node) -> node.label)));
197        }
198
199        public String getSegment() {
200            return segment;
201        }
202
203        public String getLabel() {
204            return label;
205        }
206
207        public Set<NodeDTO> getChildren() {
208            return children;
209        }
210
211        @Override
212        public int hashCode() {
213            final int prime = 31;
214            int result = 1;
215            result
216                = prime * result + ((segment == null) ? 0 : segment.hashCode());
217            return result;
218        }
219
220        @Override
221        public boolean equals(Object obj) {
222            if (this == obj) {
223                return true;
224            }
225            if (obj == null) {
226                return false;
227            }
228            if (getClass() != obj.getClass()) {
229                return false;
230            }
231            NodeDTO other = (NodeDTO) obj;
232            if (segment == null) {
233                if (other.segment != null) {
234                    return false;
235                }
236            } else if (!segment.equals(other.segment)) {
237                return false;
238            }
239            return true;
240        }
241    }
242
243    private List<NodeDTO> genMBeansTree() {
244        Map<String, NodeDTO> trees = new TreeMap<>();
245        Set<ObjectName> mbeanNames = mbeanServer.queryNames(null, null);
246        for (ObjectName mbn : mbeanNames) {
247            NodeDTO domainGroup = trees.computeIfAbsent(mbn.getDomain(),
248                key -> new NodeDTO(mbn.getDomain(), mbn.getDomain()));
249            appendToGroup(domainGroup, "type",
250                new Hashtable<>(mbn.getKeyPropertyList()), mbn);
251        }
252        List<NodeDTO> roots = new ArrayList<>(trees.values());
253        return roots;
254    }
255
256    private void appendToGroup(NodeDTO parent, String property,
257            Hashtable<String, String> propsLeft, ObjectName mbn) {
258        if (!propsLeft.keySet().contains(property)) {
259            try {
260                String left = ObjectName.getInstance("tmp", propsLeft)
261                    .getCanonicalKeyPropertyListString();
262                parent.children.add(new NodeDTO(left, left, null));
263            } catch (MalformedObjectNameException e) {
264                // Shoudn't happen
265            }
266            return;
267        }
268        Set<NodeDTO> candidates = parent.children;
269        String partition = property + "=" + propsLeft.get(property);
270        NodeDTO match = candidates.stream()
271            .filter(n -> n.segment.equals(partition)).findFirst()
272            .orElseGet(() -> {
273                NodeDTO node = new NodeDTO(partition, propsLeft.get(property));
274                candidates.add(node);
275                return node;
276            });
277        propsLeft.remove(property);
278        appendToGroup(match, property.equals("type") ? "name" : "", propsLeft,
279            mbn);
280    }
281
282    public static class AttributeDTO {
283        private String name;
284        private Object value;
285        private boolean writable;
286
287        public AttributeDTO(String name, Object value, boolean writable) {
288            super();
289            this.name = name;
290            this.value = value;
291            this.writable = writable;
292        }
293
294        public String getName() {
295            return name;
296        }
297
298        public Object getValue() {
299            return value;
300        }
301
302        public boolean getWritable() {
303            return writable;
304        }
305    }
306
307    private static Set<Class<?>> simpleTypes = new HashSet<>();
308
309    static {
310        simpleTypes.add(BigDecimal.class);
311        simpleTypes.add(BigInteger.class);
312        simpleTypes.add(Boolean.class);
313        simpleTypes.add(Byte.class);
314        simpleTypes.add(Character.class);
315        simpleTypes.add(Date.class);
316        simpleTypes.add(Double.class);
317        simpleTypes.add(Float.class);
318        simpleTypes.add(Integer.class);
319        simpleTypes.add(Long.class);
320        simpleTypes.add(ObjectName.class);
321        simpleTypes.add(Short.class);
322        simpleTypes.add(String.class);
323        simpleTypes.add(Void.class);
324    }
325
326    private List<AttributeDTO> genAttributesInfo(ObjectName mbeanName,
327            MBeanInfo info) {
328        List<AttributeDTO> result = new ArrayList<>();
329        for (MBeanAttributeInfo attr : info.getAttributes()) {
330            try {
331                Object value
332                    = mbeanServer.getAttribute(mbeanName, attr.getName());
333                result.add(new AttributeDTO(attr.getName(), value,
334                    attr.isWritable()));
335            } catch (InstanceNotFoundException | RuntimeMBeanException
336                    | AttributeNotFoundException | ReflectionException
337                    | MBeanException | IllegalArgumentException e) {
338                // Ignore (shouldn't happen)
339            }
340        }
341        return result;
342    }
343
344}