public void TestProfilesTypesJson() { const string path = @"TestData\profiles-types.json"; var summaries = ArtifactSummaryGenerator.Generate(path); Assert.IsNotNull(summaries); Assert.AreNotEqual(0, summaries.Count); for (int i = 0; i < summaries.Count; i++) { var summary = summaries[i]; Assert.IsFalse(summary.IsFaulted); // Common properties Assert.AreEqual(path, summary.Origin); Assert.AreEqual(ResourceType.StructureDefinition.GetLiteral(), summary.ResourceTypeName); Assert.IsTrue(summary.ResourceType == ResourceType.StructureDefinition); // Conformance resource properties Assert.IsNotNull(summary.GetConformanceCanonicalUrl()); Assert.IsTrue(summary.GetConformanceCanonicalUrl().ToString().StartsWith("http://hl7.org/fhir/StructureDefinition/")); Assert.IsNotNull(summary.GetConformanceName()); Assert.AreEqual(ConformanceResourceStatus.Draft.GetLiteral(), summary.GetConformanceStatus()); //Debug.WriteLine($"{summary.ResourceType} | {summary.Canonical()} | {summary.Name()}"); // StructureDefinition properties Assert.IsNotNull(summary.GetStructureDefinitionFhirVersion()); Assert.AreEqual(ModelInfo.Version, summary.GetStructureDefinitionFhirVersion()); Assert.AreEqual(StructureDefinition.StructureDefinitionKind.Datatype.GetLiteral(), summary.GetStructureDefinitionKind()); // If this is a constraining StructDef, then Base should also be specified Assert.IsTrue(summary.GetStructureDefinitionConstrainedType() == null || summary.GetStructureDefinitionBase() != null); } }
/// <summary> /// Create a new <see cref="DirectorySource"/> instance to browse and resolve resources /// from the specified <paramref name="contentDirectory"/> /// and using the default <see cref="DirectorySourceSettings"/>. /// </summary> /// <para> /// Initialization is thread-safe. The source ensures that only a single thread will /// collect the artifact summaries, while any other threads will block. /// </para> /// <param name="contentDirectory">The file path of the target directory.</param> /// <exception cref="ArgumentNullException">The specified argument is <c>null</c>.</exception> public DirectorySource(string contentDirectory) { ContentDirectory = contentDirectory ?? throw Error.ArgumentNull(nameof(contentDirectory)); _settings = new DirectorySourceSettings(); _summaryGenerator = new ArtifactSummaryGenerator(_settings.ExcludeSummariesForUnknownArtifacts); // Initialize Lazy Refresh(); }
/// <summary> /// Create a new <see cref="DirectorySource"/> instance to browse and resolve resources /// from the specified <paramref name="contentDirectory"/> /// and using the specified <see cref="DirectorySourceSettings"/>. /// <para> /// Initialization is thread-safe. The source ensures that only a single thread will /// collect the artifact summaries, while any other threads will block. /// </para> /// </summary> /// <param name="contentDirectory">The file path of the target directory.</param> /// <param name="settings">Configuration settings that control the behavior of the <see cref="DirectorySource"/>.</param> /// <exception cref="ArgumentNullException">One of the specified arguments is <c>null</c>.</exception> public DirectorySource(string contentDirectory, DirectorySourceSettings settings) { ContentDirectory = contentDirectory ?? throw Error.ArgumentNull(nameof(contentDirectory)); // [WMR 20171023] Always copy the specified settings, to prevent shared state _settings = new DirectorySourceSettings(settings); _summaryGenerator = new ArtifactSummaryGenerator(_settings.ExcludeSummariesForUnknownArtifacts); // Initialize Lazy Refresh(); }
ArtifactSummary assertSummary(string path, params ArtifactSummaryHarvester[] harvesters) { var summaries = ArtifactSummaryGenerator.Generate(path, harvesters); Assert.IsNotNull(summaries); Assert.AreEqual(1, summaries.Count); var summary = summaries[0]; Assert.IsFalse(summary.IsFaulted); Assert.AreEqual(path, summary.Origin); return(summary); }
public void TestLoadResourceFromZipStream() { // Harvest summaries and load artifact straight from core ZIP archive // Use XmlNavigatorStream to navigate resources stored inside a zip file // ZipDeflateStream does not support seeking (forward-only stream) // Therefore this only works for the XmlNavigatorStream, as the ctor does NOT (need to) call Reset() // JsonNavigatorStream cannot support zip streams; ctor needs to call Reset after scanning resourceType ArtifactSummary corePatientSummary; var corePatientUrl = ModelInfo.CanonicalUriForFhirCoreType(FHIRDefinedType.Patient); string zipEntryName = "profiles-resources.xml"; // Generate summaries from core ZIP resource definitions (extract in memory) using (var archive = ZipFile.Open(ZipSource.SpecificationZipFileName, ZipArchiveMode.Read)) { var entry = archive.Entries.FirstOrDefault(e => e.Name == zipEntryName); Assert.IsNotNull(entry); using (var entryStream = entry.Open()) using (var navStream = new XmlNavigatorStream(entryStream)) { var summaries = ArtifactSummaryGenerator.Generate(navStream); Assert.IsNotNull(summaries); corePatientSummary = summaries.FindConformanceResources(corePatientUrl).FirstOrDefault(); } } Assert.IsNotNull(corePatientSummary); Assert.AreEqual(ResourceType.StructureDefinition, corePatientSummary.ResourceType); Assert.AreEqual(corePatientUrl, corePatientSummary.GetConformanceCanonicalUrl()); // Load core Patient resource from ZIP (extract in memory) using (var archive = ZipFile.Open(ZipSource.SpecificationZipFileName, ZipArchiveMode.Read)) { var entry = archive.Entries.FirstOrDefault(e => e.Name == zipEntryName); using (var entryStream = entry.Open()) using (var navStream = new XmlNavigatorStream(entryStream)) { var nav = navStream.Current; if (nav != null) { // Parse target resource from navigator var parser = new BaseFhirParser(); var corePatient = parser.Parse <StructureDefinition>(nav); Assert.IsNotNull(corePatient); Assert.AreEqual(corePatientUrl, corePatient.Url); } } } }
// Internal ctor DirectorySource(string contentDirectory, DirectorySourceSettings settings, bool cloneSettings) { ContentDirectory = contentDirectory ?? throw Error.ArgumentNull(nameof(contentDirectory)); // [WMR 20171023] Clone specified settings to prevent shared state _settings = settings != null ? (cloneSettings ? new DirectorySourceSettings(settings) : settings) : DirectorySourceSettings.CreateDefault(); _summaryGenerator = new ArtifactSummaryGenerator(_settings.ExcludeSummariesForUnknownArtifacts); _navigatorFactory = new ConfigurableNavigatorStreamFactory(_settings.XmlParserSettings, _settings.JsonParserSettings) { ThrowOnUnsupportedFormat = false }; // Initialize Lazy Refresh(); }
/// <summary>Request a re-scan of one or more specific artifact file(s).</summary> /// <param name="filePaths">One or more artifact file path(s).</param> /// <remarks> /// Notify the <see cref="DirectorySource"/> that specific files in the current /// <see cref="ContentDirectory"/> have been created, updated or deleted. /// The <paramref name="filePaths"/> argument should specify an array of artifact /// file paths that (may) have been deleted, modified or created. /// The source will: /// <list type="number"> /// <item> /// <description> /// Remove any existing summary information for the specified artifacts, if available. /// </description> /// </item> /// <item> /// <description> /// Try to harvest new summary information from the specified artifacts, if they still exist. /// </description> /// </item> /// </list> /// </remarks> public void Refresh(params string[] filePaths) { if (filePaths == null || filePaths.Length == 0) { throw Error.ArgumentNullOrEmpty(nameof(filePaths)); } #if THREADSAFE lock (_syncRoot) #endif { if (_artifactFilePaths == null) { // Cache is empty, perform full scan on demand return; } // Update file paths foreach (var filePath in filePaths) { // Update file paths bool exists = File.Exists(filePath); if (!exists) { _artifactFilePaths.Remove(filePath); } else if (!_artifactFilePaths.Contains(filePath)) { _artifactFilePaths.Add(filePath); } // Update summaries (if cached) if (_artifactSummaries != null) { _artifactSummaries.RemoveAll(s => StringComparer.OrdinalIgnoreCase.Equals(filePath, s.Origin)); if (exists) { // May fail, e.g. if another thread/process has deleted the target file // Generate will catch exceptions and return empty list var summaries = ArtifactSummaryGenerator.Generate(filePath, _settings.SummaryDetailsHarvesters); _artifactSummaries.AddRange(summaries); } // [WMR 20180409] No need to recreate r/o wrapper, automatically synchronized // _roArtifactSummaries = _artifactSummaries.AsReadOnly(); } } } }
public void TestProfilesResourcesXml() { const string path = @"TestData\profiles-resources.xml"; var summaries = ArtifactSummaryGenerator.Generate(path); Assert.IsNotNull(summaries); Assert.AreNotEqual(0, summaries.Count); for (int i = 0; i < summaries.Count; i++) { var summary = summaries[i]; Assert.IsFalse(summary.IsFaulted); // Common properties Assert.AreEqual(path, summary.Origin); var fi = new FileInfo(path); Assert.AreEqual(fi.Length, summary.FileSize); Assert.AreEqual(fi.LastWriteTimeUtc, summary.LastModified); if (StringComparer.Ordinal.Equals(ResourceType.StructureDefinition.GetLiteral(), summary.ResourceTypeName)) { Assert.IsTrue(summary.ResourceType == ResourceType.StructureDefinition); // Conformance resource properties Assert.IsNotNull(summary.GetConformanceCanonicalUrl()); Assert.IsTrue(summary.GetConformanceCanonicalUrl().ToString().StartsWith("http://hl7.org/fhir/StructureDefinition/")); Assert.IsNotNull(summary.GetConformanceName()); Assert.AreEqual(ConformanceResourceStatus.Draft.GetLiteral(), summary.GetConformanceStatus()); //Debug.WriteLine($"{summary.ResourceType} | {summary.Canonical()} | {summary.Name()}"); // StructureDefinition properties Assert.IsNotNull(summary.GetStructureDefinitionFhirVersion()); Assert.AreEqual(ModelInfo.Version, summary.GetStructureDefinitionFhirVersion()); Assert.AreEqual(StructureDefinition.StructureDefinitionKind.Resource.GetLiteral(), summary.GetStructureDefinitionKind()); // If this is a constraining StructDef, then Base should also be specified Assert.IsTrue(summary.GetStructureDefinitionConstrainedType() == null || summary.GetStructureDefinitionBase() != null); // [WMR 20171218] Maturity Level extension Assert.IsNotNull(summary.GetStructureDefinitionMaturityLevel()); } } }
ArtifactSummary assertSummary(string path, params ArtifactSummaryHarvester[] harvesters) { var summaries = ArtifactSummaryGenerator.Generate(path, harvesters); Assert.IsNotNull(summaries); Assert.AreEqual(1, summaries.Count); var summary = summaries[0]; Assert.IsFalse(summary.IsFaulted); Assert.AreEqual(path, summary.Origin); var fi = new FileInfo(path); Assert.AreEqual(fi.Length, summary.FileSize); Assert.AreEqual(fi.LastWriteTimeUtc, summary.LastModified); return(summary); }
private static List <ArtifactSummary> harvestSummaries(List <string> paths, ArtifactSummaryHarvester[] harvesters, bool singleThreaded) { // [WMR 20171023] Note: some files may no longer exist var cnt = paths.Count; var scanResult = new List <ArtifactSummary>(cnt); if (singleThreaded) { foreach (var filePath in paths) { var summaries = ArtifactSummaryGenerator.Generate(filePath, harvesters); scanResult.AddRange(summaries); } } else { // Optimization: use Task.Parallel.ForEach to process files in parallel // More efficient then creating task per file (esp. if many files) // // For netstandard13, add NuGet package System.Threading.Tasks.Parallel // // <ItemGroup Condition=" '$(TargetFramework)' != 'net45' "> // <PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" /> // </ItemGroup> // // TODO: // - Support TimeOut // - Support CancellationToken (how to inject?) // Pre-allocate results array, one entry per file // Each entry receives a list with summaries harvested from a single file (Bundles return 0..*) var results = new List <ArtifactSummary> [cnt]; try { // Process files in parallel var loopResult = Parallel.For(0, cnt, // new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount }, i => { // Harvest summaries from single file // Save each result to a separate array entry (no locking required) results[i] = ArtifactSummaryGenerator.Generate(paths[i], harvesters); }); } catch (AggregateException aex) { // ArtifactSummaryHarvester.HarvestAll catches and returns exceptions using ArtifactSummary.FromException // However Parallel.For may still throw, e.g. due to time out or cancel // var isCanceled = ex.InnerExceptions.OfType<TaskCanceledException>().Any(); Debug.WriteLine($"[{nameof(DirectorySource)}.{nameof(harvestSummaries)}] {aex.GetType().Name}: {aex.Message}" + aex.InnerExceptions?.Select(ix => $"\r\n\t{ix.GetType().Name}: {ix.Message}")); // [WMR 20171023] Return exceptions via ArtifactSummary.FromException // Or unwrap all inner exceptions? // scanResult.Add(ArtifactSummary.FromException(aex)); scanResult.AddRange(aex.InnerExceptions.Select(ArtifactSummary.FromException)); } // Aggregate completed results into single list scanResult.AddRange(results.SelectMany(r => r ?? Enumerable.Empty <ArtifactSummary>())); } return(scanResult); }
/// <summary> /// Re-index one or more specific artifact file(s). /// <para> /// Notifies the <see cref="DirectorySource"/> that specific files in the current /// <see cref="ContentDirectory"/> have been created, updated or deleted. /// The <paramref name="filePaths"/> argument should specify an array of artifact /// file paths that (may) have been deleted, modified or created. /// </para> /// <para> /// The source will: /// <list type="number"> /// <item>remove any existing summary information for the specified artifacts, if available;</item> /// <item>try to harvest updated summary information from the specified artifacts, if they still exist.</item> /// </list> /// </para> /// </summary> /// <param name="filePaths">One or more artifact file path(s).</param> /// <returns> /// <c>true</c> if succesful, i.e. if matching cache items have been evicted, or <c>false</c> otherwise. /// </returns> public bool Refresh(params string[] filePaths) { if (filePaths == null || filePaths.Length == 0) { throw Error.ArgumentNullOrEmpty(nameof(filePaths)); } bool result = false; #if THREADSAFE lock (_syncRoot) #endif { var artifactFilePaths = _artifactFilePaths; if (artifactFilePaths == null) { // Cache is empty, perform full scan on demand return(false); } // Update file paths foreach (var filePath in filePaths) { // Update file paths bool exists = File.Exists(filePath); if (!exists) { // Return true if existing file path was evicted from cache result = artifactFilePaths.Remove(filePath); } else if (!artifactFilePaths.Contains(filePath)) { // Discovered new artifact; cache file path and return true artifactFilePaths.Add(filePath); result = true; } // Update summaries (if cached) var artifactSummaries = _artifactSummaries; if (artifactSummaries != null) { if (artifactSummaries.RemoveAll(s => StringComparer.OrdinalIgnoreCase.Equals(filePath, s.Origin)) > 0) { // Evicted some existing summaries from the cache; return true result = true; } if (exists) { // May fail, e.g. if another thread/process has deleted the target file // Generate will catch exceptions and return empty list var summaries = ArtifactSummaryGenerator.Generate(filePath, _settings.SummaryDetailsHarvesters); artifactSummaries.AddRange(summaries); if (summaries.Count > 0) { // Added some new/updated summaries to the cache; return true result = true; } } } } } return(result); }