/// <summary>Process the operation topic list R4.</summary> /// <param name="response">[in,out] The response.</param> internal static void ProcessOperationTopicList( ref HttpResponseMessage response) { Parameters topics = new Parameters(); foreach (string topicUrl in SubscriptionTopicManager.GetTopicUrlList()) { topics.Add("subscription-topic-canonical", new Canonical(topicUrl)); } ProcessorUtils.SerializeR4(ref response, topics); }
/// <summary>Topic list to r 5.</summary> /// <param name="t4">The R4 topic list (Parameters object)</param> /// <returns>A List<r5.SubscriptionTopic>.</returns> public static List <r5.SubscriptionTopic> TopicListToR5(r4.Parameters t4) { List <r5.SubscriptionTopic> t5List = new List <r5.SubscriptionTopic>(); if (t4 == null) { return(t5List); } foreach (r4.Parameters.ParameterComponent param in t4.Parameter) { if ((param.Name == ParameterTopicName) && (param.Value != null)) { string topicUrl = string.Empty; switch (param.Value) { case Canonical canonical: topicUrl = canonical.Value; break; case FhirUri fhirUri: topicUrl = fhirUri.Value; break; case FhirUrl fhirUrl: topicUrl = fhirUrl.Value; break; case FhirString fhirString: topicUrl = fhirString.Value; break; } if (string.IsNullOrEmpty(topicUrl)) { continue; } if (!SubscriptionTopicManager.TryGetTopic(topicUrl, out r5.SubscriptionTopic t5)) { Console.WriteLine($"Unknown R5 SubscriptionTopic: {topicUrl}"); return(null); } t5List.Add(t5); } } return(t5List); }
/// <summary>Main entry-point for this application.</summary> /// <param name="args">An array of command-line argument strings.</param> public static void Main(string[] args) { // setup our configuration (command line > environment > appsettings.json) Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build() ; // update configuration to make sure listen url is properly formatted Regex regex = new Regex(_regexBaseUrlMatch); Match match = regex.Match(Configuration["Server_Public_Url"]); Configuration["Server_Public_Url"] = match.ToString(); PublicUrl = match.ToString(); match = regex.Match(Configuration["Server_Internal_Url"]); Configuration["Server_Internal_Url"] = match.ToString(); // update external urls to make sure the DO have trailing slashes if (!Configuration["Server_FHIR_Url_R4"].EndsWith('/')) { Configuration["Server_FHIR_Url_R4"] = Configuration["Server_FHIR_Url_R4"] + '/'; } FhirServerUrlR4 = Configuration["Server_FHIR_Url_R4"]; // update external urls to make sure the DO have trailing slashes if (!Configuration["Server_FHIR_Url_R5"].EndsWith('/')) { Configuration["Server_FHIR_Url_R5"] = Configuration["Server_FHIR_Url_R5"] + '/'; } FhirServerUrlR5 = Configuration["Server_FHIR_Url_R5"]; // create our REST client RestClient = new HttpClient(); // initialize managers SubscriptionTopicManager.Init(); SubscriptionManagerR4.Init(); SubscriptionManagerR5.Init(); WebsocketManager.Init(); // create our service host CreateHostBuilder(args).Build().StartAsync(); // create our web host CreateWebHostBuilder(args).Build().Run(); }
/// <summary>Converts a different FHIR version SubscriptionTopic to R5.</summary> /// <param name="t4">The R4 topic (canonical URL).</param> /// <returns>R5 SubscriptionTopic.</returns> public static r5.SubscriptionTopic TopicToR5(string t4) { if (t4 == null) { return(null); } if (!SubscriptionTopicManager.TryGetTopic(t4, out r5.SubscriptionTopic t5)) { Console.WriteLine($"Unknown R5 SubscriptionTopic: {t4}"); return(null); } return(t5); }
/// <summary>Process the request.</summary> /// <param name="appInner"> The application inner.</param> /// <param name="fhirServerUrl">URL of the fhir server.</param> public static void ProcessRequest(IApplicationBuilder appInner, string fhirServerUrl) { string serialized; // run the proxy for this request appInner.RunProxy(async context => { // look for a FHIR server header if (context.Request.Headers.ContainsKey(Program._proxyHeaderKey) && (context.Request.Headers[Program._proxyHeaderKey].Count > 0)) { fhirServerUrl = context.Request.Headers[Program._proxyHeaderKey][0]; } // create some response objects HttpResponseMessage response = new HttpResponseMessage(); StringContent localResponse; r5s.FhirJsonSerializer serializer = new r5s.FhirJsonSerializer(); // default to returning the representation if not specified string preferredResponse = "return=representation"; // check for headers we are interested int foreach (KeyValuePair <string, StringValues> kvp in context.Request.Headers) { if (kvp.Key.ToLowerInvariant() == "prefer") { preferredResponse = kvp.Value; } } // act depending on request type switch (context.Request.Method.ToUpperInvariant()) { case "GET": // check for an ID string requestUrl = context.Request.Path; if (requestUrl.EndsWith('/')) { requestUrl = requestUrl.Substring(0, requestUrl.Length - 1); } string id = requestUrl.Substring(requestUrl.LastIndexOf('/') + 1); // check for a subscription if (SubscriptionManager.TryGetBasicSerialized(id, out serialized)) { // build our response response.Content = new StringContent( serialized, Encoding.UTF8, "application/fhir+json"); response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/fhir+json"); response.StatusCode = System.Net.HttpStatusCode.OK; // done return(response); } // check for a topic if (SubscriptionTopicManager.TryGetBasicSerialized(id, out serialized)) { // build our response response.Content = new StringContent( serialized, Encoding.UTF8, "application/fhir+json"); response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/fhir+json"); response.StatusCode = System.Net.HttpStatusCode.OK; // done return(response); } // look for query parameters for a search we are interested in if (context.Request.Query.ContainsKey("code")) { // check for topic if (context.Request.Query["code"] == "R5SubscriptionTopic") { // serialize the bundle of topics response.Content = new StringContent( serializer.SerializeToString(SubscriptionTopicManager.GetTopicsBundle(true))); response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/fhir+json"); response.StatusCode = System.Net.HttpStatusCode.OK; return(response); } // check for basic if (context.Request.Query["code"] == "R5Subscription") { // serialize the bundle of subscriptions response.Content = new StringContent( serializer.SerializeToString(SubscriptionManager.GetSubscriptionsBundle(true))); response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/fhir+json"); response.StatusCode = System.Net.HttpStatusCode.OK; return(response); } } break; case "PUT": // don't deal with PUT yet response.StatusCode = System.Net.HttpStatusCode.NotImplemented; return(response); case "POST": try { // grab the message body to look at System.IO.StreamReader requestReader = new System.IO.StreamReader(context.Request.Body); string requestContent = requestReader.ReadToEndAsync().Result; r5s.FhirJsonParser parser = new r5s.FhirJsonParser(); r5.Basic basic = parser.Parse <r5.Basic>(requestContent); // check to see if this is a something we are interested in if ((basic.Code != null) && (basic.Code.Coding != null) && basic.Code.Coding.Any()) { // look for codes we want foreach (Coding coding in basic.Code.Coding) { if (coding.System.Equals("http://hl7.org/fhir/resource-types", StringComparison.Ordinal) && coding.Code.Equals("R5SubscriptionTopic", StringComparison.Ordinal)) { // posting topics is not yet implemented response.StatusCode = HttpStatusCode.NotImplemented; return(response); } if (coding.System.Equals("http://hl7.org/fhir/resource-types", StringComparison.Ordinal) && coding.Code.Equals("R5Subscription", StringComparison.Ordinal)) { // check for having the required resource if ((basic.Extension == null) || (!basic.Extension.Any()) || (!basic.Extension[0].Url.Equals("http://hl7.org/fhir/StructureDefinition/json-embedded-resource", StringComparison.Ordinal))) { response.StatusCode = System.Net.HttpStatusCode.BadRequest; return(response); } // check to see if the manager does anything with this text SubscriptionManager.HandlePost( basic.Extension[0].Value.ToString(), out r5.Subscription subscription, out HttpStatusCode statusCode, out string failureContent, true); // check for errors if (statusCode != HttpStatusCode.Created) { switch (preferredResponse) { case "return=minimal": localResponse = new StringContent(string.Empty, Encoding.UTF8, "text/plain"); break; case "return=OperationOutcome": r5.OperationOutcome outcome = new r5.OperationOutcome() { Id = Guid.NewGuid().ToString(), Issue = new List <r5.OperationOutcome.IssueComponent>() { new r5.OperationOutcome.IssueComponent() { Severity = r5.OperationOutcome.IssueSeverity.Error, Code = r5.OperationOutcome.IssueType.Unknown, Diagnostics = failureContent, }, }, }; localResponse = new StringContent( serializer.SerializeToString(outcome), Encoding.UTF8, "application/fhir+json"); break; default: localResponse = new StringContent(failureContent, Encoding.UTF8, "text/plain"); break; } response.Content = localResponse; response.StatusCode = statusCode; return(response); } // figure out our link to this resource string url = Program.UrlForResourceId("Basic", subscription.Id); switch (preferredResponse) { case "return=minimal": localResponse = new StringContent(string.Empty, Encoding.UTF8, "text/plain"); break; case "return=OperationOutcome": r5.OperationOutcome outcome = new r5.OperationOutcome() { Id = Guid.NewGuid().ToString(), Issue = new List <r5.OperationOutcome.IssueComponent>() { new r5.OperationOutcome.IssueComponent() { Severity = r5.OperationOutcome.IssueSeverity.Information, Code = r5.OperationOutcome.IssueType.Value, }, }, }; localResponse = new StringContent( serializer.SerializeToString(outcome), Encoding.UTF8, "application/fhir+json"); break; default: localResponse = new StringContent( serializer.SerializeToString(SubscriptionManager.WrapInBasic(subscription)), Encoding.UTF8, "application/fhir+json"); break; } response.Headers.Add("Location", url); response.Headers.Add("Access-Control-Expose-Headers", "Location,ETag"); response.Content = localResponse; response.StatusCode = HttpStatusCode.Created; return(response); } } } } catch (Exception) { } break; case "DELETE": try { // check to see if this is a subscription if (SubscriptionManager.HandleDelete(context.Request)) { // deleted response.StatusCode = System.Net.HttpStatusCode.NoContent; return(response); } // fall through to proxy } catch (Exception) { } break; } // if we're still here, proxy this call ForwardContext proxiedContext = context.ForwardTo(fhirServerUrl); // send to server and await response return(await proxiedContext.Send().ConfigureAwait(false)); }); }
/// <summary>Converts a different FHIR version subscription to R5.</summary> /// <param name="s4">The subscription.</param> /// <returns>R5 Subscription.</returns> public static r5.Subscription SubscriptionToR5(r4.Subscription s4) { if (s4 == null) { return(null); } r5.Subscription s5 = new r5.Subscription() { Id = s4.Id, ImplicitRules = s4.ImplicitRules, Language = s4.Language, End = s4.End, Reason = s4.Reason, Endpoint = s4.Channel.Endpoint, Header = s4.Channel.Header, ContentType = s4.Channel.Payload, }; if (s4.Meta != null) { s5.Meta = new Meta(); if (s4.Meta.LastUpdated != null) { s5.Meta.LastUpdated = s4.Meta.LastUpdated; } if (s4.Meta.Profile != null) { s5.Meta.Profile = s4.Meta.Profile; } if (s4.Meta.Security != null) { s5.Meta.Security = new List <Coding>(); foreach (Coding c4 in s4.Meta.Security) { s5.Meta.Security.Add((Coding)c4.DeepCopy()); } } if (s4.Meta.Source != null) { s5.Meta.Source = s4.Meta.Source; } if (s4.Meta.Tag != null) { s5.Meta.Tag = new List <Coding>(); foreach (Coding c4 in s4.Meta.Tag) { s5.Meta.Tag.Add((Coding)c4.DeepCopy()); } } } switch (s4.Status) { case r4.Subscription.SubscriptionStatus.Active: s5.Status = r5.SubscriptionState.Active; break; case r4.Subscription.SubscriptionStatus.Error: s5.Status = r5.SubscriptionState.Error; break; case r4.Subscription.SubscriptionStatus.Off: s5.Status = r5.SubscriptionState.Off; break; case r4.Subscription.SubscriptionStatus.Requested: s5.Status = r5.SubscriptionState.Requested; break; default: Console.WriteLine($"Invalid R4 Subscription.Status: {s4.Status}"); return(null); } switch (s4.Channel.Type) { case r4.Subscription.SubscriptionChannelType.Email: s5.ChannelType = new Coding() { System = CanonicalChannelType, Code = "email", }; break; case r4.Subscription.SubscriptionChannelType.Message: s5.ChannelType = new Coding() { System = CanonicalChannelType, Code = "message", }; break; case r4.Subscription.SubscriptionChannelType.RestHook: s5.ChannelType = new Coding() { System = CanonicalChannelType, Code = "rest-hook", }; break; case r4.Subscription.SubscriptionChannelType.Websocket: s5.ChannelType = new Coding() { System = CanonicalChannelType, Code = "websocket", }; break; default: Console.WriteLine($"Invalid R4 Subscription.Channel.Type: {s4.Channel.Type}"); return(null); } if ((s4.Extension == null) || (s4.Extension.Count == 0)) { return(null); } foreach (Extension ext in s4.Extension) { if (ext.Url == ExtensionUrlTopic) { if (ext.Value is FhirUri) { s5.Topic = new ResourceReference(((FhirUri)ext.Value).Value); break; } else if (ext.Value is Canonical) { s5.Topic = new ResourceReference(((Canonical)ext.Value).Value); break; } } } if (s5.Topic == null) { #pragma warning disable CA1303 // Do not pass literals as localized parameters Console.WriteLine($"R4 Subscription requires Extension: {ExtensionUrlTopic}"); #pragma warning restore CA1303 // Do not pass literals as localized parameters return(null); } if (!SubscriptionTopicManager.TryGetTopic(s5.Topic.Reference, out r5.SubscriptionTopic topic)) { Console.WriteLine($"Unknown R4 SubscriptionTopic: {s5.Topic.Reference}"); return(null); } if (topic.ResourceTrigger == null) { Console.WriteLine($"SubscriptionTopic: {topic.Url} requires resourceTrigger"); return(null); } if ((topic.ResourceTrigger.ResourceType == null) || (!topic.ResourceTrigger.ResourceType.Any())) { Console.WriteLine($"SubscriptionTopic: {topic.Url} requires resourceTrigger.resourceType"); return(null); } if (s4.Channel.Extension != null) { foreach (Extension ext in s4.Channel.Extension) { switch (ext.Url) { case ExtensionUrlHeartbeat: if ((ext.Value != null) && (ext.Value is UnsignedInt)) { s5.HeartbeatPeriod = ((UnsignedInt)ext.Value).Value; } break; case ExtensionUrlTimeout: if ((ext.Value != null) && (ext.Value is UnsignedInt)) { s5.Timeout = ((UnsignedInt)ext.Value).Value; } break; case ExtensionNotificationUrlLocation: if ((ext.Value != null) && (ext.Value is Code)) { // TODO: Need December 2020 R5 build } break; case ExtensionMaxCount: if ((ext.Value != null) && (ext.Value is PositiveInt)) { // TODO: Need December 2020 R5 Build } break; } } } if ((s4.Channel.PayloadElement.Extension != null) && (s4.Channel.PayloadElement.Extension.Count > 0)) { foreach (Extension ext in s4.Channel.PayloadElement.Extension) { if ((ext.Url == ExtensionUrlContent) && (ext.Value is Code)) { switch (((Code)ext.Value).Value) { case "empty": s5.Content = r5.Subscription.SubscriptionPayloadContent.Empty; break; case "id-only": s5.Content = r5.Subscription.SubscriptionPayloadContent.IdOnly; break; case "full-resource": s5.Content = r5.Subscription.SubscriptionPayloadContent.FullResource; break; default: Console.WriteLine($"Invalid R4 Subscription.Channel.Payload Content: {((Code)ext.Value).Value}"); return(null); } } } } if (!string.IsNullOrEmpty(s4.Criteria)) { string criteria = s4.Criteria; string resource = topic.ResourceTrigger.ResourceType.First().Value.ToString(); if (!criteria.StartsWith(resource, StringComparison.Ordinal)) { Console.WriteLine( $"R4 Subscription Criteria: {criteria}" + $" must match SubscriptionTopic.resourceTrigger.resourceType: {resource}"); return(null); } // remove initial resource type plus the ? criteria = criteria.Substring(resource.Length + 1); string[] components = criteria.Split('&'); if (components.Length > 0) { s5.FilterBy = new List <r5.Subscription.FilterByComponent>(); } foreach (string component in components) { if (TryExpandFilter(component, out r5.Subscription.FilterByComponent filter)) { s5.FilterBy.Add(filter); } } } return(s5); }
/// <summary>Converts an R5 subscription to another FHIR version.</summary> /// <param name="s5">The subscription.</param> /// <returns>The desired version of a subscription.</returns> public static r4.Subscription SubscriptionFromR5(r5.Subscription s5) { if (s5 == null) { return(null); } if (s5.Topic == null) { return(null); } if (!SubscriptionTopicManager.TryGetTopic(s5.Topic.Reference, out r5.SubscriptionTopic topic)) { Console.WriteLine($"Unknown R5 SubscriptionTopic: {s5.Topic.Reference}"); return(null); } if (topic.ResourceTrigger == null) { Console.WriteLine($"SubscriptionTopic: {topic.Url} requires resourceTrigger"); return(null); } if ((topic.ResourceTrigger.ResourceType == null) || (!topic.ResourceTrigger.ResourceType.Any())) { Console.WriteLine($"SubscriptionTopic: {topic.Url} requires resourceTrigger.resourceType"); return(null); } r4.Subscription s4 = new r4.Subscription() { Id = s5.Id, ImplicitRules = s5.ImplicitRules, Language = s5.Language, End = s5.End, Reason = s5.Reason, Channel = new r4.Subscription.ChannelComponent() { Endpoint = s5.Endpoint, Header = s5.Header, Payload = s5.ContentType, }, }; if (s5.Meta != null) { s4.Meta = new Meta(); if (s5.Meta.LastUpdated != null) { s4.Meta.LastUpdated = s5.Meta.LastUpdated; } if (s5.Meta.Profile != null) { s4.Meta.Profile = s5.Meta.Profile; } if (s5.Meta.Security != null) { s4.Meta.Security = new List <Coding>(); foreach (Coding c5 in s5.Meta.Security) { s4.Meta.Security.Add((Coding)c5.DeepCopy()); } } if (s5.Meta.Source != null) { s4.Meta.Source = s5.Meta.Source; } if (s5.Meta.Tag != null) { s4.Meta.Tag = new List <Coding>(); foreach (Coding c5 in s5.Meta.Tag) { s4.Meta.Tag.Add((Coding)c5.DeepCopy()); } } } switch (s5.Status) { case r5.SubscriptionState.Active: s4.Status = r4.Subscription.SubscriptionStatus.Active; break; case r5.SubscriptionState.Error: s4.Status = r4.Subscription.SubscriptionStatus.Error; break; case r5.SubscriptionState.Off: s4.Status = r4.Subscription.SubscriptionStatus.Off; break; case r5.SubscriptionState.Requested: s4.Status = r4.Subscription.SubscriptionStatus.Requested; break; } switch (s5.ChannelType.Code) { case "email": s4.Channel.Type = r4.Subscription.SubscriptionChannelType.Email; break; case "message": s4.Channel.Type = r4.Subscription.SubscriptionChannelType.Message; break; case "rest-hook": s4.Channel.Type = r4.Subscription.SubscriptionChannelType.RestHook; break; case "websocket": s4.Channel.Type = r4.Subscription.SubscriptionChannelType.Websocket; break; } s4.Extension.Add(new Extension() { Url = ExtensionUrlTopic, Value = new FhirUri(s5.Topic.Url), }); if (s5.HeartbeatPeriod != null) { s4.Channel.Extension.Add(new Extension() { Url = ExtensionUrlHeartbeat, Value = new UnsignedInt(s5.HeartbeatPeriod), }); } if (s5.Timeout != null) { s4.Channel.Extension.Add(new Extension() { Url = ExtensionUrlTimeout, Value = new UnsignedInt(s5.Timeout), }); } switch (s5.Content) { case r5.Subscription.SubscriptionPayloadContent.Empty: s4.Channel.PayloadElement.Extension = new List <Extension>() { new Extension() { Url = ExtensionUrlContent, Value = new Code("empty"), }, }; break; case r5.Subscription.SubscriptionPayloadContent.IdOnly: s4.Channel.PayloadElement.Extension = new List <Extension>() { new Extension() { Url = ExtensionUrlContent, Value = new Code("id-only"), }, }; break; case r5.Subscription.SubscriptionPayloadContent.FullResource: s4.Channel.PayloadElement.Extension = new List <Extension>() { new Extension() { Url = ExtensionUrlContent, Value = new Code("full-resource"), }, }; break; } if ((s5.FilterBy != null) && (s5.FilterBy.Count > 0)) { StringBuilder sb = new StringBuilder(); sb.Append($"{topic.ResourceTrigger.ResourceType.First().Value}"); int addedCount = 0; foreach (r5.Subscription.FilterByComponent filter in s5.FilterBy) { if (TryCondenseFilter(filter, out string value)) { if (addedCount++ == 0) { sb.Append('?'); } else { sb.Append('&'); } sb.Append(value); } } s4.Criteria = sb.ToString(); } // TODO: Need December 2020 R5 build to add NotificationUrlLocation s4.Channel.AddExtension(ExtensionNotificationUrlLocation, new Code("full-url")); // TODO: Need December 2020 R5 build to add MaxCount s4.Channel.AddExtension(ExtensionMaxCount, new PositiveInt(10)); return(s4); }
/// <summary>Process the get.</summary> /// <param name="context"> [in,out] The context.</param> /// <param name="response">[in,out] The response message.</param> internal static void ProcessGet(ref HttpContext context, ref HttpResponseMessage response) { ProcessorUtils.SerializeR5(ref response, SubscriptionTopicManager.GetTopicsBundle()); }