using Aspose.Gis; using GisConverter.Lib.Logging; using GisConverter.Lib.Models; using System; namespace GisConverter.Lib.Converters { /// /// Universal converter implementation that orchestrates a conversion run using Aspose.GIS drivers. /// /// /// /// Overview /// - UniversalGisConverter is the orchestrator used by hosts (ConsoleApp, services) and by tests to perform /// format conversions using driver implementations (commonly Aspose.GIS-backed drivers). It centralizes the /// "non-driver" responsibilities so individual converters can remain focused on driver-specific mapping and /// option construction. /// /// /// /// Responsibilities /// - Validate inputs and early-fail with friendly diagnostics using . /// - Prepare filesystem locations (output and temp) with . /// - Handle archives and multi-file sources via , preserving the supplied /// temp-folder contract and attempting best-effort cleanup when this instance performed extraction. /// - Resolve source and destination drivers by mapping canonical option keys to driver instances /// via . /// - Construct driver-specific conversion options via /// and derive deterministic output paths via . /// - Invoke the underlying driver conversion call (VectorLayer.Convert) and translate driver outcomes into /// stable values for callers and tests. /// /// /// /// Logging and diagnostics /// - This orchestrator emits structured, level-appropriate log tokens to facilitate automated assertions in tests: /// - Debug: parameter traces, detailed cleanup internals and low-level diagnostics. /// - Info: start/finish markers and high-level success messages with output paths. /// - Warn: non-fatal issues (cleanup failures, fallbacks). /// - Error: validation failures, mapping errors and unexpected exceptions (exception objects are logged). /// - Tests and maintainers should assert on stable tokens (case-insensitive substrings) rather than entire messages. /// /// /// /// Error handling and return contract /// - The method surfaces errors via returned /// instances. Known/expected problems (validation, mapping, missing archive components) /// return ConversionResult.Failure(...) with user-friendly messages suitable for display or CI assertions. /// - Unexpected exceptions are caught at the orchestrator boundary, logged at Error level (with exception details) and /// converted to a failure result. The method is designed not to throw for recoverable errors so callers and tests /// remain deterministic. /// /// /// /// Concurrency and side-effects /// - The class contains no shared mutable state and is safe to construct/invoke concurrently. Converters must avoid /// relying on mutable static state so multiple instances can safely operate in parallel test runners or hosting services. /// - Side-effects are limited to creating/modifying files under the supplied and /// . When extraction is performed the method attempts best-effort cleanup of temp /// content it created; it will not remove a caller-managed temp folder unless it performed the extraction into it. /// /// /// /// Test guidance /// - Unit tests /// - Drive early-failure branches by supplying invalid inputs, missing archive components or invalid mapping keys. /// - Inject a test TestLogger and assert on log tokens (Debug/Info/Warn/Error) with LogAssert. /// - Use injected factories or fakes to avoid invoking heavy drivers for unit-level coverage. /// - Integration tests /// - Gate driver-backed integration tests with environment flags (for example GISCONVERTER_ENABLE_INTEGRATION=1) /// and license checks (for example AsposeLicenseManager.ApplyLicense()). /// - Keep sample artifacts small and place them under TestsApp/TestData/<format> with copy-to-output set. /// - Assertions /// - Prefer checking , file existence under the output folder, and case-insensitive /// substring checks of and logs. Avoid brittle exact message equality. /// /// /// /// Security and IO considerations /// - Treat untrusted inputs conservatively. Use helpers that implement zip-slip guards and /// bounded reads when inspecting archive contents. Do not write files outside the intended temp or output folders. /// - Sanitize temporary names and limit resource creation to avoid denial-of-service risks on shared CI agents. /// /// /// /// Extension points and best practices for implementers /// - Keep orchestration separate from driver specifics: let ConverterUtils and driver-specific option builders /// hold mapping logic so orchestration remains simple and testable. /// - If the orchestrator requires additional configuration or logging, prefer constructor injection of dependencies /// rather than using global/static state to improve testability. /// - Document whether temp folders are removed on success. Tests sometimes assert on temp-folder cleanup, so be explicit. /// /// /// /// Example usage /// /// var conv = new UniversalGisConverter(); /// var result = conv.Convert("input.geojson", "GeoJson", "Shapefile", "out", "temp"); /// if (result.IsSuccess) Console.WriteLine($"Wrote outputs to: {result.Message}"); /// else Console.Error.WriteLine($"Conversion failed: {result.Message}"); /// /// /// public class UniversalGisConverter : IConverter { /// /// Execute a conversion run from a source GIS artifact to a target format. /// /// Path to the GIS input (file or archive). /// Canonical source format option (for example "Shapefile", "GeoJson"). /// Canonical target format option (for example "GeoJson", "Kml"). /// Destination folder for output files. The method will create the folder if needed. /// Temporary folder for archive extraction and intermediate files. When the method /// performs extraction it will attempt best-effort cleanup of this folder; if the caller supplied an already-existing /// temp folder that should be preserved, pass a distinct path. /// /// A indicating success or failure. On success the result's /// contains a brief summary including the resolved output path. On failure the message contains a concise diagnostic /// token suitable for logs and CI assertion. /// /// /// /// High-level algorithm /// 1. Log a debug-level parameter trace and an info-level "starting conversion" marker. /// 2. Validate inputs with . Validation failures return early /// with a failure that contains a user-friendly message. /// 3. Ensure output / temp folders exist using . Fail early for /// irrecoverable path errors. /// 4. Prepare the source file. If the source is an archive, /// will extract components into the supplied temp folder and return the extracted source path plus a /// flag indicating extraction occurred. If preparation fails the method returns the failure result. /// 5. Resolve Aspose drivers for the source and target formats via /// . If a mapping cannot be resolved return a failure. /// 6. Build the driver-specific options via and compute /// the output path via . /// 7. Call VectorLayer.Convert with the resolved drivers and options. On success log Info and return /// ; on unexpected exceptions catch, log and return Failure. /// 8. In a finally block attempt to cleanup the temp folder only when this method performed extraction into it. /// /// public ConversionResult Convert( string gisInputFilePath, string gisSourceFormatOption, string gisTargetFormatOption, string outputFolderPath, string tempFolderPath) { // Parameter trace for diagnostics Log.Debug($"UniversalGisConverter.Convert params: gisInputFilePath='{gisInputFilePath}', gisSourceFormatOption='{gisSourceFormatOption}', gisTargetFormatOption='{gisTargetFormatOption}',outputFolderPath='{outputFolderPath}', tempFolderPath='{tempFolderPath}'"); Log.Info($"CsvConverter: starting conversion (option='{gisTargetFormatOption}', input='{gisInputFilePath}')."); Log.Info($"UniversalGisConverter: starting conversion (option='{gisTargetFormatOption}', input='{gisInputFilePath}')."); // Step 1: Validate inputs var validation = ConverterUtils.ValidateInputs(gisInputFilePath, outputFolderPath, tempFolderPath); if (validation != null) return validation; // Step 2: Prepare directories var preparation = ConverterUtils.PreparePaths(outputFolderPath, tempFolderPath); if (preparation != null) return preparation; bool extractedToTemp = false; try { // Step 3: Handle archive extraction or single file var (sourcePath, wasExtracted, results) = ConverterUtils.PrepareSourceFile( gisInputFilePath, gisSourceFormatOption, tempFolderPath); if (results == null) { return results; } extractedToTemp = wasExtracted; // Step 4: Resolve drivers var srcDriver = ConverterUtils.ConversionOptionToDriver(gisSourceFormatOption) as FileDriver; var destDriver = ConverterUtils.ConversionOptionToDriver(gisTargetFormatOption) as FileDriver; if (srcDriver == null || destDriver == null) { if (srcDriver == null) { Log.Error($"Conversion failed: invalid source format option '{gisSourceFormatOption}'"); } if (destDriver == null) { Log.Error($"Conversion failed: invalid destination format option '{gisTargetFormatOption}'"); } return ConversionResult.Failure("Invalid source or destination format"); } // Step 5: Build output path var outputPath = ConverterUtils.BuildOutputPath(outputFolderPath, gisTargetFormatOption); // Step 6: Build format-specific options var options = ConverterUtils.BuildConversionOptions(gisSourceFormatOption, gisTargetFormatOption); // Step 7: Perform conversion VectorLayer.Convert(sourcePath, srcDriver, outputPath, destDriver, options); Log.Info($"Conversion succeeded: {outputPath}"); return ConversionResult.Success($"Converted to {gisTargetFormatOption}; output: {outputPath}"); } catch (Exception ex) { Log.Error($"Conversion failed: {ex.Message}", ex); return ConversionResult.Failure($"Unexpected error: {ex.Message}"); } finally { Log.Info("Starting conversion attempt."); // Only cleanup when we actually extracted files into the temp folder // This avoids removing a pre-existing temp folder that may be managed by the caller. if (extractedToTemp) { Log.Debug("Cleaning up temp folder."); try { ConverterUtils.TryCleanupTempFolder(tempFolderPath); } catch (Exception ex) { Log.Debug($"Cleanup failed: {ex.Message}"); } } Log.Info("Finished conversion attempt."); } } } }