Skip to content

Instantly share code, notes, and snippets.

@engalar
Created September 25, 2025 06:58
Show Gist options
  • Select an option

  • Save engalar/c3f2281cdc9df5053605fc596e6b80ff to your computer and use it in GitHub Desktop.

Select an option

Save engalar/c3f2281cdc9df5053605fc596e6b80ff to your computer and use it in GitHub Desktop.
Analyzes userlib and vendorlib JARs to identify and visualize potential version conflicts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="assets/tailwindcss.js"></script>
<style>
/* Simple transition for collapsible sections */
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s ease-in-out;
}
.collapsible-content.open {
max-height: 2000px; /* Large enough for content */
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<div id="app"></div>
<script src="assets/vendor-bundle.umd.js"></script>
<script src="assets/babel.min.js"></script>
<script type="text/babel">
const { React, ReactDOM, redi, rediReact } = globalThis.__tmp;
delete globalThis.__tmp;
const { createIdentifier, setDependencies } = redi;
const { connectDependencies, useDependency } = rediReact;
const { useState, useEffect, useReducer, useCallback, Fragment, useMemo, useRef } = React;
// ===================================================================
// 1. STATE & COMMUNICATION SERVICES (RPC Architecture) - UNCHANGED
// ===================================================================
class MessageStore {
constructor() { this.log = []; this.listeners = new Set(); }
addLogEntry(entry) { this.log = [entry, ...this.log.slice(0, 99)]; this.notify(); } // Keep log to 100 entries
subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
notify() { this.listeners.forEach(listener => listener()); }
}
const IMessageService = createIdentifier('IMessageService');
class BrowserMessageService {
constructor(messageStore) {
this.messageStore = messageStore;
this.requestId = 0;
this.pendingRequests = new Map();
this.RPC_TIMEOUT = 15000; // 15 seconds for potentially slow analysis
window.addEventListener('message', this.handleBackendResponse);
}
async call(type, payload) {
const correlationId = `req-${this.requestId++}`;
const command = { type, payload, correlationId };
this.messageStore.addLogEntry({ type: 'request', correlationId, command });
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
if (this.pendingRequests.has(correlationId)) {
this.pendingRequests.delete(correlationId);
const error = new Error(`RPC call for '${type}' timed out.`);
this.messageStore.addLogEntry({ type: 'timeout', correlationId, error: error.message });
reject(error);
}
}, this.RPC_TIMEOUT);
this.pendingRequests.set(correlationId, { resolve, reject, timeoutId });
window.parent.sendMessage("frontend:message", command);
});
}
handleBackendResponse = (event) => {
if (event.data?.type !== 'backendResponse') return;
try {
const response = JSON.parse(event.data.data);
const { correlationId } = response;
if (!this.pendingRequests.has(correlationId)) return;
const { resolve, reject, timeoutId } = this.pendingRequests.get(correlationId);
clearTimeout(timeoutId);
this.pendingRequests.delete(correlationId);
this.messageStore.addLogEntry({ type: 'response', correlationId, response });
if (response.status === 'success') resolve(response.data);
else reject(new Error(response.message || 'Unknown backend error.'));
} catch (e) {
this.messageStore.addLogEntry({ type: 'error', error: "Frontend failed to parse backend response." });
}
};
}
setDependencies(BrowserMessageService, [MessageStore]);
// Reusable hook for handling RPC calls
const useRpc = () => {
const messageService = useDependency(IMessageService);
const [state, setState] = useState({ isLoading: false, error: null, data: null });
const execute = useCallback(async (type, payload) => {
setState({ isLoading: true, error: null, data: null });
try {
const result = await messageService.call(type, payload);
setState({ isLoading: false, error: null, data: result });
return result;
} catch (err) {
setState({ isLoading: false, error: err.message, data: null });
throw err;
}
}, [messageService]);
return { ...state, execute };
};
// ===================================================================
// 2. REFACTORED & INTERACTIVE UI COMPONENTS
// ===================================================================
const ChevronDownIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" className={className || "h-5 w-5"} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
);
const SortIcon = ({ direction }) => {
if (direction === 'ascending') return <span className="text-gray-600">▲</span>;
if (direction === 'descending') return <span className="text-gray-600">▼</span>;
return <span className="text-gray-300">▲▼</span>;
}
const CollapsibleSection = ({ title, children, startOpen = true, badge, badgeColor = 'gray' }) => {
const [isOpen, setIsOpen] = useState(startOpen);
useEffect(() => {
setIsOpen(startOpen);
}, [startOpen]);
return (
<div className="border rounded-lg bg-white shadow-sm mb-4">
<button
className="w-full flex justify-between items-center p-3 text-left font-semibold text-lg text-gray-800 bg-gray-50 rounded-t-lg hover:bg-gray-100 focus:outline-none"
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex items-center">
<span>{title}</span>
{badge !== undefined && (
<span className={`ml-3 px-2.5 py-0.5 text-xs font-semibold rounded-full bg-${badgeColor}-100 text-${badgeColor}-800`}>
{badge}
</span>
)}
</div>
<ChevronDownIcon className={`w-6 h-6 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
</button>
<div className={`collapsible-content ${isOpen ? 'open' : ''}`}>
<div className="p-4 border-t">{children}</div>
</div>
</div>
);
};
const SummaryCard = ({ title, value, color, onClick }) => (
<button
onClick={onClick}
className={`p-4 rounded-lg text-center cursor-pointer transition-all duration-200 hover:shadow-lg hover:-translate-y-1 bg-${color}-100 w-full focus:outline-none focus:ring-2 focus:ring-${color}-400`}
>
<div className={`text-3xl font-bold text-${color}-800`}>{value}</div>
<div className={`text-sm text-${color}-600 mt-1`}>{title}</div>
</button>
);
const ConflictsTable = ({ conflicts, onLibraryClick }) => {
const conflictLibs = Object.keys(conflicts);
if (conflictLibs.length === 0) {
return (
<div className="p-6 bg-green-50 border border-green-200 rounded-lg text-center">
<h3 className="text-xl font-semibold text-green-800">No Conflicts Detected</h3>
<p className="text-green-600">All libraries with identifiable versions appear consistent.</p>
</div>
);
}
return (
<div className="space-y-3">
{conflictLibs.map(libName => (
<div key={libName} className="bg-white rounded-md shadow-sm border border-red-200">
<h4
className="font-bold text-md p-2 bg-red-100 text-red-900 border-b border-red-200 cursor-pointer hover:bg-red-200 transition-colors"
onClick={() => onLibraryClick(libName)}
title={`Click to filter dependencies for "${libName}"`}
>
{libName}
</h4>
<ul className="divide-y divide-gray-200">
{conflicts[libName].map((instance, index) => (
<li key={index} className="p-3 grid grid-cols-6 gap-3 items-center">
<div className="col-span-3 font-semibold text-gray-800">{instance.version}</div>
<div className="col-span-3 px-2 py-1 text-xs font-medium rounded-full text-center text-white bg-blue-500">{instance.source}</div>
<div className="col-span-6 text-gray-600 text-sm mt-1">{instance.details}</div>
</li>
))}
</ul>
</div>
))}
</div>
);
};
const useSortableData = (items, config = { key: 'library_name', direction: 'ascending' }) => {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
let sortableItems = [...items];
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
}
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key) => {
let direction = 'ascending';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({ key, direction });
};
return { items: sortedItems, requestSort, sortConfig };
};
const AllDependenciesTable = ({ dependencies, filter, setFilter }) => {
const { items: sortedDeps, requestSort, sortConfig } = useSortableData(dependencies);
const filteredDeps = useMemo(() =>
sortedDeps.filter(dep =>
dep.library_name.toLowerCase().includes(filter.toLowerCase()) ||
dep.version.toLowerCase().includes(filter.toLowerCase())
),
[sortedDeps, filter]);
const SortableHeader = ({ children, name }) => (
<th scope="col" className="px-4 py-3 cursor-pointer hover:bg-gray-200" onClick={() => requestSort(name)}>
<div className="flex items-center justify-between">
{children}
<SortIcon direction={sortConfig?.key === name ? sortConfig.direction : null} />
</div>
</th>
);
return (
<div>
<input
type="text"
placeholder={`Filter ${dependencies.length} dependencies by name or version...`}
value={filter}
onChange={e => setFilter(e.target.value)}
className="w-full p-2 mb-4 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<div className="overflow-auto bg-white rounded-lg shadow" style={{maxHeight: '60vh'}}>
<table className="w-full text-sm text-left text-gray-700">
<thead className="text-xs text-gray-800 uppercase bg-gray-100 sticky top-0">
<tr>
<SortableHeader name="library_name">Library Name</SortableHeader>
<SortableHeader name="version">Version</SortableHeader>
<SortableHeader name="source">Source</SortableHeader>
<th scope="col" className="px-4 py-3">Details</th>
</tr>
</thead>
<tbody>
{filteredDeps.length > 0 ? filteredDeps.map((dep, index) => (
<tr key={index} className="border-b hover:bg-gray-50">
<td className="px-4 py-2 font-medium">{dep.library_name}</td>
<td className="px-4 py-2">{dep.version}</td>
<td className="px-4 py-2">{dep.source}</td>
<td className="px-4 py-2 text-gray-600 truncate max-w-xs" title={dep.details}>{dep.details}</td>
</tr>
)) : (
<tr>
<td colSpan="4" className="text-center py-8 text-gray-500">No dependencies match your filter.</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
const JarAnalysisTool = () => {
const { execute, isLoading, error, data } = useRpc();
const [filter, setFilter] = useState('');
const conflictsRef = useRef(null);
const dependenciesRef = useRef(null);
const handleAnalyze = () => {
setFilter('');
execute('ANALYZE_JARS', {});
};
const scrollTo = (ref) => ref.current?.scrollIntoView({ behavior: 'smooth' });
return (
<div className="p-4 md:p-6 max-w-6xl mx-auto">
<header className="bg-white shadow-md rounded-lg p-4 mb-6 flex flex-col md:flex-row justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-800">JAR Conflict Visualizer</h1>
<p className="text-gray-600 text-sm mt-1">Analyze userlib and platform dependencies to find version conflicts.</p>
</div>
<button onClick={handleAnalyze} className="mt-4 md:mt-0 w-full md:w-auto px-6 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-wait shrink-0" disabled={isLoading}>
{isLoading ? 'Analyzing...' : 'Run Analysis'}
</button>
</header>
<main>
{error && <div className="p-4 mb-4 bg-red-100 text-red-800 rounded-md"><strong>Error:</strong> {error}</div>}
{data && (
<div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<SummaryCard title="Userlib JARs" value={data.summary.userlib_count} color="blue" onClick={() => scrollTo(dependenciesRef)} />
<SummaryCard title="Vendorlib JARs" value={data.summary.sbom_count} color="green" onClick={() => scrollTo(dependenciesRef)} />
<SummaryCard title="Total Dependencies" value={data.summary.total_count} color="purple" onClick={() => scrollTo(dependenciesRef)} />
<SummaryCard title="Conflicts Found" value={data.summary.conflict_count} color={data.summary.conflict_count > 0 ? 'red' : 'gray'} onClick={() => scrollTo(conflictsRef)} />
</div>
<div ref={conflictsRef}>
<CollapsibleSection
title="Conflict Analysis"
startOpen={data.summary.conflict_count > 0}
badge={data.summary.conflict_count}
badgeColor={data.summary.conflict_count > 0 ? 'red' : 'green'}
>
<ConflictsTable
conflicts={data.conflicts}
onLibraryClick={(libName) => {
setFilter(libName);
scrollTo(dependenciesRef);
}}
/>
</CollapsibleSection>
</div>
<div ref={dependenciesRef}>
<CollapsibleSection title="Full Dependency List" badge={data.dependencies.length}>
<AllDependenciesTable dependencies={data.dependencies} filter={filter} setFilter={setFilter} />
</CollapsibleSection>
</div>
</div>
)}
{!isLoading && !data && !error && (
<div className="text-center py-16 bg-white rounded-lg shadow-md">
<h2 className="text-xl text-gray-700">Ready to find conflicts?</h2>
<p className="text-gray-500 mt-2">Click "Run Analysis" to begin.</p>
</div>
)}
</main>
</div>
);
};
// ===================================================================
// 3. MAIN APP & IOC CONFIGURATION - UNCHANGED
// ===================================================================
const AppWithDependencies = connectDependencies(JarAnalysisTool, [
[MessageStore],
[IMessageService, { useClass: BrowserMessageService }],
]);
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<AppWithDependencies />);
</script>
</body>
</html>
# --- Base Imports ---
import clr
from System.Text.Json import JsonSerializer
import json
import traceback
from typing import Any, Dict, Callable
# --- Dependency Injection ---
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
# --- JAR Analysis Imports ---
import os
import re
from collections import defaultdict
import pandas as pd
# pythonnet library setup for embedding C#
clr.AddReference("System.Text.Json")
clr.AddReference("Mendix.StudioPro.ExtensionsAPI")
from Mendix.StudioPro.ExtensionsAPI.Model.DomainModels import IDomainModel, IEntity
from Mendix.StudioPro.ExtensionsAPI.Model.Projects import IModule
# Runtime environment provides these globals
# PostMessage, ShowDevTools, currentApp, root, dockingWindowService
# --- Initial Setup ---
PostMessage("backend:clear", '')
ShowDevTools()
# ===================================================================
# 1. JAR CONFLICT ANALYSIS LOGIC (FROM YOUR SCRIPT)
# ===================================================================
def parse_userlib_dir(directory_path: str) -> list:
"""Analyzes a Mendix userlib directory to parse JARs and identify their sources."""
jar_pattern = re.compile(r'^(.*?)-(\d+(?:\.\d+)*.*?)\.jar$')
generic_jar_pattern = re.compile(r'^(.*?)_([\d\.]+.*?)\.jar$')
required_by_pattern = re.compile(r'^(.*\.jar)\.(.*?)\.(RequiredLib|Required\.by.*)$')
jar_info = {}
try:
filenames = os.listdir(directory_path)
except FileNotFoundError:
return []
for filename in filenames:
if filename.endswith('.jar'):
lib_name, version = None, None
match = jar_pattern.match(filename)
if not match:
match = generic_jar_pattern.match(filename)
if match:
lib_name, version = match.groups()
lib_name = lib_name.replace('org.apache.commons.', 'commons-')
lib_name = lib_name.replace('org.apache.httpcomponents.', '')
jar_info[filename] = {
'library_name': lib_name if lib_name else filename.replace('.jar', ''),
'version': version if version else 'unknown',
'source': 'userlib',
'details': {'filename': filename, 'required_by': set()}
}
for filename in filenames:
if 'Required' in filename:
match = required_by_pattern.match(filename)
if match:
jar_filename, module_name, _ = match.groups()
if jar_filename in jar_info:
jar_info[jar_filename]['details']['required_by'].add(module_name)
dependency_list = []
for info in jar_info.values():
required_by_str = ", ".join(sorted(list(info['details']['required_by']))) or "Unknown"
info['details'] = f"File: {info['details']['filename']} (Required by: {required_by_str})"
dependency_list.append(info)
return dependency_list
def parse_sbom_file(sbom_path: str) -> list:
"""Parses a CycloneDX SBOM JSON file to extract dependency information."""
dependency_list = []
try:
with open(sbom_path, 'r', encoding='utf-8') as f:
sbom_data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
components = sbom_data.get('components', [])
for comp in components:
lib_name = comp.get('name')
if lib_name:
dependency_list.append({
'library_name': lib_name,
'version': comp.get('version', 'unknown'),
'source': 'SBOM (vendorlib)',
'details': f"PURL: {comp.get('purl', 'N/A')}"
})
return dependency_list
def analyze_conflicts(dependencies: list) -> dict:
"""Analyzes a combined list of dependencies to find version conflicts."""
grouped_libs = defaultdict(list)
for dep in dependencies:
if dep['version'] != 'unknown':
grouped_libs[dep['library_name']].append({
'version': dep['version'],
'source': dep['source'],
'details': dep['details']
})
conflicts = {lib_name: versions_info for lib_name, versions_info in grouped_libs.items() if len({info['version'] for info in versions_info}) > 1}
return conflicts
# ===================================================================
# 2. ABSTRACTIONS AND SERVICES (NEW ARCHITECTURE)
# ===================================================================
class MendixEnvironmentService:
"""Abstracts away the Mendix host environment's global variables."""
def __init__(self, app_context, post_message_func: Callable):
self.app = app_context
self.post_message = post_message_func
class JarConflictService:
"""Handles the business logic for analyzing JARs."""
def __init__(self, mendix_env: MendixEnvironmentService):
self._mendix_env = mendix_env
def analyze_project(self, payload: Dict) -> Dict:
"""Runs the full JAR analysis for the current Mendix project."""
project_path = self._mendix_env.app.Root.DirectoryPath
self._mendix_env.post_message("backend:info", f"Analyzing project at: {project_path}")
userlib_path = os.path.join(project_path, 'userlib')
# Check for SBOM in both potential Mendix 9 and 10+ locations
sbom_path_mx10 = os.path.join(userlib_path, 'vendorlib-sbom.json')
sbom_path_mx9 = os.path.join(project_path, 'vendorlib', 'vendorlib-sbom.json')
sbom_path = sbom_path_mx10 if os.path.exists(sbom_path_mx10) else sbom_path_mx9
userlib_deps = parse_userlib_dir(userlib_path)
sbom_deps = parse_sbom_file(sbom_path)
all_dependencies = userlib_deps + sbom_deps
# Create DataFrame for analysis and reporting
df = pd.DataFrame(all_dependencies)
all_deps_list = []
if not df.empty:
df = df[['library_name', 'version', 'source', 'details']]
df = df.sort_values(by=['library_name', 'version']).reset_index(drop=True)
all_deps_list = df.to_dict('records') # Convert to list of dicts for JSON
conflict_report = analyze_conflicts(all_dependencies)
return {
"dependencies": all_deps_list,
"conflicts": conflict_report,
"summary": {
"userlib_count": len(userlib_deps),
"sbom_count": len(sbom_deps),
"total_count": len(all_dependencies),
"conflict_count": len(conflict_report)
}
}
class AppController:
"""Handles routing of commands from the frontend to specific services."""
def __init__(self, jar_service: JarConflictService, mendix_env: MendixEnvironmentService):
self._mendix_env = mendix_env
self._command_handlers: Dict[str, Callable[[Dict], Any]] = {
"ANALYZE_JARS": jar_service.analyze_project,
}
def dispatch(self, request: Dict) -> Dict:
"""Dispatches a request and ensures a consistently formatted response."""
command_type = request.get("type")
payload = request.get("payload", {})
correlation_id = request.get("correlationId")
handler = self._command_handlers.get(command_type)
if not handler:
return self._create_error_response(f"No handler for command: {command_type}", correlation_id)
try:
result = handler(payload)
return self._create_success_response(result, correlation_id)
except Exception as e:
error_message = f"Error executing '{command_type}': {e}"
self._mendix_env.post_message("backend:info", f"{error_message}\n{traceback.format_exc()}")
return self._create_error_response(error_message, correlation_id, {"traceback": traceback.format_exc()})
def _create_success_response(self, data: Any, correlation_id: str) -> Dict:
return {"status": "success", "data": data, "correlationId": correlation_id}
def _create_error_response(self, message: str, correlation_id: str, data: Any = None) -> Dict:
return {"status": "error", "message": message, "data": data or {}, "correlationId": correlation_id}
# ===================================================================
# 3. IOC CONTAINER CONFIGURATION
# ===================================================================
class Container(containers.DeclarativeContainer):
"""The IoC container that wires all services together."""
config = providers.Configuration()
mendix_env = providers.Singleton(
MendixEnvironmentService,
app_context=config.app_context,
post_message_func=config.post_message_func,
)
jar_conflict_service = providers.Singleton(JarConflictService, mendix_env=mendix_env)
app_controller = providers.Singleton(
AppController,
jar_service=jar_conflict_service,
mendix_env=mendix_env,
)
# ===================================================================
# 4. APPLICATION ENTRYPOINT
# ===================================================================
container = Container()
container.config.from_dict({
"app_context": currentApp,
"post_message_func": PostMessage,
})
def onMessage(e: Any):
"""Entry point for all messages from the frontend."""
controller = container.app_controller()
if e.Message != "frontend:message":
return
try:
request_string = JsonSerializer.Serialize(e.Data)
request_object = json.loads(request_string)
if "correlationId" not in request_object:
PostMessage("backend:info", f"Msg without correlationId: {request_object}")
return
response = controller.dispatch(request_object)
PostMessage("backend:response", json.dumps(response))
except Exception as ex:
PostMessage("backend:info", f"Fatal error in onMessage: {ex}\n{traceback.format_exc()}")
correlation_id = "unknown"
try:
# Attempt to recover correlationId for a proper error response
request_string = JsonSerializer.Serialize(e.Data)
request_object = json.loads(request_string)
correlation_id = request_object.get("correlationId", "unknown")
except:
pass
error_response = controller._create_error_response(
f"A fatal error occurred in the Python backend: {ex}",
correlation_id,
{"traceback": traceback.format_exc()}
)
PostMessage("backend:response", json.dumps(error_response))
{
"name": "JAR Conflict Visualizer",
"author": "AI Assistant",
"email": "",
"ui": "index.html",
"plugin": "main.py",
"description": "Analyzes userlib and vendorlib JARs to identify and visualize potential version conflicts.",
"deps": [
"pythonnet",
"dependency-injector",
"pandas"
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment