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}