using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using GitConverter.Lib.Factories; using GitConverter.Lib.Logging; using GitConverter.Lib.Models; namespace GitConverter.Lib.Converters { /// /// High-level conversion orchestrator. /// /// /// Responsibilities /// - Validate paths (input/output exists, 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. /// public static class ConversionService { private static readonly Dictionary _s_archiveRequirements = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "EsriJson", new[] { ".json", ".esrijson" } }, { "GeoJson", new[] { ".geojson", ".json" } }, { "GeoJsonSeq", new[] { ".json" } }, { "Kml", new[] { ".kml" } }, { "Kmz", new[] { ".kml" } }, { "Shapefile", new[] { ".shp", ".shx", ".dbf" } }, { "Osm", new[] { ".osm" } }, { "Gdb", new[] { ".gdb" } }, { "Gpx", new[] { ".gpx" } }, { "TopoJson", new[] { ".json" } }, { "MapInfoInterchange", new[] { ".mif" } }, { "MapInfoTab", new[] { ".tab", ".dat", ".map", ".id" } }, { "Csv", new[] { ".csv" } }, { "GeoPackage", new[] { ".gpkg" } }, }; private static readonly Dictionary _s_extensionToConverter = new Dictionary(StringComparer.OrdinalIgnoreCase) { { ".geojson", "GeoJson" }, { ".esrijson", "EsriJson" }, { ".kml", "Kml" }, { ".kmz", "Kmz" }, { ".shp", "Shapefile" }, { ".osm", "Osm" }, { ".gpx", "Gpx" }, { ".gml", "Gml" }, { ".gdb", "Gdb" }, { ".mif", "MapInfoInterchange" }, { ".tab", "MapInfoTab" }, { ".map", "MapInfoTab" }, { ".dat", "MapInfoTab" }, { ".id", "MapInfoTab" }, { ".csv", "Csv" }, { ".gpkg", "GeoPackage" }, }; /// /// Orchestrate a conversion given paths and a factory. /// Note: outputFolderPath is expected to be a folder path (not a file path). /// public static ConversionResult Run(string gisInputFilePath, string outputFolderPath, string tempFolderPath, IConverterFactory factory = null) { try { Log.Info("ConversionService: Run invoked."); // Require an output FOLDER path (tests and callers expect folder semantics). if (string.IsNullOrWhiteSpace(outputFolderPath)) { Log.Error("ConversionService: output folder path is required."); return ConversionResult.Failure("Output folder path is required."); } // Reject file-like paths: caller must provide a folder, not a file with extension. if (Path.HasExtension(outputFolderPath)) { Log.Error($"ConversionService: output path '{outputFolderPath}' appears to be a file. Provide a folder path instead."); return ConversionResult.Failure("Output path must be a folder path (no filename/extension)."); } var outFolderForValidation = outputFolderPath; // Validate inputs and prepare folders (ensure output folder writable and temp ready) var prep = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outFolderForValidation, 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."); } 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(...)."); // converters expect an output folder path; pass the folder return conv.Convert(gisInputFilePath, matchedConverter, outputFolderPath, tempFolderPath); } else { 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)) { // JSON detection: prefer throwing variant then fallback to lightweight sniff JsonFormatDetector.Format jsonFmt; try { jsonFmt = JsonFormatDetector.DetectFromFile(gisInputFilePath); } catch (Exception detEx) { Log.Debug($"JsonFormatDetector.DetectFromFile failed: {detEx.Message}. Falling back to lightweight sniff."); jsonFmt = JsonFormatDetector.Format.Unknown; // lightweight fallback: read a small head of the file and look for decisive tokens try { var head = File.ReadAllText(gisInputFilePath, Encoding.UTF8); // First, try to detect NDJSON / GeoJsonSeq by checking the first non-empty line. string firstLine = null; using (var sr = new StringReader(head)) { string line; while ((line = sr.ReadLine()) != null) { if (!string.IsNullOrWhiteSpace(line)) { firstLine = line.Trim(); break; } } } // If the first non-empty line looks like a standalone JSON object/array, // treat as GeoJsonSeq (NDJSON) unless it clearly is a FeatureCollection/topology. if (!string.IsNullOrWhiteSpace(firstLine)) { // If the first line or the head contains FeatureCollection -> treat as GeoJson (full doc). if (firstLine.IndexOf("FeatureCollection", StringComparison.OrdinalIgnoreCase) >= 0 || head.IndexOf("FeatureCollection", StringComparison.OrdinalIgnoreCase) >= 0) { jsonFmt = JsonFormatDetector.Format.GeoJson; } else if (firstLine.IndexOf("\"Topology\"", StringComparison.OrdinalIgnoreCase) >= 0 || head.IndexOf("\"Topology\"", StringComparison.OrdinalIgnoreCase) >= 0) { jsonFmt = JsonFormatDetector.Format.TopoJson; } else if (firstLine.IndexOf("\"spatialReference\"", StringComparison.OrdinalIgnoreCase) >= 0 || head.IndexOf("\"spatialReference\"", StringComparison.OrdinalIgnoreCase) >= 0) { jsonFmt = JsonFormatDetector.Format.EsriJson; } else { // If first non-empty line starts with "{" or "[" assume NDJSON/GeoJsonSeq var flTrim = firstLine.TrimStart(); if (flTrim.StartsWith("{") || flTrim.StartsWith("[")) { jsonFmt = JsonFormatDetector.Format.GeoJsonSeq; } } } else { // No lines found — leave as Unknown } } catch (Exception readEx) { Log.Debug($"Fallback JSON sniff failed: {readEx.Message}"); } } if (jsonFmt == JsonFormatDetector.Format.Unknown) { 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)."); } 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; default: converterKeyForJson = null; 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, outputFolderPath, tempFolderPath); } 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(...)."); return convNonJson.Convert(gisInputFilePath, converterKeyNonJson, outputFolderPath, tempFolderPath); } } catch (Exception ex) { Log.Error($"Unexpected error in ConversionService.Run: {ex.Message}", ex); return ConversionResult.Failure($"Unexpected error: {ex.Message}"); } } private static string DetectConverterFromArchiveEntries(IEnumerable entries) { var exts = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var e in entries) { try { if (string.IsNullOrWhiteSpace(e)) continue; // Normal extension from the entry path (file or dir entry) var ext = Path.GetExtension(e); if (!string.IsNullOrEmpty(ext)) exts.Add(ext.ToLowerInvariant()); // Also inspect path segments for folder names that end with a known extension, // e.g. "Some.gdb/..." inside a zip. Path.GetExtension on "Some.gdb/entry" returns "". // Split on both separators to be robust across archive entry styles. var segments = e.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); foreach (var seg in segments) { if (seg.EndsWith(".gdb", StringComparison.OrdinalIgnoreCase)) { exts.Add(".gdb"); } } } catch { // ignore malformed names } } Log.Debug($"Archive contains {exts.Count} distinct extensions (or folder markers): {string.Join(", ", exts)}"); 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 'Kmz' converter."); return "Kmz"; } Log.Debug("No archive-based converter match found."); return null; } } }