using System; using System.Collections.Generic; using System.IO; using System.Linq; using GitConverter.Lib.Factories; using GitConverter.Lib.Logging; using GitConverter.Lib.Models; namespace GitConverter.Lib.Converters { /// /// High-level conversion orchestrator. /// /// /// Responsibilities /// - Validate paths (input exists, output/temp can be created). /// - Inspect input: single file or archive. /// - Detect the best matching converter based on input extension or archive contents (required file extensions). /// - Resolve converter using ConverterFactory (TryCreate) and invoke its Convert method. /// - Log each step and return a friendly ConversionResult on expected failures (validation, unknown option, /// missing required files) rather than throwing. /// /// Design notes / best practices /// - The service is intentionally small and deterministic; the detection rules are explicit and easy to extend. /// - The factory is used for converter resolution; tests may inject a custom ConverterFactory into the Factory /// instance returned by the default constructor if a test seam is added in the ConsoleApp (or tests can /// construct their own ConverterFactory and call converters directly). /// - Do not swallow unexpected exceptions at the orchestration layer; log and return a Failure result so callers /// can surface a helpful message. (We catch and convert to Failure below to keep the CLI robust.) /// public static class ConversionService { // Known converters and their required file extensions when input is an archive. // Keys are display names used by ConverterFactory. // Values: required extensions (lowercase, include leading dot) that must be present in the archive. private static readonly Dictionary _s_archiveRequirements = new Dictionary(StringComparer.OrdinalIgnoreCase) { // JSON / GeoJSON-like formats (archives containing .json / .geojson) { "EsriJson", new[] { ".json", ".esrijson" } }, { "GeoJson", new[] { ".geojson", ".json" } }, { "GeoJsonSeq", new[] { ".json" } }, // KML / KMZ (KMZ is a zip containing .kml) { "Kml", new[] { ".kml" } }, { "Kmz", new[] { ".kml" } }, // Shapefile requires .shp, .shx, .dbf { "Shapefile", new[] { ".shp", ".shx", ".dbf" } }, // OSM (OpenStreetMap XML) { "Osm", new[] { ".osm" } }, // File Geodatabase { "Gdb", new[] { ".gdb" } }, // GPX { "Gpx", new[] { ".gpx" } }, // GPX { "TopoJson", new[] { ".json" } }, // MapInfo formats { "MapInfoInterchange", new[] { ".mif" } }, { "MapInfoTab", new[] { ".tab", ".dat", ".map", ".id" } }, // CSV { "Csv", new[] { ".csv" } }, // GeoPackage { "GeoPackage", new[] { ".gpkg" } }, }; // Mapping of single-file extensions to converter display names. // Covers the requested set of GIS formats so single-file inputs are dispatched to the expected converters. private static readonly Dictionary _s_extensionToConverter = new Dictionary(StringComparer.OrdinalIgnoreCase) { // JSON / GeoJSON family - JSON is handled specially by JsonFormatDetector at runtime { ".geojson", "GeoJson" }, { ".esrijson", "EsriJson" }, // KML / KMZ { ".kml", "Kml" }, { ".kmz", "Kmz" }, // Shapefile (single-file entry not common; usually archive or folder) { ".shp", "Shapefile" }, // OSM { ".osm", "Osm" }, // GPX { ".gpx", "Gpx" }, // GML { ".gml", "Gml" }, // File Geodatabase (folder - mapped by extension .gdb where applicable) { ".gdb", "Gdb" }, // MapInfo { ".mif", "MapInfoInterchange" }, { ".tab", "MapInfoTab" }, { ".map", "MapInfoTab" }, { ".dat", "MapInfoTab" }, { ".id", "MapInfoTab" }, // Tabular / package formats { ".csv", "Csv" }, { ".gpkg", "GeoPackage" }, }; /// /// Orchestrate a conversion given paths and a factory. /// /// Input file or archive. /// Output folder for converter results. /// Working folder for extraction / intermediate files. /// ConverterFactory instance to resolve converters (optional). When null a default ConverterFactory is used. /// ConversionResult describing the outcome; friendly messages are returned for validation or detection failures. public static ConversionResult Run(string gisInputFilePath, string outFolderPath, string tempFolderPath, IConverterFactory factory = null) { try { Log.Info("ConversionService: Run invoked."); // Validate inputs and prepare folders var prep = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outFolderPath, tempFolderPath); if (prep != null) return prep; // validation failure if (factory == null) { factory = new ConverterFactory(); } // Determine input kind if (ConverterUtils.IsArchiveFile(gisInputFilePath)) { Log.Info($"Input '{gisInputFilePath}' detected as archive. Inspecting contents."); var entries = ConverterUtils.TryListArchiveEntries(gisInputFilePath); if (entries == null) { Log.Error("Failed to list archive entries."); return ConversionResult.Failure("Failed to inspect archive contents."); } // Discover candidate converter by checking archive requirements var matchedConverter = DetectConverterFromArchiveEntries(entries); if (string.IsNullOrEmpty(matchedConverter)) { Log.Warn("No converter matched archive contents."); return ConversionResult.Failure("No converter matched archive contents or required files are missing."); } Log.Info($"Archive matched converter '{matchedConverter}'. Attempting to resolve converter instance."); if (!factory.TryCreate(matchedConverter, out var conv)) { Log.Error($"ConverterFactory failed to resolve converter '{matchedConverter}'."); return ConversionResult.Failure($"Converter for '{matchedConverter}' is not available."); } Log.Info($"Converter '{matchedConverter}' resolved. Invoking Convert(...)."); // pass the resolved converter key as gisTargetFormatOption (four-argument signature) return conv.Convert(gisInputFilePath, matchedConverter, outFolderPath, tempFolderPath); } else { // Single-file path: map extension to converter, with special handling for JSON-family files var ext = Path.GetExtension(gisInputFilePath); Log.Info($"Input '{gisInputFilePath}' detected as single file with extension '{ext}'."); if (!string.IsNullOrWhiteSpace(ext) && ext.EndsWith("json", StringComparison.OrdinalIgnoreCase)) { // Use JsonFormatDetector to determine specific JSON format if (!JsonFormatDetector.TryDetectFromFile(gisInputFilePath, out var jsonFmt)) { Log.Error("Unable to parse JSON input to determine specific JSON GIS format."); return ConversionResult.Failure("Unable to determine JSON format (GeoJson / EsriJson / GeoJsonSeq / TopoJson)."); } switch (jsonFmt) { case JsonFormatDetector.Format.GeoJson: ext = ".geojson"; break; case JsonFormatDetector.Format.EsriJson: ext = ".esrijson"; break; case JsonFormatDetector.Format.GeoJsonSeq: ext = ".geojsonseq"; break; case JsonFormatDetector.Format.TopoJson: ext = ".topojson"; break; default: Log.Error("JSON format detected as Unknown."); return ConversionResult.Failure("Unrecognized JSON format; provide a GeoJSON, EsriJSON, TopoJSON or GeoJSON sequence file."); } // Map the detected JSON format to converter key string converterKeyForJson = null; switch (jsonFmt) { case JsonFormatDetector.Format.GeoJson: converterKeyForJson = "GeoJson"; break; case JsonFormatDetector.Format.EsriJson: converterKeyForJson = "EsriJson"; break; case JsonFormatDetector.Format.GeoJsonSeq: converterKeyForJson = "GeoJsonSeq"; break; case JsonFormatDetector.Format.TopoJson: converterKeyForJson = "TopoJson"; break; } if (string.IsNullOrWhiteSpace(converterKeyForJson)) { Log.Error("Failed to map detected JSON format to a converter key."); return ConversionResult.Failure("Failed to map JSON format to converter."); } Log.Info($"Detected JSON format '{jsonFmt}'. Resolving converter '{converterKeyForJson}'."); if (!factory.TryCreate(converterKeyForJson, out var convJson)) { Log.Error($"ConverterFactory failed to resolve converter '{converterKeyForJson}'."); return ConversionResult.Failure($"Converter for '{converterKeyForJson}' is not available."); } return convJson.Convert(gisInputFilePath, converterKeyForJson, outFolderPath, tempFolderPath); } // Not JSON-family: use extension mapping if (!_s_extensionToConverter.TryGetValue(ext, out var converterKeyNonJson)) { Log.Warn($"No converter mapping for extension '{ext}'."); return ConversionResult.Failure($"Unknown input file type '{ext}'."); } Log.Info($"Mapped extension '{ext}' to converter '{converterKeyNonJson}'. Attempting to resolve."); if (!factory.TryCreate(converterKeyNonJson, out var convNonJson)) { Log.Error($"ConverterFactory failed to resolve converter '{converterKeyNonJson}'."); return ConversionResult.Failure($"Converter for '{converterKeyNonJson}' is not available."); } Log.Info($"Converter '{converterKeyNonJson}' resolved. Invoking Convert(...)."); // pass the resolved converter key as gisConversionOption (four-argument signature) return convNonJson.Convert(gisInputFilePath, converterKeyNonJson, outFolderPath, tempFolderPath); } } catch (Exception ex) { Log.Error($"Unexpected error in ConversionService.Run: {ex.Message}", ex); return ConversionResult.Failure($"Unexpected error: {ex.Message}"); } } /// /// Inspect archive entries and attempt to match a known converter based on required extensions. /// /// Archive entry names (relative paths inside archive). /// Display name of matching converter or null if none match. private static string DetectConverterFromArchiveEntries(IEnumerable entries) { // Normalize entry extensions var exts = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var e in entries) { try { var ext = Path.GetExtension(e); if (!string.IsNullOrEmpty(ext)) exts.Add(ext.ToLowerInvariant()); } catch { // ignore malformed names } } Log.Debug($"Archive contains {exts.Count} distinct extensions: {string.Join(", ", exts)}"); // Check each known converter's requirements foreach (var kv in _s_archiveRequirements) { var required = kv.Value; var allPresent = required.All(r => exts.Contains(r)); if (allPresent) { Log.Debug($"Archive satisfies requirements for '{kv.Key}'."); return kv.Key; } } // Special-case: KMZ archive typically contains a .kml if (exts.Contains(".kml")) { Log.Debug("Archive contains .kml; selecting 'Kml' converter."); return "Kml"; } // No match Log.Debug("No archive-based converter match found."); return null; } } }