001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.subscription.match.registry; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.model.entity.StorageSettings; 027import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; 028import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; 029import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; 030import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscription; 031import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter; 032import ca.uhn.fhir.model.api.BasePrimitive; 033import ca.uhn.fhir.model.api.ExtensionDt; 034import ca.uhn.fhir.model.dstu2.resource.Subscription; 035import ca.uhn.fhir.model.primitive.BooleanDt; 036import ca.uhn.fhir.rest.api.Constants; 037import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 038import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 039import ca.uhn.fhir.subscription.SubscriptionConstants; 040import ca.uhn.fhir.util.HapiExtensions; 041import ca.uhn.fhir.util.SubscriptionUtil; 042import jakarta.annotation.Nonnull; 043import jakarta.annotation.Nullable; 044import org.hl7.fhir.exceptions.FHIRException; 045import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 046import org.hl7.fhir.instance.model.api.IBaseMetaType; 047import org.hl7.fhir.instance.model.api.IBaseReference; 048import org.hl7.fhir.instance.model.api.IBaseResource; 049import org.hl7.fhir.instance.model.api.IPrimitiveType; 050import org.hl7.fhir.r4.model.BooleanType; 051import org.hl7.fhir.r4.model.Extension; 052import org.hl7.fhir.r5.model.Enumerations; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055import org.springframework.beans.factory.annotation.Autowired; 056 057import java.util.Collections; 058import java.util.HashMap; 059import java.util.List; 060import java.util.Map; 061import java.util.stream.Collectors; 062 063import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; 064import static java.util.Objects.nonNull; 065import static java.util.stream.Collectors.mapping; 066import static java.util.stream.Collectors.toList; 067 068public class SubscriptionCanonicalizer { 069 private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class); 070 071 final FhirContext myFhirContext; 072 private final StorageSettings myStorageSettings; 073 074 @Autowired 075 public SubscriptionCanonicalizer(FhirContext theFhirContext, StorageSettings theStorageSettings) { 076 myFhirContext = theFhirContext; 077 myStorageSettings = theStorageSettings; 078 } 079 080 // TODO: LD: remove this constructor once all callers call the 2 arg constructor above 081 082 /** 083 * @deprecated All callers should invoke {@link SubscriptionCanonicalizer()} instead. 084 */ 085 @Deprecated 086 public SubscriptionCanonicalizer(FhirContext theFhirContext) { 087 myFhirContext = theFhirContext; 088 myStorageSettings = new StorageSettings(); 089 } 090 091 public CanonicalSubscription canonicalize(IBaseResource theSubscription) { 092 switch (myFhirContext.getVersion().getVersion()) { 093 case DSTU2: 094 return canonicalizeDstu2(theSubscription); 095 case DSTU3: 096 return canonicalizeDstu3(theSubscription); 097 case R4: 098 return canonicalizeR4(theSubscription); 099 case R4B: 100 return canonicalizeR4B(theSubscription); 101 case R5: 102 return canonicalizeR5(theSubscription); 103 case DSTU2_HL7ORG: 104 case DSTU2_1: 105 default: 106 throw new ConfigurationException(Msg.code(556) + "Subscription not supported for version: " 107 + myFhirContext.getVersion().getVersion()); 108 } 109 } 110 111 private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) { 112 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = 113 (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 114 CanonicalSubscription retVal = new CanonicalSubscription(); 115 try { 116 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus())); 117 retVal.setChannelType(getChannelType(theSubscription)); 118 retVal.setCriteriaString(subscription.getCriteria()); 119 Subscription.Channel channel = subscription.getChannel(); 120 retVal.setEndpointUrl(channel.getEndpoint()); 121 retVal.setHeaders(channel.getHeader()); 122 retVal.setChannelExtensions(extractExtension(subscription)); 123 retVal.setIdElement(subscription.getIdElement()); 124 retVal.setPayloadString(channel.getPayload()); 125 retVal.setTags(extractTags(subscription)); 126 handleCrossPartition(theSubscription, retVal); 127 retVal.setSendDeleteMessages(extractDeleteExtensionDstu2(subscription)); 128 } catch (FHIRException theE) { 129 throw new InternalErrorException(Msg.code(557) + theE); 130 } 131 return retVal; 132 } 133 134 private boolean extractDeleteExtensionDstu2(ca.uhn.fhir.model.dstu2.resource.Subscription theSubscription) { 135 return theSubscription.getChannel().getUndeclaredExtensionsByUrl(EX_SEND_DELETE_MESSAGES).stream() 136 .map(ExtensionDt::getValue) 137 .map(value -> (BooleanDt) value) 138 .map(BasePrimitive::getValue) 139 .findFirst() 140 .orElse(false); 141 } 142 143 /** 144 * Extract the meta tags from the subscription and convert them to a simple string map. 145 * 146 * @param theSubscription The subscription to extract the tags from 147 * @return A map of tags System:Code 148 */ 149 private Map<String, String> extractTags(IBaseResource theSubscription) { 150 Map<String, String> retVal = new HashMap<>(); 151 theSubscription.getMeta().getTag().stream() 152 .filter(t -> t.getSystem() != null && t.getCode() != null) 153 .forEach(t -> retVal.put(t.getSystem(), t.getCode())); 154 return retVal; 155 } 156 157 private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) { 158 org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 159 160 CanonicalSubscription retVal = new CanonicalSubscription(); 161 try { 162 org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus status = subscription.getStatus(); 163 if (status != null) { 164 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 165 } 166 setPartitionIdOnReturnValue(theSubscription, retVal); 167 retVal.setChannelType(getChannelType(theSubscription)); 168 retVal.setCriteriaString(subscription.getCriteria()); 169 org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); 170 retVal.setEndpointUrl(channel.getEndpoint()); 171 retVal.setHeaders(channel.getHeader()); 172 retVal.setChannelExtensions(extractExtension(subscription)); 173 retVal.setIdElement(subscription.getIdElement()); 174 retVal.setPayloadString(channel.getPayload()); 175 retVal.setPayloadSearchCriteria( 176 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 177 retVal.setTags(extractTags(subscription)); 178 handleCrossPartition(theSubscription, retVal); 179 180 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 181 String from; 182 String subjectTemplate; 183 184 try { 185 from = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 186 subjectTemplate = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 187 } catch (FHIRException theE) { 188 throw new ConfigurationException( 189 Msg.code(558) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 190 } 191 retVal.getEmailDetails().setFrom(from); 192 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 193 } 194 195 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 196 197 String stripVersionIds; 198 String deliverLatestVersion; 199 try { 200 stripVersionIds = 201 channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 202 deliverLatestVersion = 203 channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 204 } catch (FHIRException theE) { 205 throw new ConfigurationException( 206 Msg.code(559) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 207 } 208 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 209 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 210 } 211 retVal.setSendDeleteMessages(extractSendDeletesDstu3(subscription)); 212 213 } catch (FHIRException theE) { 214 throw new InternalErrorException(Msg.code(560) + theE); 215 } 216 return retVal; 217 } 218 219 private Boolean extractSendDeletesDstu3(org.hl7.fhir.dstu3.model.Subscription subscription) { 220 return subscription.getChannel().getExtensionsByUrl(EX_SEND_DELETE_MESSAGES).stream() 221 .map(org.hl7.fhir.dstu3.model.Extension::getValue) 222 .filter(val -> val instanceof org.hl7.fhir.dstu3.model.BooleanType) 223 .map(val -> (org.hl7.fhir.dstu3.model.BooleanType) val) 224 .map(org.hl7.fhir.dstu3.model.BooleanType::booleanValue) 225 .findFirst() 226 .orElse(false); 227 } 228 229 private @Nonnull Map<String, List<String>> extractExtension(IBaseResource theSubscription) { 230 try { 231 switch (theSubscription.getStructureFhirVersionEnum()) { 232 case DSTU2: { 233 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = 234 (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 235 return subscription.getChannel().getUndeclaredExtensions().stream() 236 .collect(Collectors.groupingBy( 237 t -> t.getUrl(), 238 mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 239 } 240 case DSTU3: { 241 org.hl7.fhir.dstu3.model.Subscription subscription = 242 (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 243 return subscription.getChannel().getExtension().stream() 244 .collect(Collectors.groupingBy( 245 t -> t.getUrl(), 246 mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 247 } 248 case R4: { 249 org.hl7.fhir.r4.model.Subscription subscription = 250 (org.hl7.fhir.r4.model.Subscription) theSubscription; 251 return subscription.getChannel().getExtension().stream() 252 .collect(Collectors.groupingBy( 253 t -> t.getUrl(), 254 mapping( 255 t -> { 256 return t.getValueAsPrimitive().getValueAsString(); 257 }, 258 toList()))); 259 } 260 case R5: { 261 // TODO KHS fix org.hl7.fhir.r4b.model.BaseResource.getStructureFhirVersionEnum() for R4B 262 if (theSubscription instanceof org.hl7.fhir.r4b.model.Subscription) { 263 org.hl7.fhir.r4b.model.Subscription subscription = 264 (org.hl7.fhir.r4b.model.Subscription) theSubscription; 265 return subscription.getExtension().stream() 266 .collect(Collectors.groupingBy( 267 t -> t.getUrl(), 268 mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 269 } else if (theSubscription instanceof org.hl7.fhir.r5.model.Subscription) { 270 org.hl7.fhir.r5.model.Subscription subscription = 271 (org.hl7.fhir.r5.model.Subscription) theSubscription; 272 return subscription.getExtension().stream() 273 .collect(Collectors.groupingBy( 274 t -> t.getUrl(), 275 mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 276 } 277 } 278 case DSTU2_HL7ORG: 279 case DSTU2_1: 280 default: { 281 ourLog.error( 282 "Failed to extract extension from subscription {}", 283 theSubscription.getIdElement().toUnqualified().getValue()); 284 break; 285 } 286 } 287 } catch (FHIRException theE) { 288 ourLog.error( 289 "Failed to extract extension from subscription {}", 290 theSubscription.getIdElement().toUnqualified().getValue(), 291 theE); 292 } 293 return Collections.emptyMap(); 294 } 295 296 private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) { 297 org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; 298 CanonicalSubscription retVal = new CanonicalSubscription(); 299 retVal.setStatus(subscription.getStatus()); 300 org.hl7.fhir.r4.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); 301 retVal.setHeaders(channel.getHeader()); 302 retVal.setChannelExtensions(extractExtension(subscription)); 303 retVal.setIdElement(subscription.getIdElement()); 304 retVal.setPayloadString(channel.getPayload()); 305 retVal.setPayloadSearchCriteria( 306 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 307 retVal.setTags(extractTags(subscription)); 308 setPartitionIdOnReturnValue(theSubscription, retVal); 309 handleCrossPartition(theSubscription, retVal); 310 311 List<org.hl7.fhir.r4.model.CanonicalType> profiles = 312 subscription.getMeta().getProfile(); 313 for (org.hl7.fhir.r4.model.CanonicalType next : profiles) { 314 if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) { 315 retVal.setTopicSubscription(true); 316 } 317 } 318 319 if (retVal.isTopicSubscription()) { 320 CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription(); 321 topicSubscription.setTopic(getCriteria(theSubscription)); 322 323 retVal.setEndpointUrl(channel.getEndpoint()); 324 retVal.setChannelType(getChannelType(subscription)); 325 326 for (org.hl7.fhir.r4.model.Extension next : 327 subscription.getCriteriaElement().getExtension()) { 328 if (SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL.equals(next.getUrl())) { 329 List<CanonicalTopicSubscriptionFilter> filters = CanonicalTopicSubscriptionFilter.fromQueryUrl( 330 next.getValue().primitiveValue()); 331 filters.forEach(topicSubscription::addFilter); 332 } 333 } 334 335 if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL)) { 336 org.hl7.fhir.r4.model.Extension channelHeartbeatPeriotUrlExtension = channel.getExtensionByUrl( 337 SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL); 338 topicSubscription.setHeartbeatPeriod(Integer.valueOf( 339 channelHeartbeatPeriotUrlExtension.getValue().primitiveValue())); 340 } 341 if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL)) { 342 org.hl7.fhir.r4.model.Extension channelTimeoutUrlExtension = 343 channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL); 344 topicSubscription.setTimeout( 345 Integer.valueOf(channelTimeoutUrlExtension.getValue().primitiveValue())); 346 } 347 if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT)) { 348 org.hl7.fhir.r4.model.Extension channelMaxCountExtension = 349 channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT); 350 topicSubscription.setMaxCount( 351 Integer.valueOf(channelMaxCountExtension.getValue().primitiveValue())); 352 } 353 354 // setting full-resource PayloadContent if backport-payload-content is not provided 355 org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent = 356 org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE; 357 358 org.hl7.fhir.r4.model.Extension channelPayloadContentExtension = channel.getPayloadElement() 359 .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT); 360 361 if (nonNull(channelPayloadContentExtension)) { 362 payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode( 363 channelPayloadContentExtension.getValue().primitiveValue()); 364 } 365 366 topicSubscription.setContent(payloadContent); 367 } else { 368 retVal.setCriteriaString(getCriteria(theSubscription)); 369 retVal.setEndpointUrl(channel.getEndpoint()); 370 retVal.setChannelType(getChannelType(subscription)); 371 } 372 373 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 374 String from; 375 String subjectTemplate; 376 try { 377 from = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 378 subjectTemplate = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 379 } catch (FHIRException theE) { 380 throw new ConfigurationException( 381 Msg.code(561) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 382 } 383 retVal.getEmailDetails().setFrom(from); 384 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 385 } 386 387 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 388 String stripVersionIds; 389 String deliverLatestVersion; 390 try { 391 stripVersionIds = 392 channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 393 deliverLatestVersion = 394 channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 395 } catch (FHIRException theE) { 396 throw new ConfigurationException( 397 Msg.code(562) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 398 } 399 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 400 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 401 } 402 403 List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 404 if (topicExts.size() > 0) { 405 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 406 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 407 throw new PreconditionFailedException(Msg.code(563) + "Topic reference must be an EventDefinition"); 408 } 409 } 410 411 Extension extension = channel.getExtensionByUrl(EX_SEND_DELETE_MESSAGES); 412 if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) { 413 retVal.setSendDeleteMessages(((BooleanType) extension.getValue()).booleanValue()); 414 } 415 return retVal; 416 } 417 418 private CanonicalSubscription canonicalizeR4B(IBaseResource theSubscription) { 419 org.hl7.fhir.r4b.model.Subscription subscription = (org.hl7.fhir.r4b.model.Subscription) theSubscription; 420 421 CanonicalSubscription retVal = new CanonicalSubscription(); 422 org.hl7.fhir.r4b.model.Enumerations.SubscriptionStatus status = subscription.getStatus(); 423 if (status != null) { 424 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 425 } 426 setPartitionIdOnReturnValue(theSubscription, retVal); 427 org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); 428 retVal.setHeaders(channel.getHeader()); 429 retVal.setChannelExtensions(extractExtension(subscription)); 430 retVal.setIdElement(subscription.getIdElement()); 431 retVal.setPayloadString(channel.getPayload()); 432 retVal.setPayloadSearchCriteria( 433 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 434 retVal.setTags(extractTags(subscription)); 435 436 List<org.hl7.fhir.r4b.model.CanonicalType> profiles = 437 subscription.getMeta().getProfile(); 438 for (org.hl7.fhir.r4b.model.CanonicalType next : profiles) { 439 if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) { 440 retVal.setTopicSubscription(true); 441 } 442 } 443 444 if (retVal.isTopicSubscription()) { 445 CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription(); 446 topicSubscription.setTopic(getCriteria(theSubscription)); 447 448 retVal.setEndpointUrl(channel.getEndpoint()); 449 retVal.setChannelType(getChannelType(subscription)); 450 451 // setting full-resource PayloadContent if backport-payload-content is not provided 452 org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent = 453 org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE; 454 455 org.hl7.fhir.r4b.model.Extension channelPayloadContentExtension = channel.getPayloadElement() 456 .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT); 457 458 if (nonNull(channelPayloadContentExtension)) { 459 payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode( 460 channelPayloadContentExtension.getValue().primitiveValue()); 461 } 462 463 topicSubscription.setContent(payloadContent); 464 } else { 465 retVal.setCriteriaString(getCriteria(theSubscription)); 466 retVal.setEndpointUrl(channel.getEndpoint()); 467 retVal.setChannelType(getChannelType(subscription)); 468 } 469 470 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 471 String from; 472 String subjectTemplate; 473 try { 474 from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 475 subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 476 } catch (FHIRException theE) { 477 throw new ConfigurationException( 478 Msg.code(564) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 479 } 480 retVal.getEmailDetails().setFrom(from); 481 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 482 } 483 484 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 485 String stripVersionIds; 486 String deliverLatestVersion; 487 try { 488 stripVersionIds = 489 getExtensionString(channel, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 490 deliverLatestVersion = 491 getExtensionString(channel, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 492 } catch (FHIRException theE) { 493 throw new ConfigurationException( 494 Msg.code(565) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 495 } 496 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 497 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 498 } 499 500 List<org.hl7.fhir.r4b.model.Extension> topicExts = 501 subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 502 if (topicExts.size() > 0) { 503 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 504 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 505 throw new PreconditionFailedException(Msg.code(566) + "Topic reference must be an EventDefinition"); 506 } 507 } 508 509 org.hl7.fhir.r4b.model.Extension extension = channel.getExtensionByUrl(EX_SEND_DELETE_MESSAGES); 510 if (extension != null && extension.hasValue() && extension.hasValueBooleanType()) { 511 retVal.setSendDeleteMessages(extension.getValueBooleanType().booleanValue()); 512 } 513 514 handleCrossPartition(theSubscription, retVal); 515 516 return retVal; 517 } 518 519 private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) { 520 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 521 522 CanonicalSubscription retVal = new CanonicalSubscription(); 523 524 setPartitionIdOnReturnValue(theSubscription, retVal); 525 retVal.setChannelExtensions(extractExtension(subscription)); 526 retVal.setIdElement(subscription.getIdElement()); 527 retVal.setPayloadString(subscription.getContentType()); 528 retVal.setPayloadSearchCriteria( 529 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 530 retVal.setTags(extractTags(subscription)); 531 532 List<org.hl7.fhir.r5.model.Extension> topicExts = 533 subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 534 if (topicExts.size() > 0) { 535 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 536 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 537 throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition"); 538 } 539 } 540 541 // All R5 subscriptions are topic subscriptions 542 retVal.setTopicSubscription(true); 543 544 Enumerations.SubscriptionStatusCodes status = subscription.getStatus(); 545 if (status != null) { 546 switch (status) { 547 case REQUESTED: 548 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.REQUESTED); 549 break; 550 case ACTIVE: 551 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ACTIVE); 552 break; 553 case ERROR: 554 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ERROR); 555 break; 556 case OFF: 557 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.OFF); 558 break; 559 case NULL: 560 case ENTEREDINERROR: 561 default: 562 ourLog.warn("Converting R5 Subscription status from {} to ERROR", status); 563 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ERROR); 564 } 565 } 566 retVal.getTopicSubscription().setContent(subscription.getContent()); 567 retVal.setEndpointUrl(subscription.getEndpoint()); 568 retVal.getTopicSubscription().setTopic(subscription.getTopic()); 569 retVal.setChannelType(getChannelType(subscription)); 570 571 subscription.getFilterBy().forEach(filter -> { 572 retVal.getTopicSubscription().addFilter(convertFilter(filter)); 573 }); 574 575 retVal.getTopicSubscription().setHeartbeatPeriod(subscription.getHeartbeatPeriod()); 576 retVal.getTopicSubscription().setMaxCount(subscription.getMaxCount()); 577 578 setR5FlagsBasedOnChannelType(subscription, retVal); 579 580 handleCrossPartition(theSubscription, retVal); 581 582 return retVal; 583 } 584 585 private void setR5FlagsBasedOnChannelType( 586 org.hl7.fhir.r5.model.Subscription subscription, CanonicalSubscription retVal) { 587 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 588 String from; 589 String subjectTemplate; 590 try { 591 from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 592 subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 593 } catch (FHIRException theE) { 594 throw new ConfigurationException( 595 Msg.code(2323) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 596 } 597 retVal.getEmailDetails().setFrom(from); 598 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 599 } 600 601 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 602 String stripVersionIds; 603 String deliverLatestVersion; 604 try { 605 stripVersionIds = 606 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 607 deliverLatestVersion = getExtensionString( 608 subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 609 } catch (FHIRException theE) { 610 throw new ConfigurationException( 611 Msg.code(2324) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 612 } 613 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 614 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 615 } 616 } 617 618 private CanonicalTopicSubscriptionFilter convertFilter( 619 org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent theFilter) { 620 CanonicalTopicSubscriptionFilter retVal = new CanonicalTopicSubscriptionFilter(); 621 retVal.setResourceType(theFilter.getResourceType()); 622 retVal.setFilterParameter(theFilter.getFilterParameter()); 623 retVal.setModifier(theFilter.getModifier()); 624 retVal.setComparator(theFilter.getComparator()); 625 retVal.setValue(theFilter.getValue()); 626 return retVal; 627 } 628 629 private void setPartitionIdOnReturnValue(IBaseResource theSubscription, CanonicalSubscription retVal) { 630 RequestPartitionId requestPartitionId = 631 (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID); 632 if (requestPartitionId != null) { 633 retVal.setPartitionId(requestPartitionId.getFirstPartitionIdOrNull()); 634 } 635 } 636 637 private String getExtensionString(IBaseHasExtensions theBase, String theUrl) { 638 return theBase.getExtension().stream() 639 .filter(t -> theUrl.equals(t.getUrl())) 640 .filter(t -> t.getValue() instanceof IPrimitiveType) 641 .map(t -> (IPrimitiveType<?>) t.getValue()) 642 .map(t -> t.getValueAsString()) 643 .findFirst() 644 .orElse(null); 645 } 646 647 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 648 public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) { 649 CanonicalSubscriptionChannelType retVal = null; 650 651 switch (myFhirContext.getVersion().getVersion()) { 652 case DSTU2: { 653 String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription) 654 .getChannel() 655 .getType(); 656 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 657 break; 658 } 659 case DSTU3: { 660 org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type = 661 ((org.hl7.fhir.dstu3.model.Subscription) theSubscription) 662 .getChannel() 663 .getType(); 664 if (type != null) { 665 String channelTypeCode = type.toCode(); 666 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 667 } 668 break; 669 } 670 case R4: { 671 org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription) 672 theSubscription) 673 .getChannel() 674 .getType(); 675 if (type != null) { 676 String channelTypeCode = type.toCode(); 677 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 678 } 679 break; 680 } 681 case R4B: { 682 org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelType type = 683 ((org.hl7.fhir.r4b.model.Subscription) theSubscription) 684 .getChannel() 685 .getType(); 686 if (type != null) { 687 String channelTypeCode = type.toCode(); 688 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 689 } 690 break; 691 } 692 case R5: { 693 org.hl7.fhir.r5.model.Coding nextTypeCode = 694 ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannelType(); 695 CanonicalSubscriptionChannelType code = 696 CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode()); 697 if (code != null) { 698 retVal = code; 699 } 700 break; 701 } 702 default: 703 throw new IllegalStateException(Msg.code(2326) + "Unsupported Subscription FHIR version: " 704 + myFhirContext.getVersion().getVersion()); 705 } 706 707 return retVal; 708 } 709 710 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 711 @Nullable 712 public String getCriteria(IBaseResource theSubscription) { 713 String retVal = null; 714 715 switch (myFhirContext.getVersion().getVersion()) { 716 case DSTU2: 717 retVal = ((Subscription) theSubscription).getCriteria(); 718 break; 719 case DSTU3: 720 retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria(); 721 break; 722 case R4: 723 retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria(); 724 break; 725 case R4B: 726 retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria(); 727 break; 728 case R5: 729 default: 730 throw new IllegalStateException( 731 Msg.code(2327) + "Subscription criteria is not supported for FHIR version: " 732 + myFhirContext.getVersion().getVersion()); 733 } 734 735 return retVal; 736 } 737 738 public void setMatchingStrategyTag( 739 @Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) { 740 IBaseMetaType meta = theSubscription.getMeta(); 741 742 // Remove any existing strategy tag 743 meta.getTag().stream() 744 .filter(t -> HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem())) 745 .forEach(t -> { 746 t.setCode(null); 747 t.setSystem(null); 748 t.setDisplay(null); 749 }); 750 751 if (theStrategy == null) { 752 return; 753 } 754 755 String value = theStrategy.toString(); 756 String display; 757 758 if (theStrategy == SubscriptionMatchingStrategy.DATABASE) { 759 display = "Database"; 760 } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) { 761 display = "In-memory"; 762 } else if (theStrategy == SubscriptionMatchingStrategy.TOPIC) { 763 display = "SubscriptionTopic"; 764 } else { 765 throw new IllegalStateException(Msg.code(567) + "Unknown " 766 + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy); 767 } 768 meta.addTag() 769 .setSystem(HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY) 770 .setCode(value) 771 .setDisplay(display); 772 } 773 774 public String getSubscriptionStatus(IBaseResource theSubscription) { 775 final IPrimitiveType<?> status = myFhirContext 776 .newTerser() 777 .getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class); 778 if (status == null) { 779 return null; 780 } 781 return status.getValueAsString(); 782 } 783 784 private void handleCrossPartition(IBaseResource theSubscription, CanonicalSubscription retVal) { 785 if (myStorageSettings.isCrossPartitionSubscriptionEnabled()) { 786 retVal.setCrossPartitionEnabled(true); 787 } else { 788 retVal.setCrossPartitionEnabled(SubscriptionUtil.isCrossPartition(theSubscription)); 789 } 790 } 791}