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