Created
September 25, 2025 06:58
-
-
Save engalar/c3f2281cdc9df5053605fc596e6b80ff to your computer and use it in GitHub Desktop.
Analyzes userlib and vendorlib JARs to identify and visualize potential version conflicts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # --- 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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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