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