package com.citi.get.cet.comet.tools;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.SmartLifecycle;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import static org.apache.commons.lang.StringUtils.trim;
public class SpringVisualizer implements ApplicationContextAware, SmartLifecycle {
private ConfigurableApplicationContext startContext;
private boolean isRunning = false;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.startContext = (ConfigurableApplicationContext) applicationContext;
}
@Override
public void start() {
StringBuilder dot = new StringBuilder("digraph G {\n");
// VERTICAL LAYOUT BIAS - Force top-to-bottom layout
dot.append(" rankdir=TB;\n"); // Top to Bottom direction
dot.append(" ranksep=0.8;\n"); // Increased vertical spacing
dot.append(" nodesep=0.5;\n"); // Horizontal spacing between nodes
dot.append(" splines=ortho;\n"); // Orthogonal edges for cleaner layout
dot.append(" compound=true;\n"); // Allow clustering
// Node styling with fixed width to prevent overflow
dot.append(" node [shape=plaintext, fontname=\"Arial\", fontsize=9, margin=0];\n");
dot.append(" edge [penwidth=1.5, arrowsize=0.8];\n\n");
// Collect all beans first for better organization
Map<String, BeanInfo> allBeans = new LinkedHashMap<>();
Set<String> referencedBeans = new HashSet<>();
// First pass: collect all beans and dependencies
ApplicationContext current = this.startContext;
while (current != null) {
ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext) current).getBeanFactory();
for (String name : factory.getBeanDefinitionNames()) {
if (!allBeans.containsKey(name)) {
BeanDefinition def = factory.getBeanDefinition(name);
allBeans.put(name, new BeanInfo(def, factory));
// Collect dependencies to identify referenced beans
for (String dep : factory.getDependenciesForBean(name)) {
referencedBeans.add(dep);
}
}
}
current = current.getParent();
}
// Create subgraphs to group referenced vs unreferenced beans
dot.append(" subgraph cluster_referenced {\n");
dot.append(" label=\"Referenced Beans\";\n");
dot.append(" fontname=\"Arial\";\n");
dot.append(" fontsize=12;\n");
dot.append(" style=filled;\n");
dot.append(" fillcolor=\"#F9F9F9\";\n");
dot.append(" color=\"#CCCCCC\";\n");
dot.append(" margin=20;\n\n");
// Render referenced beans first
for (Map.Entry<String, BeanInfo> entry : allBeans.entrySet()) {
String name = entry.getKey();
if (referencedBeans.contains(name) || isReferencedByOthers(name, allBeans)) {
renderBeanNode(dot, name, entry.getValue());
}
}
dot.append(" }\n\n");
// Create subgraph for unreferenced beans (orphans)
dot.append(" subgraph cluster_unreferenced {\n");
dot.append(" label=\"Unreferenced Beans (Orphans)\";\n");
dot.append(" fontname=\"Arial\";\n");
dot.append(" fontsize=12;\n");
dot.append(" style=filled;\n");
dot.append(" fillcolor=\"#FFF9E6\";\n");
dot.append(" color=\"#FFCC66\";\n");
dot.append(" margin=20;\n\n");
// Render unreferenced beans
for (Map.Entry<String, BeanInfo> entry : allBeans.entrySet()) {
String name = entry.getKey();
if (!referencedBeans.contains(name) && !isReferencedByOthers(name, allBeans)) {
renderBeanNode(dot, name, entry.getValue());
}
}
dot.append(" }\n\n");
// Draw dependency arrows (edges)
for (Map.Entry<String, BeanInfo> entry : allBeans.entrySet()) {
String name = entry.getKey();
for (String dep : entry.getValue().dependencies) {
if (allBeans.containsKey(dep)) {
dot.append(String.format(" \"%s\" -> \"%s\" [penwidth=1.5];\n", name, dep));
}
}
}
dot.append("}\n");
writeToFile(dot.toString());
this.isRunning = true;
}
private void renderBeanNode(StringBuilder dot, String beanName, BeanInfo info) {
String color = beanName.toLowerCase().contains("comet") ? "#FFF904" :
(beanName.toLowerCase().contains("citi") ? "#CBE6C7" : "#E1F5FE");
dot.append(String.format(" \"%s\" [label=<", beanName));
// FIXED: Table with FIXED WIDTH and WRAPPING to prevent overflow
dot.append("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"5\" ");
dot.append("FIXEDSIZE=\"FALSE\" WIDTH=\"280\" BALIGN=\"LEFT\">");
dot.append("<BGCOLOR=\"").append(color).append("\"/>");
// Header with better wrapping
dot.append("<TR><TD COLSPAN=\"2\" BGCOLOR=\"#DDDDDD\" CELLPADDING=\"6\" ");
dot.append("WIDTH=\"280\" ALIGN=\"CENTER\"><B>");
dot.append(wrapText(xmlEscape(trim(beanName)), 35));
dot.append("</B></TD></TR>");
// Property count indicator
int propCount = info.propertyValues.size();
int argCount = info.constructorArgs.size();
if (propCount > 0 || argCount > 0) {
dot.append("<TR><TD COLSPAN=\"2\" BGCOLOR=\"#EEEEEE\" CELLPADDING=\"2\" ");
dot.append("ALIGN=\"CENTER\"><FONT POINT-SIZE=\"9\">");
dot.append(propCount).append(" properties");
if (argCount > 0) dot.append(" | ").append(argCount).append(" constructor args");
dot.append("</FONT></TD></TR>");
}
// Constructor arguments (prioritize these first)
for (ConstructorArg arg : info.constructorArgs) {
dot.append("<TR>");
dot.append("<TD ALIGN=\"LEFT\" CELLPADDING=\"3\" WIDTH=\"100\">");
dot.append("<FONT FACE=\"Courier\" POINT-SIZE=\"9\">");
dot.append(xmlEscape(arg.name));
dot.append("</FONT></TD>");
dot.append("<TD ALIGN=\"LEFT\" CELLPADDING=\"3\" WIDTH=\"180\">");
dot.append(trimAndWrap(extractValue(arg.value), 40));
dot.append("</TD>");
dot.append("</TR>");
}
// Properties (with wrapping)
for (PropertyValue pv : info.propertyValues) {
dot.append("<TR>");
dot.append("<TD ALIGN=\"LEFT\" CELLPADDING=\"3\" WIDTH=\"100\">");
dot.append("<FONT FACE=\"Courier\" POINT-SIZE=\"9\">");
dot.append(xmlEscape(pv.getName()));
dot.append("</FONT></TD>");
dot.append("<TD ALIGN=\"LEFT\" CELLPADDING=\"3\" WIDTH=\"180\">");
dot.append(trimAndWrap(extractValue(pv.getValue()), 40));
dot.append("</TD>");
dot.append("</TR>");
}
// Show empty state if no content
if (propCount == 0 && argCount == 0) {
dot.append("<TR><TD COLSPAN=\"2\" ALIGN=\"CENTER\" CELLPADDING=\"8\">");
dot.append("<FONT POINT-SIZE=\"9\" COLOR=\"#999999\">no configuration</FONT>");
dot.append("</TD></TR>");
}
dot.append("</TABLE>>];\n");
}
private String wrapText(String text, int maxLength) {
if (text.length() <= maxLength) return text;
StringBuilder wrapped = new StringBuilder();
int start = 0;
while (start < text.length()) {
int end = Math.min(start + maxLength, text.length());
wrapped.append(text.substring(start, end));
if (end < text.length()) {
wrapped.append("<BR ALIGN=\"LEFT\"/>");
}
start = end;
}
return wrapped.toString();
}
private String trimAndWrap(String text, int maxLength) {
if (text == null) return "";
String trimmed = text.trim();
if (trimmed.length() <= maxLength) return trimmed;
return trimmed.substring(0, maxLength - 3).trim() + "...";
}
private boolean isReferencedByOthers(String beanName, Map<String, BeanInfo> allBeans) {
for (BeanInfo info : allBeans.values()) {
if (info.dependencies.contains(beanName)) {
return true;
}
}
return false;
}
private String extractValue(Object value) {
if (value == null) return "null";
if (value instanceof TypedStringValue) {
String raw = ((TypedStringValue) value).getValue();
return xmlEscape(raw != null ? raw.trim() : "null");
}
if (value instanceof BeanReference) {
return "@" + ((BeanReference) value).getBeanName();
}
if (value instanceof BeanDefinition) {
String className = ((BeanDefinition) value).getBeanClassName();
if (className != null) {
return "<i>" + className.substring(className.lastIndexOf('.') + 1) + "</i>";
}
return "<i>InnerBean</i>";
}
if (value instanceof Iterable) {
List<String> cleaned = new ArrayList<>();
int count = 0;
for (Object item : (Iterable<?>) value) {
if (count++ >= 8) {
cleaned.add("...(" + ((Iterable<?>) value).spliterator().getExactSizeIfKnown() + " more)");
break;
}
cleaned.add(trim(extractValue(item)));
}
String result = String.join(", ", cleaned);
return result.length() > 50 ? result.substring(0, 47) + "..." : result;
}
if (value instanceof Map) {
StringBuilder mapStr = new StringBuilder();
int count = 0;
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
if (count++ >= 5) {
mapStr.append("...(").append(((Map<?, ?>) value).size() - 5).append(" more)");
break;
}
if (count > 1) mapStr.append(", ");
mapStr.append(extractValue(entry.getKey())).append("=").append(extractValue(entry.getValue()));
}
return mapStr.toString();
}
String result = value.toString().trim();
if (result.length() > 50) {
result = result.substring(0, 47) + "...";
}
return xmlEscape(result);
}
private void writeToFile(String content) {
try {
File projectRoot = findProjectRoot();
File buildDir = new File(projectRoot, "build");
if (!buildDir.exists()) {
buildDir.mkdirs();
}
File file = new File(buildDir, "spring-beans.dot");
try (FileWriter writer = new FileWriter(file)) {
writer.write(content);
}
String fileUrl = file.toURI().toString();
String sep = "==================================================";
System.out.println("\n" + sep);
System.out.println("SPRING BEANS VISUALIZATION GENERATED");
System.out.println("Layout: Vertical (Top-to-Bottom)");
System.out.println("Root Detected: " + projectRoot.getAbsolutePath());
System.out.println("Click to open: " + fileUrl);
System.out.println(sep + "\n");
} catch (IOException e) {
System.err.println("CRITICAL: Failed to write DOT file: " + e.getMessage());
}
}
private File findProjectRoot() {
String userDir = System.getProperty("user.dir");
File current = new File(userDir);
while (current != null) {
if (new File(current, "build.gradle").exists() ||
new File(current, "settings.gradle").exists() ||
new File(current, "build.gradle.kts").exists() ||
new File(current, "pom.xml").exists()) {
return current;
}
current = current.getParentFile();
}
return new File(userDir);
}
private String xmlEscape(String input) {
if (input == null) return "";
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
// Inner class to hold bean information
private static class BeanInfo {
List<PropertyValue> propertyValues = new ArrayList<>();
List<ConstructorArg> constructorArgs = new ArrayList<>();
Set<String> dependencies = new HashSet<>();
BeanInfo(BeanDefinition def, ConfigurableListableBeanFactory factory) {
// Collect properties
for (PropertyValue pv : def.getPropertyValues().getPropertyValues()) {
propertyValues.add(pv);
}
// Collect constructor arguments
ConstructorArgumentValues cav = def.getConstructorArgumentValues();
for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cav.getIndexedArgumentValues().entrySet()) {
constructorArgs.add(new ConstructorArg("arg[" + entry.getKey() + "]", entry.getValue().getValue()));
}
for (ConstructorArgumentValues.ValueHolder vh : cav.getGenericArgumentValues()) {
String type = vh.getType() != null ?
vh.getType().substring(vh.getType().lastIndexOf('.') + 1) : "arg";
constructorArgs.add(new ConstructorArg(type, vh.getValue()));
}
// Collect dependencies
try {
for (String dep : factory.getDependenciesForBean(
def.getBeanClassName() != null ? def.getBeanClassName() : "")) {
dependencies.add(dep);
}
} catch (Exception e) {
// Ignore dependency resolution errors
}
}
}
private static class ConstructorArg {
String name;
Object value;
ConstructorArg(String name, Object value) {
this.name = name;
this.value = value;
}
}
@Override
public int getPhase() { return Integer.MAX_VALUE; }
@Override
public boolean isAutoStartup() { return true; }
@Override
public void stop() { this.isRunning = false; }
@Override
public boolean isRunning() { return this.isRunning; }
@Override
public void stop(Runnable callback) { stop(); callback.run(); }
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)