diff --git a/appinfo/routes.php b/appinfo/routes.php index 388f8b3d..df580324 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -131,6 +131,14 @@ ['name' => 'settings#getSyncConfig', 'url' => '/api/settings/sync/config', 'verb' => 'GET'], ['name' => 'settings#updateSyncConfig', 'url' => '/api/settings/sync/config', 'verb' => 'POST'], + // ======================================================================== + // VIEW API ENDPOINTS - ArchiMate Views with Enrichment Support + // ======================================================================== + + // View API endpoints for querying and enriching ArchiMate views + ['name' => 'view#getAllViews', 'url' => '/api/views', 'verb' => 'GET'], + ['name' => 'view#getView', 'url' => '/api/views/{viewId}', 'verb' => 'GET'], + ['name' => 'view#getApiDocumentation', 'url' => '/api/views/docs', 'verb' => 'GET'], ], ]; diff --git a/debug_export_issues.php b/debug_export_issues.php new file mode 100644 index 00000000..5ead81ec --- /dev/null +++ b/debug_export_issues.php @@ -0,0 +1,208 @@ +getDatabaseConnection(); + + // First check what columns exist + $stmt = $db->prepare('SHOW COLUMNS FROM oc_openregister_objects'); + $stmt->execute(); + $columns = $stmt->fetchAll(); + echo "Database columns available:\n"; + foreach (array_slice($columns, 0, 10) as $column) { + echo " - {$column['Field']} ({$column['Type']})\n"; + } + echo "\n"; + + // Check total objects count + $stmt = $db->prepare('SELECT COUNT(*) as count FROM oc_openregister_objects'); + $stmt->execute(); + $totalCount = $stmt->fetch()['count']; + echo "Total objects in database: $totalCount\n"; + + // Check if register column exists and what values it has + try { + $stmt = $db->prepare('SELECT DISTINCT register, COUNT(*) as count FROM oc_openregister_objects GROUP BY register ORDER BY count DESC LIMIT 10'); + $stmt->execute(); + $registers = $stmt->fetchAll(); + echo "Register distribution:\n"; + foreach ($registers as $register) { + echo " - Register {$register['register']}: {$register['count']} objects\n"; + } + + // Focus on register 20 (AMEF) + $stmt = $db->prepare('SELECT COUNT(*) as count FROM oc_openregister_objects WHERE register = ?'); + $stmt->execute([20]); + $amefCount = $stmt->fetch()['count']; + } catch (Exception $e) { + echo "Could not check by register column: {$e->getMessage()}\n"; + $amefCount = $totalCount; // Use total as fallback + } + + if ($amefCount == 0) { + echo "āŒ No objects found in AMEF register! Import may have failed.\n"; + exit(1); + } + + echo "āœ… Found $amefCount objects to potentially export\n\n"; + + // Step 2: Check export service availability + echo "2ļøāƒ£ CHECKING EXPORT SERVICE\n"; + echo "---------------------------\n"; + + $container = \OC::$server->get(\Psr\Container\ContainerInterface::class); + + try { + $archiMateService = $container->get('OCA\\SoftwareCatalog\\Service\\ArchiMateService'); + echo "āœ… ArchiMateService available\n"; + } catch (Exception $e) { + echo "āŒ ArchiMateService not available: {$e->getMessage()}\n"; + exit(1); + } + + // Step 3: Test export method directly (bypass API) + echo "\n3ļøāƒ£ TESTING EXPORT METHOD DIRECTLY\n"; + echo "-----------------------------------\n"; + + try { + $settingsService = $container->get('OCA\\SoftwareCatalog\\Service\\SettingsService'); + + // Test basic export parameters + $exportOptions = [ + 'format' => 'archimate', + 'includeRelationships' => true, + 'includeViews' => false, // Start simple + 'organizationSpecific' => false + ]; + + echo "Attempting direct export with options:\n"; + print_r($exportOptions); + echo "\n"; + + // Check if export method exists + $reflection = new ReflectionClass($archiMateService); + if ($reflection->hasMethod('exportArchiMateXml')) { + echo "āœ… exportArchiMateXml method exists\n"; + + // Try to call it + $method = $reflection->getMethod('exportArchiMateXml'); + if ($method->isPublic() || $method->isProtected()) { + $method->setAccessible(true); + + echo "Calling export method...\n"; + $startTime = microtime(true); + + $result = $method->invoke($archiMateService, $exportOptions); + + $duration = microtime(true) - $startTime; + echo "Export completed in " . round($duration, 3) . " seconds\n"; + + if (is_array($result) && isset($result['success'])) { + if ($result['success']) { + echo "āœ… Export successful!\n"; + if (isset($result['xml'])) { + $xmlLength = strlen($result['xml']); + echo "Generated XML length: $xmlLength bytes\n"; + + // Validate XML + libxml_use_internal_errors(true); + $dom = new DOMDocument(); + if ($dom->loadXML($result['xml'])) { + echo "āœ… Generated XML is valid\n"; + + // Save sample for inspection + file_put_contents('/tmp/debug_export_sample.xml', $result['xml']); + echo "Sample saved to /tmp/debug_export_sample.xml\n"; + } else { + echo "āŒ Generated XML is invalid:\n"; + $errors = libxml_get_errors(); + foreach ($errors as $error) { + echo " - Line {$error->line}: {$error->message}"; + } + } + } + } else { + echo "āŒ Export failed: {$result['message']}\n"; + if (isset($result['error'])) { + echo "Error details: {$result['error']}\n"; + } + } + } else { + echo "āŒ Unexpected export result format\n"; + print_r($result); + } + } else { + echo "āŒ Export method is not accessible\n"; + } + } else { + echo "āŒ exportArchiMateXml method does not exist\n"; + + // List available methods + echo "Available methods:\n"; + $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); + foreach ($methods as $method) { + if (strpos(strtolower($method->getName()), 'export') !== false) { + echo " - {$method->getName()}\n"; + } + } + } + + } catch (Exception $e) { + echo "āŒ Direct export test failed: {$e->getMessage()}\n"; + echo "Stack trace:\n{$e->getTraceAsString()}\n"; + } + + // Step 4: Check SettingsController export method + echo "\n4ļøāƒ£ CHECKING SETTINGS CONTROLLER\n"; + echo "--------------------------------\n"; + + try { + $settingsController = $container->get('OCA\\SoftwareCatalog\\Controller\\SettingsController'); + echo "āœ… SettingsController available\n"; + + $reflection = new ReflectionClass($settingsController); + if ($reflection->hasMethod('exportArchiMate')) { + echo "āœ… exportArchiMate method exists in controller\n"; + } else { + echo "āŒ exportArchiMate method missing in controller\n"; + } + + } catch (Exception $e) { + echo "āŒ SettingsController issue: {$e->getMessage()}\n"; + } + + // Step 5: Summary and recommendations + echo "\n5ļøāƒ£ SUMMARY & RECOMMENDATIONS\n"; + echo "=============================\n"; + + if ($amefCount > 0) { + echo "āœ… Data is available for export\n"; + echo "šŸ“‹ NEXT STEPS:\n"; + echo " 1. Fix the export method implementation\n"; + echo " 2. Ensure XML generation is working correctly\n"; + echo " 3. Test with simple export options first\n"; + echo " 4. Gradually add complexity (relationships, views)\n"; + } else { + echo "āŒ No data available - import issues need to be resolved first\n"; + } + +} catch (Exception $e) { + echo "\nšŸ’„ DEBUG SCRIPT FAILED: {$e->getMessage()}\n"; + echo "Stack trace:\n{$e->getTraceAsString()}\n"; + exit(1); +} +?> diff --git a/docs/View_API.md b/docs/View_API.md new file mode 100644 index 00000000..dfcd7145 --- /dev/null +++ b/docs/View_API.md @@ -0,0 +1,329 @@ +# View API Documentation + +## Overview + +The SoftwareCatalog View API provides endpoints for querying and enriching ArchiMate views with additional data such as products, usage information (gebruik), and participation data (deelnames gebruik). + +**Base URL**: '/api/views' +**API Version**: 1.0.0 + +## Authentication + +All View API endpoints are currently configured as: +- `@NoAdminRequired` - No admin privileges required +- `@NoCSRFRequired` - No CSRF token required +- `@PublicPage` - Publicly accessible + +> Note: In production, you may want to adjust these security settings based on your requirements. + +## Endpoints + +### 1. Get All Views + +**Endpoint**: `GET /api/views` + +Returns all available ArchiMate views with optional enrichment data. + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `include_products` | boolean | No | false | Include product data in view nodes | +| `include_gebruik` | boolean | No | false | Include usage data in view nodes | +| `include_deelnames_gebruik` | boolean | No | false | Include participation usage data in view nodes | + +#### Boolean Parameter Formats + +The API accepts various formats for boolean parameters: +- **True values**: `true`, `1`, `yes`, `on`, `True`, `TRUE` +- **False values**: `false`, `0`, `no`, `off`, `False`, `FALSE` + +#### Example Requests + +```bash +# Get all views without enrichment +GET /api/views + +# Get all views with product data +GET /api/views?include_products=true + +# Get all views with multiple enrichments +GET /api/views?include_products=1&include_gebruik=yes&include_deelnames_gebruik=true +``` + +#### Response Format + +```json +{ + "success": true, + "views": [ + { + "id": "view-lv01", + "name": "LV01 BGT basisregistratie en SVB view", + "documentation": "Mocked from GEMMA LV01.", + "viewNodes": [ + { + "modelNodeId": "bs-assembleren", + "viewNodeId": "vn-assembleren-bgt", + "x": 7, + "y": 7, + "width": 341, + "height": 36, + "parent": null, + "name": "Assembleren BGT aanleveringen van bronhouders", + "type": "businessservice", + "color": "rgb(255, 255, 204)", + "borderColor": "rgb(0, 0, 0)", + "font": { + "name": "Segoe UI, Arial", + "size": 12, + "style": "normal", + "color": "rgb(0, 0, 0)" + }, + "description": null, + "elementRef": "bs-assembleren", + "products": [], // Only present if include_products=true + "gebruik": [], // Only present if include_gebruik=true + "deelnamesGebruik": [] // Only present if include_deelnames_gebruik=true + } + ], + "viewRelationships": [ + { + "modelRelationshipId": "rel-1", + "sourceId": "vn-aanleveringen-bgt", + "targetId": "vn-stuf-imgeo", + "viewRelationshipId": "rel-1", + "type": "realization", + "bendpoints": [ + { + "x": 92.5, + "y": 282.5 + } + ], + "label": {} + } + ] + } + ], + "count": 1, + "enrichments_applied": ["products", "gebruik"] +} +``` + +### 2. Get Specific View + +**Endpoint**: `GET /api/views/{viewId}` + +Returns a specific ArchiMate view by its identifier with optional enrichment data. + +#### URL Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `viewId` | string | Yes | The unique identifier of the view to retrieve | + +#### Query Parameters + +Same as "Get All Views" endpoint. + +#### Example Requests + +```bash +# Get specific view without enrichment +GET /api/views/view-lv01 + +# Get specific view with all enrichments +GET /api/views/view-lv01?include_products=true&include_gebruik=true&include_deelnames_gebruik=true +``` + +#### Response Format + +```json +{ + "success": true, + "view": { + "id": "view-lv01", + "name": "LV01 BGT basisregistratie en SVB view", + "documentation": "Mocked from GEMMA LV01.", + "viewNodes": [], + "viewRelationships": [] + }, + "enrichments_applied": [] +} +``` + +#### Error Response (View Not Found) + +```json +{ + "success": false, + "error": "View with ID 'non-existent-view' not found", + "view": null +} +``` + +### 3. Get API Documentation + +**Endpoint**: `GET /api/views/docs` + +Returns comprehensive API documentation in JSON format. + +#### Example Request + +```bash +GET /api/views/docs +``` + +#### Response Format + +Returns a detailed JSON object containing API version, endpoints, parameters, and examples. + +## Data Structures + +### ViewNode Structure + +Each view node contains the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `modelNodeId` | string | Reference to the ArchiMate element | +| `viewNodeId` | string | Unique identifier within the view | +| `x`, `y` | integer | Position coordinates | +| `width`, `height` | integer | Dimensions | +| `parent` | string\|null | Parent node for nested structures | +| `name` | string | Display name | +| `type` | string | Element type (e.g., "businessservice") | +| `color` | string | Background color in RGB format | +| `borderColor` | string | Border color in RGB format | +| `font` | object | Font styling information | +| `description` | string\|null | Element description | +| `elementRef` | string | Reference to the source element | + +### ViewRelationship Structure + +Each view relationship contains: + +| Property | Type | Description | +|----------|------|-------------| +| `modelRelationshipId` | string | Reference to the ArchiMate relationship | +| `sourceId` | string | Source node viewNodeId | +| `targetId` | string | Target node viewNodeId | +| `viewRelationshipId` | string | Unique identifier within the view | +| `type` | string | Relationship type (e.g., "realization") | +| `bendpoints` | array | Array of bend point coordinates | +| `label` | object | Label information and styling | + +## Enrichment Options + +### Products (`include_products=true`) + +When enabled, adds a `products` array to each viewNode containing product information related to the referenced element. + +**Implementation Status**: Placeholder - requires implementation of product data retrieval logic. + +### Gebruik (`include_gebruik=true`) + +When enabled, adds a `gebruik` array to each viewNode containing usage statistics and data. + +**Implementation Status**: Placeholder - requires implementation of usage data retrieval logic. + +### Deelnames Gebruik (`include_deelnames_gebruik=true`) + +When enabled, adds a `deelnamesGebruik` array to each viewNode containing participation-based usage data. + +**Implementation Status**: Placeholder - requires implementation of participation data retrieval logic. + +## Error Handling + +### HTTP Status Codes + +- `200 OK` - Request successful +- `400 Bad Request` - Invalid parameters (e.g., empty viewId) +- `404 Not Found` - View not found +- `500 Internal Server Error` - Server error + +### Error Response Format + +```json +{ + "success": false, + "error": "Error description", + "view": null // or "views": [] for getAllViews +} +``` + +## Implementation Notes + +### Performance Considerations + +- The API uses the ViewService for business logic separation +- Enrichment data is loaded upfront when any enrichment option is enabled +- View nodes are processed in batches for optimal performance +- Database queries are optimized using OpenRegister's ObjectService + +### Future Enhancements + +The API is designed to support future enhancements: + +1. **Authentication & Authorization**: Add proper security controls +2. **Pagination**: Support for large result sets +3. **Filtering**: Add query parameters for filtering views +4. **Caching**: Implement response caching for improved performance +5. **Real Enrichment Logic**: Replace placeholder enrichment methods with actual implementations + +### Dependencies + +- **OpenRegister**: Required for data storage and retrieval +- **AMEF Configuration**: Must be properly configured for view schema access +- **SettingsService**: Used for configuration management + +## Testing + +### Using curl + +```bash +# Test basic functionality +curl -X GET "http://localhost/index.php/apps/softwarecatalog/api/views" \\ + -H "Content-Type: application/json" + +# Test with enrichment +curl -X GET "http://localhost/index.php/apps/softwarecatalog/api/views?include_products=true" \\ + -H "Content-Type: application/json" + +# Test specific view +curl -X GET "http://localhost/index.php/apps/softwarecatalog/api/views/view-lv01" \\ + -H "Content-Type: application/json" +``` + +### Expected Data Flow + +1. **Client Request** → ViewController +2. **ViewController** → ViewService (business logic) +3. **ViewService** → OpenRegister ObjectService (data retrieval) +4. **ViewService** → Enrichment methods (if enabled) +5. **ViewService** → Response formatting +6. **ViewController** → JSON response to client + +## Integration Examples + +### JavaScript/Frontend Integration + +```javascript +// Get all views with products +const response = await fetch('/api/views?include_products=true'); +const data = await response.json(); + +if (data.success) { + console.log('Found views:', data.count); + data.views.forEach(view => { + console.log('View:', view.name); + view.viewNodes.forEach(node => { + if (node.products && node.products.length > 0) { + console.log(' Node products:', node.products); + } + }); + }); +} +``` + +This completes the View API implementation with full documentation for querying ArchiMate views with enrichment capabilities. diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 94498fa7..094078af 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -255,6 +255,18 @@ public function register(IRegistrationContext $context): void ); }); + // Register View service for ArchiMate views with enrichment capabilities + $context->registerService(\OCA\SoftwareCatalog\Service\ViewService::class, function ($container) { + return new \OCA\SoftwareCatalog\Service\ViewService( + $container->get(IAppConfig::class), + $container->get('OCP\App\IAppManager'), + $container, + $container->get('Psr\Log\LoggerInterface'), + $container->get(SettingsService::class), + $container->get('OCP\IUserSession') + ); + }); + // Register progress tracking service $context->registerService(\OCA\SoftwareCatalog\Service\ProgressTracker::class, function ($container) { return new \OCA\SoftwareCatalog\Service\ProgressTracker( diff --git a/lib/Controller/ViewController.php b/lib/Controller/ViewController.php new file mode 100644 index 00000000..c3b22a58 --- /dev/null +++ b/lib/Controller/ViewController.php @@ -0,0 +1,388 @@ +logger->info('API: Getting all views', [ + 'endpoint' => '/api/views', + 'method' => 'GET', + 'query_params' => $this->request->getParams() + ]); + + try { + // Parse query parameters for enrichment options + $options = $this->parseEnrichmentOptions(); + + // Get views from service with enrichments + $result = $this->viewService->getAllViews($options); + + // Return appropriate HTTP status code + $statusCode = $result['success'] ? 200 : 500; + + $this->logger->info('API: All views request completed', [ + 'success' => $result['success'], + 'views_count' => $result['count'] ?? 0, + 'enrichments_applied' => $result['enrichments_applied'] ?? [] + ]); + + return new JSONResponse($result, $statusCode); + + } catch (\Exception $e) { + $this->logger->error('API: Failed to get all views', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return new JSONResponse([ + 'success' => false, + 'error' => 'Internal server error: ' . $e->getMessage(), + 'views' => [], + 'count' => 0 + ], 500); + } + } + + /** + * Get a specific view by ID with optional enrichment + * + * API Endpoint: GET /api/views/{viewId} + * + * Query Parameters: + * - include_products (bool): Include product data in view nodes + * - include_modules (bool): Include module data in view nodes (linked via elementRef) + * - include_gebruik (bool): Include usage data in view nodes + * - include_deelnames_gebruik (bool): Include participation usage data in view nodes + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @param string $viewId The view identifier + * @return JSONResponse JSON response with view object + */ + public function getView(string $viewId): JSONResponse + { + $this->logger->info('API: Getting specific view', [ + 'endpoint' => "/api/views/{$viewId}", + 'method' => 'GET', + 'view_id' => $viewId, + 'query_params' => $this->request->getParams() + ]); + + try { + // Validate view ID + if (empty($viewId)) { + return new JSONResponse([ + 'success' => false, + 'error' => 'View ID is required', + 'view' => null + ], 400); + } + + // Parse query parameters for enrichment options + $options = $this->parseEnrichmentOptions(); + + // Get view from service with enrichments + $result = $this->viewService->getView($viewId, $options); + + // Return appropriate HTTP status code + $statusCode = $result['success'] ? 200 : ($result['view'] === null ? 404 : 500); + + $this->logger->info('API: Specific view request completed', [ + 'view_id' => $viewId, + 'success' => $result['success'], + 'found' => $result['view'] !== null, + 'enrichments_applied' => $result['enrichments_applied'] ?? [] + ]); + + return new JSONResponse($result, $statusCode); + + } catch (\Exception $e) { + $this->logger->error('API: Failed to get specific view', [ + 'view_id' => $viewId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return new JSONResponse([ + 'success' => false, + 'error' => 'Internal server error: ' . $e->getMessage(), + 'view' => null + ], 500); + } + } + + /** + * Parse enrichment options from query parameters + * + * @return array Parsed enrichment options + */ + private function parseEnrichmentOptions(): array + { + $options = []; + + // Parse boolean query parameters + $includeProducts = $this->request->getParam('include_products'); + if ($includeProducts !== null) { + $options['include_products'] = $this->parseBooleanParam($includeProducts); + } + + $includeModules = $this->request->getParam('include_modules'); + if ($includeModules !== null) { + $options['include_modules'] = $this->parseBooleanParam($includeModules); + } + + $includeGebruik = $this->request->getParam('include_gebruik'); + if ($includeGebruik !== null) { + $options['include_gebruik'] = $this->parseBooleanParam($includeGebruik); + } + + $includeDeelnamesGebruik = $this->request->getParam('include_deelnames_gebruik'); + if ($includeDeelnamesGebruik !== null) { + $options['include_deelnames_gebruik'] = $this->parseBooleanParam($includeDeelnamesGebruik); + } + + $this->logger->debug('Parsed enrichment options', [ + 'raw_params' => [ + 'include_products' => $includeProducts, + 'include_modules' => $includeModules, + 'include_gebruik' => $includeGebruik, + 'include_deelnames_gebruik' => $includeDeelnamesGebruik + ], + 'parsed_options' => $options + ]); + + return $options; + } + + /** + * Parse a boolean parameter from string values + * + * Accepts: true, false, 1, 0, "true", "false", "1", "0", "yes", "no" + * + * @param mixed $value The parameter value to parse + * @return bool The parsed boolean value + */ + private function parseBooleanParam($value): bool + { + if (is_bool($value)) { + return $value; + } + + if (is_numeric($value)) { + return (int)$value > 0; + } + + if (is_string($value)) { + $lowerValue = strtolower(trim($value)); + return in_array($lowerValue, ['true', '1', 'yes', 'on'], true); + } + + return false; + } + + /** + * Get API documentation for view endpoints + * + * API Endpoint: GET /api/views/docs + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @return JSONResponse JSON response with API documentation + */ + public function getApiDocumentation(): JSONResponse + { + $documentation = [ + 'api_version' => '1.0.0', + 'description' => 'SoftwareCatalog View API - Query and enrich ArchiMate views with additional data', + 'base_url' => '/api/views', + 'endpoints' => [ + [ + 'method' => 'GET', + 'path' => '/api/views', + 'description' => 'Get all views with optional enrichment', + 'parameters' => [ + [ + 'name' => 'include_products', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include product data in view nodes' + ], + [ + 'name' => 'include_gebruik', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include usage data in view nodes' + ], + [ + 'name' => 'include_deelnames_gebruik', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include participation usage data in view nodes' + ] + ], + 'response_example' => [ + 'success' => true, + 'views' => [ + [ + 'id' => 'view-lv01', + 'name' => 'LV01 BGT basisregistratie en SVB view', + 'documentation' => 'Mocked from GEMMA LV01.', + 'viewNodes' => [], + 'viewRelationships' => [] + ] + ], + 'count' => 1, + 'enrichments_applied' => [] + ] + ], + [ + 'method' => 'GET', + 'path' => '/api/views/{viewId}', + 'description' => 'Get a specific view by ID with optional enrichment', + 'parameters' => [ + [ + 'name' => 'viewId', + 'type' => 'string', + 'required' => true, + 'description' => 'The view identifier (in URL path)' + ], + [ + 'name' => 'include_products', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include product data in view nodes' + ], + [ + 'name' => 'include_gebruik', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include usage data in view nodes' + ], + [ + 'name' => 'include_deelnames_gebruik', + 'type' => 'boolean', + 'required' => false, + 'description' => 'Include participation usage data in view nodes' + ] + ], + 'response_example' => [ + 'success' => true, + 'view' => [ + 'id' => 'view-lv01', + 'name' => 'LV01 BGT basisregistratie en SVB view', + 'documentation' => 'Mocked from GEMMA LV01.', + 'viewNodes' => [], + 'viewRelationships' => [] + ], + 'enrichments_applied' => [] + ] + ], + [ + 'method' => 'GET', + 'path' => '/api/views/docs', + 'description' => 'Get this API documentation', + 'parameters' => [], + 'response_example' => '(this response)' + ] + ], + 'enrichment_options' => [ + [ + 'name' => 'products', + 'description' => 'Adds product information to view nodes that reference elements with product associations' + ], + [ + 'name' => 'gebruik', + 'description' => 'Adds usage/usage statistics to view nodes' + ], + [ + 'name' => 'deelnames_gebruik', + 'description' => 'Adds participation-based usage data to view nodes created from participation records' + ] + ], + 'boolean_parameter_formats' => [ + 'accepted_true_values' => ['true', '1', 'yes', 'on', true, 1], + 'accepted_false_values' => ['false', '0', 'no', 'off', false, 0], + 'examples' => [ + '/api/views?include_products=true', + '/api/views?include_gebruik=1&include_products=false', + '/api/views/view-123?include_deelnames_gebruik=yes' + ] + ] + ]; + + return new JSONResponse($documentation, 200); + } +} diff --git a/lib/Service/ArchiMateExportService.php b/lib/Service/ArchiMateExportService.php index c3018814..4397d79c 100644 --- a/lib/Service/ArchiMateExportService.php +++ b/lib/Service/ArchiMateExportService.php @@ -110,6 +110,16 @@ public function arrayToXml(array $data, \SimpleXMLElement $xml): \SimpleXMLEleme continue; } + // Skip property-like fields that should be handled by specialized property methods + // These fields often appear as direct data but should only be in structure + $propertyLikeFields = [ + 'beschikbaarheid', 'integriteit', 'vertrouwelijkheid', 'gemmaType', + 'objectId', 'bivScoreBbn', 'belangrijksteReden' + ]; + if (in_array($key, $propertyLikeFields, true)) { + continue; // Skip these - they should only appear in proper structure + } + // Ensure key is always a string for XML tag names $tagName = (string) $key; @@ -1007,8 +1017,8 @@ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?st } } } - // Add properties from root fields using propertyDefinitionMap - if (!empty($propertyDefinitionMap)) { + // Add properties from root fields using propertyDefinitionMap ONLY if no properties were already processed + if (!empty($propertyDefinitionMap) && !isset($data['properties'])) { $this->addPropertiesFromRootFields($node, $data, $propertyDefinitionMap); } } @@ -1092,11 +1102,16 @@ private function addPropertiesToXml(\SimpleXMLElement $node, array $properties): } } - // Also check in _attributes + // Also check in _attributes, but avoid duplicate if we already found one if (!$propDefRef && isset($property['_attributes']['propertyDefinitionRef'])) { $propDefRef = (string)$property['_attributes']['propertyDefinitionRef']; } + // Skip empty namespace prefix attributes (these cause :propertyDefinitionRef errors) + if (isset($property['_attributes'][':propertyDefinitionRef'])) { + unset($property['_attributes'][':propertyDefinitionRef']); + } + if ($propDefRef) { $propertyNode->addAttribute('propertyDefinitionRef', $propDefRef); } diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index 0070ed86..51158238 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -2063,16 +2063,14 @@ private function extractEssentialXmlData(array $item, array $elementsLookup = [] } /** - * Extract nodes and connections for view objects with full nested hierarchy and element splicing + * Extract view nodes and relationships for view objects with proper JSON structure * - * This method recursively extracts the complete nested node structure from view XML data. - * Each node can contain child nodes, creating a deep hierarchical structure that matches - * the original ArchiMate view exactly. Additionally, elements are spliced into nodes - * that reference them via elementRef. + * This method extracts and transforms nodes and connections from view XML data into + * the standardized viewNodes and viewRelationships format used by the frontend. * * @param array $item The complete XML item data - * @param array &$essential Essential XML data to add nodes/connections to (by reference) - * @param array $elementsLookup Optional lookup array of elements by identifier for splicing + * @param array &$essential Essential XML data to add viewNodes/viewRelationships to (by reference) + * @param array $elementsLookup Optional lookup array of elements by identifier for enrichment * @return void */ private function extractViewNodesAndConnections(array $item, array &$essential, array $elementsLookup = []): void @@ -2082,19 +2080,165 @@ private function extractViewNodesAndConnections(array $item, array &$essential, return; } - // Extract nodes array with full nested hierarchy and element splicing + // Extract viewNodes array with proper JSON structure if (isset($item['node'])) { - $essential['nodes'] = $this->extractNodesRecursively($item['node'], $elementsLookup); + $essential['viewNodes'] = $this->extractViewNodesRecursively($item['node'], $elementsLookup); } - // Extract connections array + // Extract viewRelationships array with proper JSON structure if (isset($item['connection'])) { - $essential['connections'] = $this->extractConnectionsRecursively($item['connection']); + $essential['viewRelationships'] = $this->extractViewRelationshipsRecursively($item['connection']); } } /** - * Recursively extract nested nodes with full hierarchy and element splicing + * Extract view nodes with proper JSON structure for frontend consumption + * + * This method transforms ArchiMate XML node data into the standardized viewNodes format + * expected by the frontend visualization components. + * + * @param array $nodeData Node data (can be single node or array of nodes) + * @param array $elementsLookup Lookup array of elements by identifier for enrichment + * @return array Array of viewNodes with standardized structure + */ + private function extractViewNodesRecursively($nodeData, array $elementsLookup = []): array + { + $viewNodes = []; + + // Handle both single node and array of nodes + if (!isset($nodeData[0])) { + // Single node + $nodeData = [$nodeData]; + } + + foreach ($nodeData as $node) { + if (!isset($node['_attributes'])) { + continue; + } + + $nodeId = $node['_attributes']['identifier'] ?? null; + $elementRef = $node['_attributes']['elementRef'] ?? null; + + if (!$nodeId) { + continue; + } + + // Create viewNode with standardized structure + $viewNode = [ + 'modelNodeId' => $elementRef, // Reference to the ArchiMate element + 'viewNodeId' => $nodeId, // Unique identifier within this view + 'x' => isset($node['_attributes']['x']) ? (int)$node['_attributes']['x'] : 0, + 'y' => isset($node['_attributes']['y']) ? (int)$node['_attributes']['y'] : 0, + 'width' => isset($node['_attributes']['w']) ? (int)$node['_attributes']['w'] : 100, + 'height' => isset($node['_attributes']['h']) ? (int)$node['_attributes']['h'] : 50, + 'parent' => null, // TODO: Handle parent relationships for nested nodes + 'name' => null, + 'type' => null, + 'color' => 'rgb(255, 255, 255)', // Default white background + 'borderColor' => 'rgb(0, 0, 0)', // Default black border + 'font' => [ + 'name' => 'Segoe UI, Arial', + 'size' => 12, + 'style' => 'normal', + 'color' => 'rgb(0, 0, 0)' + ], + 'description' => null, + 'elementRef' => $elementRef + ]; + + // Extract style information if present + if (isset($node['style'])) { + $this->applyNodeStyle($viewNode, $node['style']); + } + + // Extract label text for Label type nodes + if (isset($node['label'])) { + if (is_array($node['label']) && isset($node['label']['_value'])) { + $viewNode['name'] = $node['label']['_value']; + } elseif (is_string($node['label'])) { + $viewNode['name'] = $node['label']; + } + } + + // Enrich with element data if available and elementRef exists + if ($elementRef && isset($elementsLookup[$elementRef])) { + $element = $elementsLookup[$elementRef]; + + // Use element name if node doesn't have its own label + if ($viewNode['name'] === null && isset($element['name'])) { + $viewNode['name'] = $element['name']; + } + + // Set description from element documentation + if (isset($element['summary'])) { + $viewNode['description'] = $element['summary']; + } elseif (isset($element['documentation'])) { + $viewNode['description'] = $element['documentation']; + } + + // Extract element type from ArchiMate type or GEMMA type + if (isset($element['gemmaType'])) { + $gemmaType = $element['gemmaType']; + // Handle case where gemmaType might be an array + if (is_array($gemmaType)) { + $gemmaType = $gemmaType['_value'] ?? $gemmaType[0] ?? 'unknown'; + } + $viewNode['type'] = strtolower((string)$gemmaType); + } elseif (isset($element['xml']['_attributes']['xsi:type'])) { + $archiType = $element['xml']['_attributes']['xsi:type']; + // Convert ArchiMate type to simplified type (e.g., "archimate:BusinessService" -> "businessservice") + $viewNode['type'] = strtolower(preg_replace('/^archimate:|^[a-z]+:/', '', $archiType)); + } + + // Add all element properties to view node for full data access + $viewNode['elementProperties'] = $this->extractElementProperties($element); + + // Add specific useful properties directly to the node + if (isset($element['objectId'])) { + $viewNode['objectId'] = $element['objectId']; + } + + // Add GEMMA-specific properties if they exist + $gemmaProperties = ['gemmaType', 'bivScoreBbn', 'belangrijksteReden', 'beschikbaarheid', 'integriteit', 'vertrouwelijkheid']; + foreach ($gemmaProperties as $prop) { + if (isset($element[$prop])) { + $viewNode[$prop] = $element[$prop]; + } + } + + // Store element section for reference + if (isset($element['section'])) { + $viewNode['elementSection'] = $element['section']; + } + + // Store model identifier for linking + if (isset($element['model_identifier'])) { + $viewNode['modelIdentifier'] = $element['model_identifier']; + } + } + + // Handle child nodes recursively (flatten hierarchy into single array) + if (isset($node['node'])) { + $childNodes = $this->extractViewNodesRecursively($node['node'], $elementsLookup); + + // Set parent reference for child nodes + foreach ($childNodes as &$childNode) { + $childNode['parent'] = $nodeId; + } + unset($childNode); + + // Add child nodes to the main array + $viewNodes = array_merge($viewNodes, $childNodes); + } + + $viewNodes[] = $viewNode; + } + + return $viewNodes; + } + + /** + * Recursively extract nested nodes with full hierarchy and element splicing (LEGACY - for backward compatibility) * * This method processes nodes and their children recursively to capture the complete * nested structure as it appears in the ArchiMate XML. When a node references an element @@ -2228,7 +2372,237 @@ private function extractConnectionsRecursively($connectionData): array } /** - * Extract style information from a node + * Extract all properties from an element for view node enrichment + * + * @param array $element Element data containing properties + * @return array Clean array of element properties + */ + private function extractElementProperties(array $element): array + { + $properties = []; + + // Extract basic element properties + $basicProperties = ['identifier', 'name', 'summary', 'section', 'model_identifier']; + foreach ($basicProperties as $prop) { + if (isset($element[$prop])) { + $properties[$prop] = $element[$prop]; + } + } + + // Extract all flattened properties (camelCase converted properties) + $excludedKeys = ['@self', 'xml', '_propertyMapping', '_slug', '_essential_data']; + foreach ($element as $key => $value) { + if (!in_array($key, $excludedKeys) && !in_array($key, $basicProperties)) { + // Only include non-object values or simple arrays + if (is_scalar($value) || (is_array($value) && !$this->isComplexArray($value))) { + $properties[$key] = $value; + } + } + } + + // Add property mapping for reference if available + if (isset($element['_propertyMapping'])) { + $properties['_propertyMapping'] = $element['_propertyMapping']; + } + + return $properties; + } + + /** + * Check if an array contains complex nested structures + * + * @param array $array Array to check + * @return bool True if array contains complex nested structures + */ + private function isComplexArray(array $array): bool + { + foreach ($array as $value) { + if (is_array($value) || is_object($value)) { + return true; + } + } + return false; + } + + /** + * Apply style information to a viewNode structure + * + * @param array &$viewNode ViewNode structure to apply styles to (by reference) + * @param array $style Style data from XML + * @return void + */ + private function applyNodeStyle(array &$viewNode, array $style): void + { + // Extract fillColor + if (isset($style['fillColor']['_attributes'])) { + $fillColor = $style['fillColor']['_attributes']; + $r = isset($fillColor['r']) ? (int)$fillColor['r'] : 255; + $g = isset($fillColor['g']) ? (int)$fillColor['g'] : 255; + $b = isset($fillColor['b']) ? (int)$fillColor['b'] : 255; + $viewNode['color'] = "rgb($r, $g, $b)"; + } + + // Extract lineColor + if (isset($style['lineColor']['_attributes'])) { + $lineColor = $style['lineColor']['_attributes']; + $r = isset($lineColor['r']) ? (int)$lineColor['r'] : 0; + $g = isset($lineColor['g']) ? (int)$lineColor['g'] : 0; + $b = isset($lineColor['b']) ? (int)$lineColor['b'] : 0; + $viewNode['borderColor'] = "rgb($r, $g, $b)"; + } + + // Extract font information + if (isset($style['font'])) { + $font = []; + if (isset($style['font']['_attributes'])) { + $font['name'] = $style['font']['_attributes']['name'] ?? 'Segoe UI, Arial'; + $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12; + $font['style'] = 'normal'; + } + + if (isset($style['font']['color']['_attributes'])) { + $fontColor = $style['font']['color']['_attributes']; + $r = isset($fontColor['r']) ? (int)$fontColor['r'] : 0; + $g = isset($fontColor['g']) ? (int)$fontColor['g'] : 0; + $b = isset($fontColor['b']) ? (int)$fontColor['b'] : 0; + $font['color'] = "rgb($r, $g, $b)"; + } + + if (!empty($font)) { + $viewNode['font'] = array_merge($viewNode['font'], $font); + } + } + } + + /** + * Extract view relationships with proper JSON structure for frontend consumption + * + * This method transforms ArchiMate XML connection data into the standardized viewRelationships + * format expected by the frontend visualization components. + * + * @param array $connectionData Connection data (can be single connection or array) + * @return array Array of viewRelationships with standardized structure + */ + private function extractViewRelationshipsRecursively($connectionData): array + { + $viewRelationships = []; + + // Handle both single connection and array of connections + if (!isset($connectionData[0])) { + // Single connection + $connectionData = [$connectionData]; + } + + foreach ($connectionData as $connection) { + if (!isset($connection['_attributes'])) { + continue; + } + + $connectionId = $connection['_attributes']['identifier'] ?? null; + $relationshipRef = $connection['_attributes']['relationshipRef'] ?? null; + $source = $connection['_attributes']['source'] ?? null; + $target = $connection['_attributes']['target'] ?? null; + + if (!$connectionId || !$source || !$target) { + continue; + } + + // Create viewRelationship with standardized structure + $viewRelationship = [ + 'modelRelationshipId' => $relationshipRef, // Reference to the ArchiMate relationship + 'sourceId' => $source, // Source node viewNodeId + 'targetId' => $target, // Target node viewNodeId + 'viewRelationshipId' => $connectionId, // Unique identifier within this view + 'type' => 'association', // Default relationship type + 'bendpoints' => [], // Array of bend points + 'label' => [] // Label information + ]; + + // Extract relationship type from ArchiMate type if available + if (isset($connection['_attributes']['xsi:type'])) { + $archiType = $connection['_attributes']['xsi:type']; + // Convert ArchiMate type to simplified type (e.g., "archimate:ServingRelationship" -> "serving") + $viewRelationship['type'] = strtolower(preg_replace('/^archimate:|relationship$|^[a-z]+:/', '', $archiType)); + } + + // Extract bend points if present + if (isset($connection['bendpoint'])) { + $bendpoints = isset($connection['bendpoint'][0]) ? $connection['bendpoint'] : [$connection['bendpoint']]; + + foreach ($bendpoints as $bendpoint) { + if (isset($bendpoint['_attributes'])) { + $viewRelationship['bendpoints'][] = [ + 'x' => isset($bendpoint['_attributes']['x']) ? (float)$bendpoint['_attributes']['x'] : 0, + 'y' => isset($bendpoint['_attributes']['y']) ? (float)$bendpoint['_attributes']['y'] : 0 + ]; + } + } + } + + // Extract label information if present + if (isset($connection['label'])) { + $label = []; + + if (is_array($connection['label']) && isset($connection['label']['_value'])) { + $label['text'] = $connection['label']['_value']; + } elseif (is_string($connection['label'])) { + $label['text'] = $connection['label']; + } + + // Extract label markup/style if present + if (isset($connection['style'])) { + $label['markup'] = [$this->extractLabelMarkup($connection['style'])]; + } + + $viewRelationship['label'] = $label; + } + + $viewRelationships[] = $viewRelationship; + } + + return $viewRelationships; + } + + /** + * Extract label markup information from connection style + * + * @param array $style Style data from XML + * @return array Label markup structure + */ + private function extractLabelMarkup(array $style): array + { + $markup = [ + 'style' => [ + 'fontSize' => 11, + 'fontFamily' => 'Segoe UI, Arial', + 'fontColor' => 'rgba(0, 0, 0, 1)', + 'fontStyle' => 'normal', + 'fontWeight' => 'normal' + ] + ]; + + // Extract font information if present + if (isset($style['font'])) { + if (isset($style['font']['_attributes'])) { + $markup['style']['fontFamily'] = $style['font']['_attributes']['name'] ?? 'Segoe UI, Arial'; + $markup['style']['fontSize'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 11; + } + + if (isset($style['font']['color']['_attributes'])) { + $fontColor = $style['font']['color']['_attributes']; + $r = isset($fontColor['r']) ? (int)$fontColor['r'] : 0; + $g = isset($fontColor['g']) ? (int)$fontColor['g'] : 0; + $b = isset($fontColor['b']) ? (int)$fontColor['b'] : 0; + $a = isset($fontColor['a']) ? ((int)$fontColor['a'] / 100) : 1; // Convert percentage to decimal + $markup['style']['fontColor'] = "rgba($r, $g, $b, $a)"; + } + } + + return $markup; + } + + /** + * Extract style information from a node (LEGACY - for backward compatibility) * * @param array $style Style data from XML * @return array Processed style information @@ -2819,12 +3193,12 @@ private function transformViewsOptimized( } } - // Copy nodes and connections from XML to root level for easy access - if (isset($object['xml']['nodes'])) { - $object['nodes'] = $object['xml']['nodes']; + // Copy viewNodes and viewRelationships from XML to root level for easy access + if (isset($object['xml']['viewNodes'])) { + $object['viewNodes'] = $object['xml']['viewNodes']; } - if (isset($object['xml']['connections'])) { - $object['connections'] = $object['xml']['connections']; + if (isset($object['xml']['viewRelationships'])) { + $object['viewRelationships'] = $object['xml']['viewRelationships']; } $objects[] = $object; @@ -3143,13 +3517,13 @@ private function transformSectionObjectsBatch( } } - // NEW: For view objects, copy nodes and connections from XML to root level + // NEW: For view objects, copy viewNodes and viewRelationships from XML to root level if ($schemaType === 'view' && isset($object['xml'])) { - if (isset($object['xml']['nodes'])) { - $object['nodes'] = $object['xml']['nodes']; + if (isset($object['xml']['viewNodes'])) { + $object['viewNodes'] = $object['xml']['viewNodes']; } - if (isset($object['xml']['connections'])) { - $object['connections'] = $object['xml']['connections']; + if (isset($object['xml']['viewRelationships'])) { + $object['viewRelationships'] = $object['xml']['viewRelationships']; } } @@ -3162,9 +3536,9 @@ private function transformSectionObjectsBatch( 'xml_keys' => isset($object['xml']) ? array_keys($object['xml']) : null, 'has_property_mapping' => isset($object['_propertyMapping']), 'property_mapping_count' => isset($object['_propertyMapping']) ? count($object['_propertyMapping']) : 0, - 'nodes_count' => isset($object['nodes']) ? count($object['nodes']) : 0, - 'connections_count' => isset($object['connections']) ? count($object['connections']) : 0, - 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'nodes', 'connections']), 0, 5) + 'viewNodes_count' => isset($object['viewNodes']) ? count($object['viewNodes']) : 0, + 'viewRelationships_count' => isset($object['viewRelationships']) ? count($object['viewRelationships']) : 0, + 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'viewNodes', 'viewRelationships']), 0, 5) ]); $objects[] = $object; @@ -3583,11 +3957,11 @@ private function bulkTransformViews( } // SPEED OPTIMIZATION: Direct copy without checks (we know it exists) - if (isset($object['xml']['nodes'])) { - $object['nodes'] = $object['xml']['nodes']; + if (isset($object['xml']['viewNodes'])) { + $object['viewNodes'] = $object['xml']['viewNodes']; } - if (isset($object['xml']['connections'])) { - $object['connections'] = $object['xml']['connections']; + if (isset($object['xml']['viewRelationships'])) { + $object['viewRelationships'] = $object['xml']['viewRelationships']; } $objects[] = $object; diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php new file mode 100644 index 00000000..1ed7438c --- /dev/null +++ b/lib/Service/ViewService.php @@ -0,0 +1,1043 @@ +logger->info('Getting all views', [ + 'options' => $options + ]); + + try { + // Get all view objects from OpenRegister + $views = $this->getViewsFromRegister(); + + // Apply enrichments based on options + if (!empty($options)) { + $views = $this->enrichViews($views, $options); + } + + // Apply module-to-viewNode expansion if modules are enabled + if (isset($options['include_modules']) && $options['include_modules']) { + $views = $this->expandModulesToViewNodes($views, $options); + } + + return [ + 'success' => true, + 'views' => $views, + 'count' => count($views), + 'enrichments_applied' => $this->getAppliedEnrichments($options) + ]; + + } catch (\Exception $e) { + $this->logger->error('Failed to get all views', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'views' => [], + 'count' => 0 + ]; + } + } + + /** + * Get a specific view by ID + * + * @param string $viewId The view identifier + * @param array $options Query options including enrichment flags + * @return array View object with optional enrichments or error response + */ + public function getView(string $viewId, array $options = []): array + { + $this->logger->info('Getting specific view', [ + 'view_id' => $viewId, + 'options' => $options + ]); + + try { + // Get specific view from OpenRegister + $view = $this->getViewFromRegister($viewId); + + if (!$view) { + return [ + 'success' => false, + 'error' => "View with ID '$viewId' not found", + 'view' => null + ]; + } + + // Apply enrichments based on options + if (!empty($options)) { + $view = $this->enrichView($view, $options); + + // Apply module-to-viewNode expansion if modules are enabled + if (isset($options['include_modules']) && $options['include_modules']) { + $views = $this->expandModulesToViewNodes([$view], $options); + $view = $views[0] ?? $view; // Get the expanded view back + } + } + + return [ + 'success' => true, + 'view' => $view, + 'enrichments_applied' => $this->getAppliedEnrichments($options) + ]; + + } catch (\Exception $e) { + $this->logger->error('Failed to get view', [ + 'view_id' => $viewId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'view' => null + ]; + } + } + + /** + * Get views from OpenRegister system + * + * @return array Array of view objects + */ + private function getViewsFromRegister(): array + { + $objectService = $this->getObjectService(); + if (!$objectService) { + throw new \RuntimeException('OpenRegister ObjectService not available'); + } + + // Get AMEF configuration for view schema + $amefConfig = $this->settingsService->getAmefConfig(); + $registerId = $amefConfig['register_id'] ?? $amefConfig['register'] ?? null; + $viewSchemaId = $amefConfig['view_schema'] ?? null; + + if (!$registerId || !$viewSchemaId) { + throw new \RuntimeException('AMEF configuration not found for views'); + } + + // Query for all view objects + $query = [ + '@self' => [ + 'register' => $registerId, + 'schema' => $viewSchemaId + ], + 'section' => 'view' + ]; + + $views = $objectService->searchObjects($query); + + $this->logger->debug('Retrieved views from register', [ + 'register_id' => $registerId, + 'view_schema_id' => $viewSchemaId, + 'views_count' => count($views) + ]); + + return $views; + } + + /** + * Get a specific view from OpenRegister system + * + * @param string $viewId The view identifier + * @return array|null View object or null if not found + */ + private function getViewFromRegister(string $viewId): ?array + { + $objectService = $this->getObjectService(); + if (!$objectService) { + throw new \RuntimeException('OpenRegister ObjectService not available'); + } + + // Get AMEF configuration for view schema + $amefConfig = $this->settingsService->getAmefConfig(); + $registerId = $amefConfig['register_id'] ?? $amefConfig['register'] ?? null; + $viewSchemaId = $amefConfig['view_schema'] ?? null; + + if (!$registerId || !$viewSchemaId) { + throw new \RuntimeException('AMEF configuration not found for views'); + } + + try { + // Get specific view object by ID + $view = $objectService->getObject($registerId, $viewSchemaId, $viewId); + + $this->logger->debug('Retrieved specific view from register', [ + 'view_id' => $viewId, + 'register_id' => $registerId, + 'view_schema_id' => $viewSchemaId, + 'found' => $view !== null + ]); + + return $view; + + } catch (\Exception $e) { + $this->logger->warning('View not found in register', [ + 'view_id' => $viewId, + 'error' => $e->getMessage() + ]); + return null; + } + } + + /** + * Enrich multiple views with additional data + * + * @param array $views Array of view objects + * @param array $options Enrichment options + * @return array Array of enriched view objects + */ + private function enrichViews(array $views, array $options): array + { + $enrichedViews = []; + + foreach ($views as $view) { + $enrichedViews[] = $this->enrichView($view, $options); + } + + return $enrichedViews; + } + + /** + * Enrich a single view with additional data + * + * @param array $view View object to enrich + * @param array $options Enrichment options + * @return array Enriched view object + */ + private function enrichView(array $view, array $options): array + { + $enrichedView = $view; + + // Enrich viewNodes if present + if (isset($view['viewNodes']) && is_array($view['viewNodes'])) { + $enrichedView['viewNodes'] = $this->enrichViewNodes($view['viewNodes'], $options); + } + + // TODO: Add view-level enrichments here if needed + + return $enrichedView; + } + + /** + * Enrich view nodes with additional data based on options + * + * @param array $viewNodes Array of view nodes + * @param array $options Enrichment options + * @return array Array of enriched view nodes + */ + private function enrichViewNodes(array $viewNodes, array $options): array + { + $enrichedNodes = []; + + // Get enrichment data upfront if requested + $productsData = []; + $modulesData = []; + $gebruikData = []; + $deelnamesGebruikData = []; + + if ($this->shouldIncludeProducts($options)) { + $productsData = $this->getProductsData(); + } + + if ($this->shouldIncludeModules($options)) { + $modulesData = $this->getModulesData(); + } + + if ($this->shouldIncludeGebruik($options)) { + $gebruikData = $this->getGebruikData($options); + } + + if ($this->shouldIncludeDeelnamesGebruik($options)) { + $deelnamesGebruikData = $this->getDeelnamesGebruikData(); + } + + foreach ($viewNodes as $node) { + $enrichedNode = $node; + + // Apply enrichments based on the node's modelNodeId (element reference) + $modelNodeId = $node['modelNodeId'] ?? null; + + if ($modelNodeId) { + if ($this->shouldIncludeProducts($options)) { + $enrichedNode['products'] = $this->getNodeProducts($modelNodeId, $productsData); + } + + if ($this->shouldIncludeModules($options)) { + $enrichedNode['modules'] = $this->getNodeModules($modelNodeId, $modulesData); + } + + if ($this->shouldIncludeGebruik($options)) { + $enrichedNode['gebruik'] = $this->getNodeGebruik($modelNodeId, $gebruikData); + } + + if ($this->shouldIncludeDeelnamesGebruik($options)) { + $enrichedNode['deelnamesGebruik'] = $this->getNodeDeelnamesGebruik($modelNodeId, $deelnamesGebruikData); + } + } + + $enrichedNodes[] = $enrichedNode; + } + + return $enrichedNodes; + } + + /** + * Check if products should be included in enrichment + * + * @param array $options Enrichment options + * @return bool True if products should be included + */ + private function shouldIncludeProducts(array $options): bool + { + return isset($options['include_products']) && $options['include_products'] === true; + } + + /** + * Check if modules should be included in enrichment + * + * @param array $options Enrichment options + * @return bool True if modules should be included + */ + private function shouldIncludeModules(array $options): bool + { + return isset($options['include_modules']) && $options['include_modules'] === true; + } + + /** + * Check if gebruik should be included in enrichment + * + * @param array $options Enrichment options + * @return bool True if gebruik should be included + */ + private function shouldIncludeGebruik(array $options): bool + { + return isset($options['include_gebruik']) && $options['include_gebruik'] === true; + } + + /** + * Check if deelnames gebruik should be included in enrichment + * + * @param array $options Enrichment options + * @return bool True if deelnames gebruik should be included + */ + private function shouldIncludeDeelnamesGebruik(array $options): bool + { + return isset($options['include_deelnames_gebruik']) && $options['include_deelnames_gebruik'] === true; + } + + /** + * Get applied enrichments list for response metadata + * + * @param array $options Enrichment options + * @return array List of applied enrichments + */ + private function getAppliedEnrichments(array $options): array + { + $enrichments = []; + + if ($this->shouldIncludeProducts($options)) { + $enrichments[] = 'products'; + } + + if ($this->shouldIncludeModules($options)) { + $enrichments[] = 'modules'; + } + + if ($this->shouldIncludeGebruik($options)) { + $enrichments[] = 'gebruik'; + } + + if ($this->shouldIncludeDeelnamesGebruik($options)) { + $enrichments[] = 'deelnames_gebruik'; + } + + return $enrichments; + } + + /** + * Get products data for enrichment (placeholder implementation) + * + * @return array Products data + */ + private function getProductsData(): array + { + // TODO: Implement actual products data retrieval + $this->logger->debug('Getting products data for enrichment'); + return []; + } + + /** + * Get current active organisation for filtering + * + * @return string|null Current organisation identifier + */ + private function getCurrentOrganisation(): ?string + { + $user = $this->userSession->getUser(); + if (!$user) { + return null; + } + + // TODO: Implement proper organisation determination logic + // This could be based on user groups, settings, or other mechanisms + // For now, return a placeholder that would need actual implementation + + $userId = $user->getUID(); + $this->logger->debug('Getting current organisation for user', [ + 'user_id' => $userId + ]); + + // Placeholder - needs actual implementation based on your organisation structure + return 'default'; // Replace with actual organisation logic + } + + /** + * Get modules data for enrichment based on elementRef linkage + * + * Modules are directly linked to nodes based on their elementRef. + * This method retrieves all module data from OpenRegister, filtered by current organisation. + * + * @return array Modules data indexed by elementRef + */ + private function getModulesData(): array + { + $this->logger->debug('Getting modules data for enrichment'); + + try { + $objectService = $this->getObjectService(); + if (!$objectService) { + return []; + } + + // Get current organisation for filtering + $currentOrg = $this->getCurrentOrganisation(); + + // Get configuration for modules register/schema + $amefConfig = $this->settingsService->getAmefConfig(); + $registerId = $amefConfig['register_id'] ?? null; + + // Modules could be in various schemas - check common ones + $moduleSchemas = [ + $amefConfig['module_schema'] ?? null, + $amefConfig['component_schema'] ?? null, + $amefConfig['element_schema'] ?? null + ]; + + $allModules = []; + + foreach ($moduleSchemas as $schemaId) { + if (!$schemaId) continue; + + try { + $query = [ + '@self' => [ + 'register' => $registerId, + 'schema' => $schemaId + ] + ]; + + // Add organisation filter if current organisation is available + if ($currentOrg) { + $query['@self']['organisation'] = $currentOrg; + } + + $modules = $objectService->searchObjects($query); + + foreach ($modules as $module) { + // Additional check for organisation in metadata if not caught by query + if ($currentOrg && isset($module['@self']['organisation']) && $module['@self']['organisation'] !== $currentOrg) { + continue; + } + + // Index by elementRef or identifier for quick lookup + $elementRef = $module['elementRef'] ?? $module['identifier'] ?? null; + if ($elementRef) { + $allModules[$elementRef] = $module; + } + } + + $this->logger->debug('Retrieved modules from schema', [ + 'schema_id' => $schemaId, + 'modules_count' => count($modules) + ]); + + } catch (\Exception $e) { + $this->logger->warning('Failed to get modules from schema', [ + 'schema_id' => $schemaId, + 'error' => $e->getMessage() + ]); + } + } + + $this->logger->debug('Total modules retrieved for enrichment', [ + 'total_modules' => count($allModules) + ]); + + return $allModules; + + } catch (\Exception $e) { + $this->logger->error('Failed to get modules data', [ + 'error' => $e->getMessage() + ]); + return []; + } + } + + /** + * Get gebruik data for enrichment based on elementRef linkage + * + * Usage data is linked to nodes based on their elementRef. + * This method retrieves usage statistics from OpenRegister, filtered by organisation. + * Also handles deelnames logic when enabled. + * + * @param array $options Query options for deelnames handling + * @return array Gebruik data indexed by elementRef with extended modules + */ + private function getGebruikData(array $options = []): array + { + $this->logger->debug('Getting gebruik data for enrichment with options', ['options' => array_keys($options)]); + + try { + $objectService = $this->getObjectService(); + if (!$objectService) { + return []; + } + + // Get current organisation for filtering + $currentOrg = $this->getCurrentOrganisation(); + + // Get configuration for usage register/schema + $amefConfig = $this->settingsService->getAmefConfig(); + $registerId = $amefConfig['register_id'] ?? null; + + // Usage data could be in various schemas + $gebruikSchemas = [ + $amefConfig['gebruik_schema'] ?? null, + $amefConfig['usage_schema'] ?? null, + $amefConfig['statistics_schema'] ?? null + ]; + + $allGebruik = []; + + // STEP 1: Get regular gebruik data filtered by current organisation + foreach ($gebruikSchemas as $schemaId) { + if (!$schemaId) continue; + + try { + $query = [ + '@self' => [ + 'register' => $registerId, + 'schema' => $schemaId + ] + ]; + + // Add organisation filter for regular gebruik + if ($currentOrg) { + $query['@self']['organisation'] = $currentOrg; + } + + $gebruikItems = $objectService->searchObjects($query); + $this->processGebruikItems($gebruikItems, $allGebruik, $currentOrg, 'regular'); + + $this->logger->debug('Retrieved regular gebruik from schema', [ + 'schema_id' => $schemaId, + 'gebruik_count' => count($gebruikItems), + 'organisation' => $currentOrg + ]); + + } catch (\Exception $e) { + $this->logger->warning('Failed to get regular gebruik from schema', [ + 'schema_id' => $schemaId, + 'error' => $e->getMessage() + ]); + } + } + + // STEP 2: If deelnames is enabled, get additional gebruik with RBAC off + if (isset($options['include_deelnames_gebruik']) && $options['include_deelnames_gebruik'] && $currentOrg) { + $this->logger->debug('Processing deelnames gebruik with RBAC disabled'); + + foreach ($gebruikSchemas as $schemaId) { + if (!$schemaId) continue; + + try { + // Search with RBAC disabled to find gebruik where current org is in deelnemers + $query = [ + '@self' => [ + 'register' => $registerId, + 'schema' => $schemaId + ], + 'deelnemers' => $currentOrg // Current org in deelnemers field + ]; + + // Call with RBAC disabled + $deelnamesGebruikItems = $objectService->searchObjects($query, rbac: false); + $this->processGebruikItems($deelnamesGebruikItems, $allGebruik, $currentOrg, 'deelnames'); + + $this->logger->debug('Retrieved deelnames gebruik from schema', [ + 'schema_id' => $schemaId, + 'deelnames_gebruik_count' => count($deelnamesGebruikItems), + 'organisation_in_deelnemers' => $currentOrg + ]); + + } catch (\Exception $e) { + $this->logger->warning('Failed to get deelnames gebruik from schema', [ + 'schema_id' => $schemaId, + 'error' => $e->getMessage() + ]); + } + } + } + + // STEP 3: Extend gebruik with modules data + $allGebruik = $this->extendGebruikWithModules($allGebruik); + + $this->logger->debug('Total gebruik retrieved and processed', [ + 'total_element_refs_with_gebruik' => count($allGebruik), + 'current_organisation' => $currentOrg, + 'deelnames_enabled' => isset($options['include_deelnames_gebruik']) && $options['include_deelnames_gebruik'] + ]); + + return $allGebruik; + + } catch (\Exception $e) { + $this->logger->error('Failed to get gebruik data', [ + 'error' => $e->getMessage() + ]); + return []; + } + } + + /** + * Process gebruik items and group them by elementRef + * + * @param array $gebruikItems Array of gebruik objects + * @param array &$allGebruik Array to add items to (by reference) + * @param string $type Type identifier ('regular' or 'deelnames') + * @return void + */ + private function processGebruikItems(array $gebruikItems, array &$allGebruik, string $currentOrg, string $type): void + { + foreach ($gebruikItems as $gebruik) { + // Group by elementRef for quick lookup + $elementRef = $gebruik['elementRef'] ?? $gebruik['componentRef'] ?? $gebruik['moduleRef'] ?? null; + + if ($elementRef) { + if (!isset($allGebruik[$elementRef])) { + $allGebruik[$elementRef] = []; + } + + // Add type indicator and processing info + $gebruik['_type'] = $type; + $gebruik['_processed_for_org'] = $currentOrg; + + $allGebruik[$elementRef][] = $gebruik; + } + } + } + + /** + * Extend gebruik data with modules information + * + * @param array $allGebruik Gebruik data indexed by elementRef + * @return array Extended gebruik data with modules + */ + private function extendGebruikWithModules(array $allGebruik): array + { + $this->logger->debug('Extending gebruik data with modules information'); + + // Get modules data for extension + $modulesData = $this->getModulesData(); + + foreach ($allGebruik as $elementRef => &$gebruikList) { + // Check if we have a module for this elementRef + if (isset($modulesData[$elementRef])) { + $module = $modulesData[$elementRef]; + + // Add module data to each gebruik item for this elementRef + foreach ($gebruikList as &$gebruik) { + $gebruik['_linked_module'] = $module; + + $this->logger->debug('Linked module to gebruik', [ + 'element_ref' => $elementRef, + 'gebruik_id' => $gebruik['id'] ?? $gebruik['identifier'] ?? 'unknown', + 'module_id' => $module['id'] ?? $module['identifier'] ?? 'unknown', + 'module_name' => $module['name'] ?? 'unnamed' + ]); + } + unset($gebruik); // Clean up reference + } + } + unset($gebruikList); // Clean up reference + + return $allGebruik; + } + + /** + * Expand modules to viewNodes based on referentiecomponent relationships + * + * For each module that has referentiecomponent relationships, add new nodes + * to viewNodes with the module as parent. + * + * @param array $views Array of views to process + * @return array Views with expanded module nodes + */ + private function expandModulesToViewNodes(array $views): array + { + $this->logger->debug('Expanding modules to viewNodes'); + + // Get modules data for expansion + $modulesData = $this->getModulesData(); + + foreach ($views as &$view) { + if (!isset($view['viewNodes']) || !is_array($view['viewNodes'])) { + continue; + } + + $originalNodes = $view['viewNodes']; + $expandedNodes = $originalNodes; + + // Create a lookup of existing nodes by modelNodeId for quick parent matching + $nodesByModelId = []; + foreach ($originalNodes as $node) { + $modelNodeId = $node['modelNodeId'] ?? null; + if ($modelNodeId) { + $nodesByModelId[$modelNodeId] = $node; + } + } + + // Process each module for expansion + foreach ($modulesData as $elementRef => $module) { + $expandedNodes = $this->expandModuleToNodes($module, $expandedNodes, $nodesByModelId); + } + + // Update the view with expanded nodes + $view['viewNodes'] = $expandedNodes; + + $addedNodesCount = count($expandedNodes) - count($originalNodes); + if ($addedNodesCount > 0) { + $this->logger->debug('Added module nodes to view', [ + 'view_id' => $view['id'] ?? 'unknown', + 'original_nodes' => count($originalNodes), + 'expanded_nodes' => count($expandedNodes), + 'added_nodes' => $addedNodesCount + ]); + } + } + unset($view); // Clean up reference + + return $views; + } + + /** + * Expand a single module to nodes based on its referentiecomponent relationships + * + * @param array $module Module data + * @param array $existingNodes Current view nodes + * @param array $nodesByModelId Lookup of nodes by modelNodeId + * @return array Updated nodes array with module expansions + */ + private function expandModuleToNodes(array $module, array $existingNodes, array $nodesByModelId): array + { + $expandedNodes = $existingNodes; + + // Look for referentiecomponent relationships in the module + $referentieComponenten = $this->extractReferentieComponenten($module); + + foreach ($referentieComponenten as $referentieComponentId) { + // Find if there's an existing node for this referentiecomponent + $parentNode = $nodesByModelId[$referentieComponentId] ?? null; + + if ($parentNode) { + // Create a new node for this module as child of the referentiecomponent + $moduleNode = $this->createModuleNode($module, $parentNode, $referentieComponentId); + $expandedNodes[] = $moduleNode; + + $this->logger->debug('Created module node', [ + 'module_id' => $module['id'] ?? $module['identifier'] ?? 'unknown', + 'module_name' => $module['name'] ?? 'unnamed', + 'parent_node_id' => $parentNode['viewNodeId'] ?? 'unknown', + 'referentie_component_id' => $referentieComponentId + ]); + } + } + + return $expandedNodes; + } + + /** + * Extract referentiecomponent IDs from module data + * + * @param array $module Module data + * @return array Array of referentiecomponent identifiers + */ + private function extractReferentieComponenten(array $module): array + { + $referentieComponenten = []; + + // Look for referentiecomponent relationships in various possible locations + $possibleFields = [ + 'referentiecomponenten', + 'referentieComponenten', + 'relatedComponents', + 'components', + 'relationships' + ]; + + foreach ($possibleFields as $field) { + if (isset($module[$field])) { + $value = $module[$field]; + + if (is_array($value)) { + // Handle array of components + foreach ($value as $component) { + if (is_string($component)) { + $referentieComponenten[] = $component; + } elseif (is_array($component) && isset($component['id'])) { + $referentieComponenten[] = $component['id']; + } elseif (is_array($component) && isset($component['identifier'])) { + $referentieComponenten[] = $component['identifier']; + } + } + } elseif (is_string($value)) { + // Handle single component ID + $referentieComponenten[] = $value; + } + } + } + + return array_unique($referentieComponenten); + } + + /** + * Create a module node as child of a referentiecomponent node + * + * @param array $module Module data + * @param array $parentNode Parent referentiecomponent node + * @param string $referentieComponentId Referentiecomponent identifier + * @return array New module node + */ + private function createModuleNode(array $module, array $parentNode, string $referentieComponentId): array + { + $moduleId = $module['id'] ?? $module['identifier'] ?? uniqid('module_'); + $moduleName = $module['name'] ?? 'Unnamed Module'; + + // Position the module node relative to parent (slightly offset) + $parentX = $parentNode['x'] ?? 0; + $parentY = $parentNode['y'] ?? 0; + $parentWidth = $parentNode['width'] ?? 100; + + return [ + 'modelNodeId' => $moduleId, + 'viewNodeId' => 'module-' . $moduleId . '-' . uniqid(), + 'x' => $parentX + $parentWidth + 20, // Position to the right of parent + 'y' => $parentY, + 'width' => 150, + 'height' => 50, + 'parent' => $parentNode['viewNodeId'] ?? null, + 'name' => $moduleName, + 'type' => 'module', + 'color' => 'rgb(200, 255, 200)', // Light green for modules + 'borderColor' => 'rgb(0, 150, 0)', + 'font' => [ + 'name' => 'Segoe UI, Arial', + 'size' => 11, + 'style' => 'normal', + 'color' => 'rgb(0, 0, 0)' + ], + 'description' => $module['description'] ?? $module['summary'] ?? null, + 'elementRef' => $moduleId, + '_isModuleExpansion' => true, + '_parentReferentieComponent' => $referentieComponentId, + '_moduleData' => $module + ]; + } + + /** + * Get deelnames gebruik data for enrichment (placeholder implementation) + * + * @return array Deelnames gebruik data + */ + private function getDeelnamesGebruikData(): array + { + // TODO: Implement actual deelnames gebruik data retrieval + $this->logger->debug('Getting deelnames gebruik data for enrichment'); + return []; + } + + /** + * Get products for a specific node (placeholder implementation) + * + * @param string $modelNodeId The model node identifier + * @param array $productsData Products data to search in + * @return array Products related to the node + */ + private function getNodeProducts(string $modelNodeId, array $productsData): array + { + // TODO: Implement actual node products matching logic + $this->logger->debug('Getting products for node', ['model_node_id' => $modelNodeId]); + return []; + } + + /** + * Get modules for a specific node based on elementRef linkage + * + * @param string $modelNodeId The model node identifier (elementRef) + * @param array $modulesData Modules data indexed by elementRef + * @return array Modules related to the node + */ + private function getNodeModules(string $modelNodeId, array $modulesData): array + { + $this->logger->debug('Getting modules for node', ['model_node_id' => $modelNodeId]); + + // Direct lookup by elementRef + if (isset($modulesData[$modelNodeId])) { + $module = $modulesData[$modelNodeId]; + + $this->logger->debug('Found module for node', [ + 'model_node_id' => $modelNodeId, + 'module_id' => $module['id'] ?? $module['identifier'] ?? 'unknown', + 'module_name' => $module['name'] ?? 'unnamed' + ]); + + return [$module]; // Return as array for consistency + } + + // No module found for this node + return []; + } + + /** + * Get gebruik for a specific node based on elementRef linkage + * + * @param string $modelNodeId The model node identifier (elementRef) + * @param array $gebruikData Gebruik data indexed by elementRef + * @return array Gebruik related to the node + */ + private function getNodeGebruik(string $modelNodeId, array $gebruikData): array + { + $this->logger->debug('Getting gebruik for node', ['model_node_id' => $modelNodeId]); + + // Direct lookup by elementRef + if (isset($gebruikData[$modelNodeId])) { + $gebruikList = $gebruikData[$modelNodeId]; + + $this->logger->debug('Found gebruik for node', [ + 'model_node_id' => $modelNodeId, + 'gebruik_count' => count($gebruikList) + ]); + + return $gebruikList; + } + + // No usage data found for this node + return []; + } + + /** + * Get deelnames gebruik for a specific node (placeholder implementation) + * + * @param string $modelNodeId The model node identifier + * @param array $deelnamesGebruikData Deelnames gebruik data to search in + * @return array Deelnames gebruik related to the node + */ + private function getNodeDeelnamesGebruik(string $modelNodeId, array $deelnamesGebruikData): array + { + // TODO: Implement actual node deelnames gebruik matching logic + $this->logger->debug('Getting deelnames gebruik for node', ['model_node_id' => $modelNodeId]); + return []; + } + + /** + * Get ObjectService from container + * + * @return ObjectService|null ObjectService instance or null if not available + */ + private function getObjectService(): ?ObjectService + { + if (!$this->appManager->isInstalled('openregister')) { + return null; + } + + try { + return $this->container->get(ObjectService::class); + } catch (\Exception $e) { + $this->logger->warning('Failed to get ObjectService', [ + 'error' => $e->getMessage() + ]); + return null; + } + } +} diff --git a/test_export_method.php b/test_export_method.php new file mode 100644 index 00000000..9147f0a9 --- /dev/null +++ b/test_export_method.php @@ -0,0 +1,98 @@ +get(\Psr\Container\ContainerInterface::class); + $archiMateService = $container->get('OCA\\SoftwareCatalog\\Service\\ArchiMateService'); + + echo "Testing exportToArchiMate method...\n"; + echo "Method signature: exportToArchiMate(?string \$organization = null)\n\n"; + + $startTime = microtime(true); + + // Test the correct method - it only takes organization parameter + $organization = null; // No organization filter + $result = $archiMateService->exportToArchiMate($organization); + + $duration = microtime(true) - $startTime; + echo "Export completed in " . round($duration, 3) . " seconds\n\n"; + + echo "Export result:\n"; + if (is_array($result)) { + echo "Result type: Array\n"; + echo "Keys: " . implode(', ', array_keys($result)) . "\n"; + + if (isset($result['success'])) { + echo "Success: " . ($result['success'] ? 'YES' : 'NO') . "\n"; + } + + if (isset($result['message'])) { + echo "Message: {$result['message']}\n"; + } + + if (isset($result['error'])) { + echo "Error: {$result['error']}\n"; + } + + if (isset($result['xml'])) { + $xmlLength = strlen($result['xml']); + echo "XML length: $xmlLength bytes\n"; + + if ($xmlLength > 0) { + // Validate XML + libxml_use_internal_errors(true); + $dom = new DOMDocument(); + if ($dom->loadXML($result['xml'])) { + echo "āœ… Generated XML is valid!\n"; + + // Save for inspection + file_put_contents('/tmp/test_export.xml', $result['xml']); + echo "XML saved to /tmp/test_export.xml\n"; + + // Show first few lines + $lines = explode("\n", $result['xml']); + echo "First 10 lines:\n"; + foreach (array_slice($lines, 0, 10) as $i => $line) { + echo sprintf("%2d: %s\n", $i+1, htmlspecialchars(substr($line, 0, 100))); + } + + } else { + echo "āŒ Generated XML is invalid:\n"; + $errors = libxml_get_errors(); + foreach ($errors as $error) { + echo " - Line {$error->line}: {$error->message}"; + } + } + } + } + + if (isset($result['file_name'])) { + echo "File name: {$result['file_name']}\n"; + } + + if (isset($result['statistics'])) { + echo "Statistics:\n"; + print_r($result['statistics']); + } + + } else { + echo "Result type: " . gettype($result) . "\n"; + echo "Result content:\n"; + var_dump($result); + } + +} catch (Exception $e) { + echo "āŒ Export test failed: {$e->getMessage()}\n"; + echo "Stack trace:\n{$e->getTraceAsString()}\n"; +} +?> diff --git a/test_small_export.php b/test_small_export.php new file mode 100644 index 00000000..f2b7089e --- /dev/null +++ b/test_small_export.php @@ -0,0 +1,115 @@ +get(\Psr\Container\ContainerInterface::class); + + // Get the export service directly + $exportService = $container->get('OCA\\SoftwareCatalog\\Service\\ArchiMateExportService'); + $objectService = $container->get('OCA\\OpenRegister\\Service\\ObjectService'); + + echo "Services loaded successfully\n\n"; + + // Get just 3 objects from the database for testing + $db = \OC::$server->getDatabaseConnection(); + $stmt = $db->prepare('SELECT * FROM oc_openregister_objects WHERE register = ? LIMIT 3'); + $stmt->execute(['20']); + $rawObjects = $stmt->fetchAll(); + + echo "Retrieved " . count($rawObjects) . " objects for testing\n"; + + if (empty($rawObjects)) { + echo "āŒ No objects found in register 20\n"; + exit(1); + } + + // Convert to object format that the export service expects + $objects = []; + foreach ($rawObjects as $rawObj) { + $objectData = json_decode($rawObj['object'], true); + if ($objectData) { + $objects[] = (object) $objectData; + echo "āœ… Object {$rawObj['id']}: section=" . ($objectData['section'] ?? 'unknown') . ", schema=" . ($objectData['schema'] ?? 'unknown') . "\n"; + } + } + + echo "\nTesting XML generation with " . count($objects) . " objects...\n\n"; + + // Test the export with minimal objects + $exportOptions = [ + 'includeRelationships' => false, + 'includeViews' => false, + 'selectedSchemas' => [] + ]; + + try { + $result = $exportService->exportArchiMateXml( + $objectService, + 20, // register ID + $exportOptions, + null // no organization filter + ); + + if (is_array($result) && isset($result['success'])) { + if ($result['success']) { + echo "āœ… Export successful!\n"; + + if (isset($result['xml'])) { + $xmlLength = strlen($result['xml']); + echo "Generated XML length: $xmlLength bytes\n"; + + // Save the XML for inspection + file_put_contents('/tmp/small_export_test.xml', $result['xml']); + echo "XML saved to /tmp/small_export_test.xml\n"; + + // Show first few lines for inspection + $lines = explode("\n", $result['xml']); + echo "\nFirst 20 lines of generated XML:\n"; + echo str_repeat("-", 60) . "\n"; + foreach (array_slice($lines, 0, 20) as $i => $line) { + echo sprintf("%2d: %s\n", $i+1, htmlspecialchars(substr($line, 0, 120))); + } + + // Try to validate the XML + echo "\nValidating XML...\n"; + libxml_use_internal_errors(true); + $dom = new DOMDocument(); + if ($dom->loadXML($result['xml'])) { + echo "āœ… Generated XML is valid!\n"; + } else { + echo "āŒ XML validation failed:\n"; + $errors = libxml_get_errors(); + foreach (array_slice($errors, 0, 10) as $error) { + echo " Line {$error->line}: {$error->message}"; + } + } + } + } else { + echo "āŒ Export failed: {$result['message']}\n"; + if (isset($result['error'])) { + echo "Error details: {$result['error']}\n"; + } + } + } else { + echo "āŒ Unexpected result format\n"; + print_r($result); + } + + } catch (Exception $e) { + echo "āŒ Export exception: {$e->getMessage()}\n"; + echo "Stack trace:\n{$e->getTraceAsString()}\n"; + } + +} catch (Exception $e) { + echo "āŒ Test failed: {$e->getMessage()}\n"; + exit(1); +} +?>