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}