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.config.SubscriptionSettings; 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 SubscriptionSettings mySubscriptionSettings; 073 074 @Autowired 075 public SubscriptionCanonicalizer(FhirContext theFhirContext, SubscriptionSettings theSubscriptionSettings) { 076 myFhirContext = theFhirContext; 077 mySubscriptionSettings = theSubscriptionSettings; 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 mySubscriptionSettings = new SubscriptionSettings(); 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 retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription)); 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(BooleanDt.class::cast) 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 retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription)); 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 retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription)); 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.isEmpty()) { 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.isEmpty()) { 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 retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription)); 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.isEmpty()) { 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 -> retVal.getTopicSubscription() 572 .addFilter(convertFilter(filter))); 573 574 retVal.getTopicSubscription().setHeartbeatPeriod(subscription.getHeartbeatPeriod()); 575 retVal.getTopicSubscription().setMaxCount(subscription.getMaxCount()); 576 577 setR5FlagsBasedOnChannelType(subscription, retVal); 578 579 retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription)); 580 581 return retVal; 582 } 583 584 private void setR5FlagsBasedOnChannelType( 585 org.hl7.fhir.r5.model.Subscription subscription, CanonicalSubscription retVal) { 586 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 587 String from; 588 String subjectTemplate; 589 try { 590 from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 591 subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 592 } catch (FHIRException theE) { 593 throw new ConfigurationException( 594 Msg.code(2323) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 595 } 596 retVal.getEmailDetails().setFrom(from); 597 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 598 } 599 600 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 601 String stripVersionIds; 602 String deliverLatestVersion; 603 try { 604 stripVersionIds = 605 getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 606 deliverLatestVersion = getExtensionString( 607 subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 608 } catch (FHIRException theE) { 609 throw new ConfigurationException( 610 Msg.code(2324) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 611 } 612 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 613 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 614 } 615 } 616 617 private CanonicalTopicSubscriptionFilter convertFilter( 618 org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent theFilter) { 619 CanonicalTopicSubscriptionFilter retVal = new CanonicalTopicSubscriptionFilter(); 620 retVal.setResourceType(theFilter.getResourceType()); 621 retVal.setFilterParameter(theFilter.getFilterParameter()); 622 retVal.setModifier(theFilter.getModifier()); 623 retVal.setComparator(theFilter.getComparator()); 624 retVal.setValue(theFilter.getValue()); 625 return retVal; 626 } 627 628 private void setPartitionIdOnReturnValue(IBaseResource theSubscription, CanonicalSubscription retVal) { 629 RequestPartitionId requestPartitionId = 630 (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID); 631 if (requestPartitionId != null) { 632 retVal.setPartitionId(requestPartitionId.getFirstPartitionIdOrNull()); 633 } 634 } 635 636 private String getExtensionString(IBaseHasExtensions theBase, String theUrl) { 637 return theBase.getExtension().stream() 638 .filter(t -> theUrl.equals(t.getUrl())) 639 .filter(t -> t.getValue() instanceof IPrimitiveType) 640 .map(t -> (IPrimitiveType<?>) t.getValue()) 641 .map(t -> t.getValueAsString()) 642 .findFirst() 643 .orElse(null); 644 } 645 646 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 647 public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) { 648 CanonicalSubscriptionChannelType retVal = null; 649 650 switch (myFhirContext.getVersion().getVersion()) { 651 case DSTU2: { 652 String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription) 653 .getChannel() 654 .getType(); 655 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 656 break; 657 } 658 case DSTU3: { 659 org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type = 660 ((org.hl7.fhir.dstu3.model.Subscription) theSubscription) 661 .getChannel() 662 .getType(); 663 if (type != null) { 664 String channelTypeCode = type.toCode(); 665 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 666 } 667 break; 668 } 669 case R4: { 670 org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription) 671 theSubscription) 672 .getChannel() 673 .getType(); 674 if (type != null) { 675 String channelTypeCode = type.toCode(); 676 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 677 } 678 break; 679 } 680 case R4B: { 681 org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelType type = 682 ((org.hl7.fhir.r4b.model.Subscription) theSubscription) 683 .getChannel() 684 .getType(); 685 if (type != null) { 686 String channelTypeCode = type.toCode(); 687 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 688 } 689 break; 690 } 691 case R5: { 692 org.hl7.fhir.r5.model.Coding nextTypeCode = 693 ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannelType(); 694 CanonicalSubscriptionChannelType code = 695 CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode()); 696 if (code != null) { 697 retVal = code; 698 } 699 break; 700 } 701 default: 702 throw new IllegalStateException(Msg.code(2326) + "Unsupported Subscription FHIR version: " 703 + myFhirContext.getVersion().getVersion()); 704 } 705 706 return retVal; 707 } 708 709 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 710 @Nullable 711 public String getCriteria(IBaseResource theSubscription) { 712 String retVal = null; 713 714 switch (myFhirContext.getVersion().getVersion()) { 715 case DSTU2: 716 retVal = ((Subscription) theSubscription).getCriteria(); 717 break; 718 case DSTU3: 719 retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria(); 720 break; 721 case R4: 722 retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria(); 723 break; 724 case R4B: 725 retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria(); 726 break; 727 case R5: 728 default: 729 throw new IllegalStateException( 730 Msg.code(2327) + "Subscription criteria is not supported for FHIR version: " 731 + myFhirContext.getVersion().getVersion()); 732 } 733 734 return retVal; 735 } 736 737 public void setMatchingStrategyTag( 738 @Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) { 739 IBaseMetaType meta = theSubscription.getMeta(); 740 741 // Remove any existing strategy tag 742 meta.getTag().stream() 743 .filter(t -> HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem())) 744 .forEach(t -> { 745 t.setCode(null); 746 t.setSystem(null); 747 t.setDisplay(null); 748 }); 749 750 if (theStrategy == null) { 751 return; 752 } 753 754 String value = theStrategy.toString(); 755 String display; 756 757 if (theStrategy == SubscriptionMatchingStrategy.DATABASE) { 758 display = "Database"; 759 } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) { 760 display = "In-memory"; 761 } else if (theStrategy == SubscriptionMatchingStrategy.TOPIC) { 762 display = "SubscriptionTopic"; 763 } else { 764 throw new IllegalStateException(Msg.code(567) + "Unknown " 765 + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy); 766 } 767 meta.addTag() 768 .setSystem(HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY) 769 .setCode(value) 770 .setDisplay(display); 771 } 772 773 public String getSubscriptionStatus(IBaseResource theSubscription) { 774 final IPrimitiveType<?> status = myFhirContext 775 .newTerser() 776 .getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class); 777 if (status == null) { 778 return null; 779 } 780 return status.getValueAsString(); 781 } 782 783 private boolean handleCrossPartition(IBaseResource theSubscription) { 784 RequestPartitionId requestPartitionId = 785 (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID); 786 787 boolean isSubscriptionCreatedOnDefaultPartition = false; 788 789 if (nonNull(requestPartitionId)) { 790 isSubscriptionCreatedOnDefaultPartition = requestPartitionId.isDefaultPartition(); 791 } 792 793 boolean isSubscriptionDefinededAsCrossPartitionSubscription = 794 SubscriptionUtil.isDefinedAsCrossPartitionSubcription(theSubscription); 795 boolean isGlobalSettingCrossPartitionSubscriptionEnabled = 796 mySubscriptionSettings.isCrossPartitionSubscriptionEnabled(); 797 798 return isSubscriptionCreatedOnDefaultPartition 799 && isSubscriptionDefinededAsCrossPartitionSubscription 800 && isGlobalSettingCrossPartitionSubscriptionEnabled; 801 } 802}