|
| 1 | +using Hl7.Fhir.Model; |
| 2 | +using Hl7.Fhir.Rest; |
| 3 | +using Hl7.Fhir.Specification.Source; |
| 4 | +using Hl7.Fhir.Support; |
| 5 | +using Hl7.Fhir.Utility; |
| 6 | +using Hl7.Fhir.WebApi; |
| 7 | +using System.Collections.Generic; |
| 8 | +using System.Linq; |
| 9 | +using System.Net; |
| 10 | +using System.Threading.Tasks; |
| 11 | + |
| 12 | +namespace Hl7.Fhir.DemoFileSystemFhirServer |
| 13 | +{ |
| 14 | + public class CanonicalResourceService<TSP> : DirectoryResourceService<TSP> |
| 15 | + where TSP : class |
| 16 | + { |
| 17 | + public CanonicalResourceService(ModelBaseInputs<TSP> requestDetails, string resourceName, string directory, IResourceResolver source, IAsyncResourceResolver asyncSource) |
| 18 | + : base(requestDetails, resourceName, directory, source, asyncSource) |
| 19 | + { |
| 20 | + } |
| 21 | + |
| 22 | + public CanonicalResourceService(ModelBaseInputs<TSP> requestDetails, string resourceName, string directory, IResourceResolver source, IAsyncResourceResolver asyncSource, SearchIndexer indexer) |
| 23 | + : base(requestDetails, resourceName, directory, source, asyncSource, indexer) |
| 24 | + { |
| 25 | + } |
| 26 | + |
| 27 | + override async public Task<OperationOutcome> ValidateResource(Resource resource, ResourceValidationMode mode, string[] profiles) |
| 28 | + { |
| 29 | + var outcome = await base.ValidateResource(resource, mode, profiles); |
| 30 | + |
| 31 | + if (outcome.Success && resource is IVersionableConformanceResource icr) |
| 32 | + { |
| 33 | + // This validation was successful, check that the canonical URL/Version/algorithm doesn't |
| 34 | + // create some invalid state for the server to be in |
| 35 | + var kvps = new List<KeyValuePair<string, string>>(); |
| 36 | + kvps.Add(new KeyValuePair<string, string>("url", icr.Url)); |
| 37 | + var bundle = await Search(kvps, null, SummaryType.True, null); |
| 38 | + |
| 39 | + var ivrs = bundle.Entry |
| 40 | + .Select(e => e.Resource as IVersionableConformanceResource) |
| 41 | + .Where(e => (e as Resource).Id != resource.Id) |
| 42 | + .ToList(); // exclude itself (for updates) |
| 43 | + |
| 44 | + // Verify that this version doesn't already exist too. |
| 45 | + if (ivrs.Any(v => v.Version == icr.Version)) |
| 46 | + { |
| 47 | + outcome.Issue.Insert(0, new OperationOutcome.IssueComponent |
| 48 | + { |
| 49 | + Code = OperationOutcome.IssueType.Duplicate, |
| 50 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 51 | + Details = new CodeableConcept(null, null, $"Version {icr.Version} already exists") |
| 52 | + }); |
| 53 | + } |
| 54 | + |
| 55 | + // Verify version algorithm isn't defined differently |
| 56 | + bool conflictingAlgorithm = false; |
| 57 | + var existingFhirpathAlg = ivrs.Where(e => e.versionAlgorithFhirPathExpression() != null).Select(e => e.versionAlgorithFhirPathExpression()); |
| 58 | + var existingVerAlg = ivrs.Where(e => e.versionAlgorithCoded() != null).Select(e => e.versionAlgorithCoded().Code); |
| 59 | + if (icr.versionAlgorithFhirPathExpression() != null |
| 60 | + && (existingVerAlg.Any() || existingFhirpathAlg.Any(e => e != icr.versionAlgorithFhirPathExpression()))) |
| 61 | + conflictingAlgorithm = true; |
| 62 | + |
| 63 | + if (icr.versionAlgorithCoded() != null |
| 64 | + && (existingFhirpathAlg.Any() || existingVerAlg.Any(e => e != icr.versionAlgorithCoded().Code))) |
| 65 | + conflictingAlgorithm = true; |
| 66 | + |
| 67 | + if (conflictingAlgorithm == true) |
| 68 | + { |
| 69 | + outcome.Issue.Insert(0, new OperationOutcome.IssueComponent |
| 70 | + { |
| 71 | + Code = OperationOutcome.IssueType.BusinessRule, |
| 72 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 73 | + Details = new CodeableConcept(null, null, $"Ambiguous version algorithms would result: {string.Join(",", existingVerAlg.Union(existingFhirpathAlg))} is the existing algorithm") |
| 74 | + }); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + return outcome; |
| 79 | + } |
| 80 | + |
| 81 | + override public async Task<Resource> PerformOperation(string operation, Parameters operationParameters, SummaryType summary) |
| 82 | + { |
| 83 | + switch (operation.ToLower()) |
| 84 | + { |
| 85 | + case "current-canonical": |
| 86 | + return await PerformOperation_CurrentCanonical(operationParameters, summary); |
| 87 | + } |
| 88 | + |
| 89 | + return await base.PerformOperation(operation, operationParameters, summary); |
| 90 | + } |
| 91 | + |
| 92 | + private async Task<Resource> PerformOperation_CurrentCanonical(Parameters operationParameters, SummaryType summary) |
| 93 | + { |
| 94 | + var outcome = new OperationOutcome(); |
| 95 | + string url = null; |
| 96 | + var statuses = new List<string>(); |
| 97 | + |
| 98 | + // read the URL parameter |
| 99 | + var urlParams = operationParameters.Parameter.Where(p => p.Name?.ToLower() == "url"); |
| 100 | + if (urlParams.Any()) |
| 101 | + { |
| 102 | + if (urlParams.Count() > 1) |
| 103 | + { |
| 104 | + outcome.Issue.Add(new OperationOutcome.IssueComponent() |
| 105 | + { |
| 106 | + Code = OperationOutcome.IssueType.Informational, |
| 107 | + Severity = OperationOutcome.IssueSeverity.Information, |
| 108 | + Details = new CodeableConcept(null, null, "Multiple 'url' parameters provided, using the first one") |
| 109 | + }); |
| 110 | + } |
| 111 | + var val = urlParams.FirstOrDefault().Value; |
| 112 | + if (val == null) |
| 113 | + { |
| 114 | + outcome.Issue.Add(new OperationOutcome.IssueComponent() |
| 115 | + { |
| 116 | + Code = OperationOutcome.IssueType.Required, |
| 117 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 118 | + Details = new CodeableConcept(null, null, "Parameter 'url' value is missing") |
| 119 | + }); |
| 120 | + } |
| 121 | + else |
| 122 | + { |
| 123 | + if (!(val is FhirString || val is FhirUri)) |
| 124 | + { |
| 125 | + outcome.Issue.Add(new OperationOutcome.IssueComponent() |
| 126 | + { |
| 127 | + Code = OperationOutcome.IssueType.Informational, |
| 128 | + Severity = OperationOutcome.IssueSeverity.Information, |
| 129 | + Details = new CodeableConcept(null, null, $"'url' parameters provided as {val.TypeName}, uri is defined in the specification") |
| 130 | + }); |
| 131 | + } |
| 132 | + if (val is PrimitiveType p) |
| 133 | + url = p.ToString(); |
| 134 | + } |
| 135 | + } |
| 136 | + if (!urlParams.Any()) |
| 137 | + { |
| 138 | + outcome.Issue.Add(new OperationOutcome.IssueComponent() |
| 139 | + { |
| 140 | + Code = OperationOutcome.IssueType.Required, |
| 141 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 142 | + Details = new CodeableConcept(null, null, "Required parmeter 'url' missing") |
| 143 | + }); |
| 144 | + } |
| 145 | + |
| 146 | + // read the status parameter(s) |
| 147 | + var statusParams = operationParameters.Parameter.Where(p => p.Name?.ToLower() == "status"); |
| 148 | + if (statusParams.Any()) |
| 149 | + { |
| 150 | + foreach (var value in statusParams.Select(p => p.Value)) |
| 151 | + { |
| 152 | + string psv = value.ToString(); |
| 153 | + if (value is FhirUri code) |
| 154 | + psv = code.Value; |
| 155 | + else if (value is FhirString str) |
| 156 | + psv = str.Value; |
| 157 | + if (!string.IsNullOrEmpty(psv)) |
| 158 | + { |
| 159 | + statuses.AddRange(psv.Split(',')); |
| 160 | + } |
| 161 | + } |
| 162 | + // validate status value is in enumeration |
| 163 | + foreach (var psv in statuses) |
| 164 | + { |
| 165 | + PublicationStatus? ps = EnumUtility.ParseLiteral<PublicationStatus>(psv); |
| 166 | + if (!ps.HasValue) |
| 167 | + { |
| 168 | + outcome.Issue.Add(new OperationOutcome.IssueComponent() |
| 169 | + { |
| 170 | + Code = OperationOutcome.IssueType.Invalid, |
| 171 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 172 | + Details = new CodeableConcept(null, null, $"Invalid 'status' parameter value [{psv}]") |
| 173 | + }); |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + // return the error if one was detected |
| 179 | + if (!outcome.Success) |
| 180 | + { |
| 181 | + outcome.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest); |
| 182 | + return outcome; |
| 183 | + } |
| 184 | + |
| 185 | + // Search for the resources using this canonical URL |
| 186 | + var kvps = new List<KeyValuePair<string, string>>(); |
| 187 | + kvps.Add(new KeyValuePair<string, string>("url", url)); |
| 188 | + if (statuses.Any()) |
| 189 | + kvps.Add(new KeyValuePair<string, string>("status", string.Join(",", statuses))); |
| 190 | + var bundle = await Search(kvps, null, summary, null); |
| 191 | + if (!bundle.Entry.Any()) |
| 192 | + { |
| 193 | + outcome.Issue.Insert(0, new OperationOutcome.IssueComponent |
| 194 | + { |
| 195 | + Code = OperationOutcome.IssueType.NotFound, |
| 196 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 197 | + Details = new CodeableConcept(null, null, $"Canonical URL '{url}' was not found") |
| 198 | + }); |
| 199 | + outcome.SetAnnotation(HttpStatusCode.NotFound); |
| 200 | + return outcome; |
| 201 | + } |
| 202 | + |
| 203 | + // use the Canonical helper function to locate the current one |
| 204 | + var ivrs = bundle.Entry.Select(e => e.Resource as IVersionableConformanceResource).Where(e => e != null); |
| 205 | + var result = CurrentCanonical.Current(ivrs); |
| 206 | + if (result != null) |
| 207 | + { |
| 208 | + return result as Resource; |
| 209 | + } |
| 210 | + |
| 211 | + // Could not evaluate the current version |
| 212 | + outcome.Issue.Insert(0, new OperationOutcome.IssueComponent |
| 213 | + { |
| 214 | + Code = OperationOutcome.IssueType.Processing, |
| 215 | + Severity = OperationOutcome.IssueSeverity.Error, |
| 216 | + Details = new CodeableConcept(null, null, $"Canonical URL '{url}' could not be calculated between versions {string.Join(",", ivrs.Select(i => i.Version))}") |
| 217 | + }); |
| 218 | + outcome.SetAnnotation(HttpStatusCode.Ambiguous); |
| 219 | + return outcome; |
| 220 | + } |
| 221 | + } |
| 222 | +} |
0 commit comments