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.");
}
}
}
}