001package ca.uhn.fhir.jpa.subscription.match.registry; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.ConfigurationException; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 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.model.dstu2.resource.Subscription; 031import ca.uhn.fhir.rest.api.Constants; 032import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 033import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 034import ca.uhn.fhir.util.HapiExtensions; 035import ca.uhn.fhir.util.SubscriptionUtil; 036import org.hl7.fhir.exceptions.FHIRException; 037import org.hl7.fhir.instance.model.api.IBaseCoding; 038import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 039import org.hl7.fhir.instance.model.api.IBaseMetaType; 040import org.hl7.fhir.instance.model.api.IBaseReference; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042import org.hl7.fhir.instance.model.api.IPrimitiveType; 043import org.hl7.fhir.r4.model.BooleanType; 044import org.hl7.fhir.r4.model.Extension; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047import org.springframework.beans.factory.annotation.Autowired; 048 049import javax.annotation.Nonnull; 050import javax.annotation.Nullable; 051import java.util.Collections; 052import java.util.List; 053import java.util.Map; 054import java.util.stream.Collectors; 055 056import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; 057import static java.util.stream.Collectors.mapping; 058import static java.util.stream.Collectors.toList; 059 060public class SubscriptionCanonicalizer { 061 private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class); 062 063 final FhirContext myFhirContext; 064 065 @Autowired 066 public SubscriptionCanonicalizer(FhirContext theFhirContext) { 067 myFhirContext = theFhirContext; 068 } 069 070 public CanonicalSubscription canonicalize(IBaseResource theSubscription) { 071 switch (myFhirContext.getVersion().getVersion()) { 072 case DSTU2: 073 return canonicalizeDstu2(theSubscription); 074 case DSTU3: 075 return canonicalizeDstu3(theSubscription); 076 case R4: 077 return canonicalizeR4(theSubscription); 078 case R5: 079 return canonicalizeR5(theSubscription); 080 case DSTU2_HL7ORG: 081 case DSTU2_1: 082 default: 083 throw new ConfigurationException(Msg.code(556) + "Subscription not supported for version: " + myFhirContext.getVersion().getVersion()); 084 } 085 } 086 087 private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) { 088 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 089 CanonicalSubscription retVal = new CanonicalSubscription(); 090 try { 091 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus())); 092 retVal.setChannelType(getChannelType(theSubscription)); 093 retVal.setCriteriaString(subscription.getCriteria()); 094 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 095 retVal.setHeaders(subscription.getChannel().getHeader()); 096 retVal.setChannelExtensions(extractExtension(subscription)); 097 retVal.setIdElement(subscription.getIdElement()); 098 retVal.setPayloadString(subscription.getChannel().getPayload()); 099 retVal.setTags(extractTags(subscription)); 100 retVal.setCrossPartitionEnabled(SubscriptionUtil.isCrossPartition(theSubscription)); 101 } catch (FHIRException theE) { 102 throw new InternalErrorException(Msg.code(557) + theE); 103 } 104 return retVal; 105 } 106 107 /** 108 * Extract the meta tags from the subscription and convert them to a simple string map. 109 * @param theSubscription The subscription to extract the tags from 110 * @return A map of tags System:Code 111 */ 112 private Map<String, String> extractTags(IBaseResource theSubscription) { 113 return theSubscription.getMeta().getTag() 114 .stream() 115 .filter(t -> t.getSystem() != null && t.getCode() != null) 116 .collect(Collectors.toMap( 117 IBaseCoding::getSystem, 118 IBaseCoding::getCode 119 )); 120 } 121 122 private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) { 123 org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 124 125 CanonicalSubscription retVal = new CanonicalSubscription(); 126 try { 127 org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus status = subscription.getStatus(); 128 if (status != null) { 129 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 130 } 131 setPartitionIdOnReturnValue(theSubscription, retVal); 132 retVal.setChannelType(getChannelType(theSubscription)); 133 retVal.setCriteriaString(subscription.getCriteria()); 134 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 135 retVal.setHeaders(subscription.getChannel().getHeader()); 136 retVal.setChannelExtensions(extractExtension(subscription)); 137 retVal.setIdElement(subscription.getIdElement()); 138 retVal.setPayloadString(subscription.getChannel().getPayload()); 139 retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 140 retVal.setTags(extractTags(subscription)); 141 retVal.setCrossPartitionEnabled(SubscriptionUtil.isCrossPartition(theSubscription)); 142 143 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 144 String from; 145 String subjectTemplate; 146 147 try { 148 from = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 149 subjectTemplate = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 150 } catch (FHIRException theE) { 151 throw new ConfigurationException(Msg.code(558) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 152 } 153 retVal.getEmailDetails().setFrom(from); 154 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 155 } 156 157 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 158 159 String stripVersionIds; 160 String deliverLatestVersion; 161 try { 162 stripVersionIds = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 163 deliverLatestVersion = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 164 } catch (FHIRException theE) { 165 throw new ConfigurationException(Msg.code(559) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 166 } 167 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 168 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 169 } 170 171 } catch (FHIRException theE) { 172 throw new InternalErrorException(Msg.code(560) + theE); 173 } 174 return retVal; 175 } 176 177 private @Nonnull 178 Map<String, List<String>> extractExtension(IBaseResource theSubscription) { 179 try { 180 switch (theSubscription.getStructureFhirVersionEnum()) { 181 case DSTU2: { 182 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 183 return subscription 184 .getChannel() 185 .getUndeclaredExtensions() 186 .stream() 187 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 188 } 189 case DSTU3: { 190 org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 191 return subscription 192 .getChannel() 193 .getExtension() 194 .stream() 195 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 196 } 197 case R4: { 198 org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; 199 return subscription 200 .getChannel() 201 .getExtension() 202 .stream() 203 .collect(Collectors.groupingBy(t -> t.getUrl(), 204 mapping(t -> { 205 return t.getValueAsPrimitive().getValueAsString(); 206 }, toList()))); 207 } 208 case R5: { 209 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 210 return subscription 211 .getExtension() 212 .stream() 213 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 214 } 215 case DSTU2_HL7ORG: 216 case DSTU2_1: 217 default: { 218 ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); 219 break; 220 } 221 } 222 } catch (FHIRException theE) { 223 ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue(), theE); 224 } 225 return Collections.emptyMap(); 226 } 227 228 private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) { 229 org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; 230 CanonicalSubscription retVal = new CanonicalSubscription(); 231 retVal.setStatus(subscription.getStatus()); 232 retVal.setChannelType(getChannelType(theSubscription)); 233 retVal.setCriteriaString(subscription.getCriteria()); 234 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 235 retVal.setHeaders(subscription.getChannel().getHeader()); 236 retVal.setChannelExtensions(extractExtension(subscription)); 237 retVal.setIdElement(subscription.getIdElement()); 238 retVal.setPayloadString(subscription.getChannel().getPayload()); 239 retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 240 retVal.setTags(extractTags(subscription)); 241 setPartitionIdOnReturnValue(theSubscription, retVal); 242 retVal.setCrossPartitionEnabled(SubscriptionUtil.isCrossPartition(theSubscription)); 243 244 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 245 String from; 246 String subjectTemplate; 247 try { 248 from = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 249 subjectTemplate = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 250 } catch (FHIRException theE) { 251 throw new ConfigurationException(Msg.code(561) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 252 } 253 retVal.getEmailDetails().setFrom(from); 254 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 255 } 256 257 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 258 String stripVersionIds; 259 String deliverLatestVersion; 260 try { 261 stripVersionIds = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 262 deliverLatestVersion = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 263 } catch (FHIRException theE) { 264 throw new ConfigurationException(Msg.code(562) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 265 } 266 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 267 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 268 } 269 270 List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 271 if (topicExts.size() > 0) { 272 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 273 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 274 throw new PreconditionFailedException(Msg.code(563) + "Topic reference must be an EventDefinition"); 275 } 276 } 277 278 Extension extension = subscription.getExtensionByUrl(EX_SEND_DELETE_MESSAGES); 279 if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) { 280 retVal.setSendDeleteMessages(((BooleanType) extension.getValue()).booleanValue()); 281 } 282 return retVal; 283 } 284 285 private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) { 286 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 287 288 CanonicalSubscription retVal = new CanonicalSubscription(); 289 org.hl7.fhir.r5.model.Enumerations.SubscriptionState status = subscription.getStatus(); 290 if (status != null) { 291 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 292 } 293 setPartitionIdOnReturnValue(theSubscription, retVal); 294 retVal.setChannelType(getChannelType(subscription)); 295 retVal.setCriteriaString(getCriteria(theSubscription)); 296 retVal.setEndpointUrl(subscription.getEndpoint()); 297 retVal.setHeaders(subscription.getHeader()); 298 retVal.setChannelExtensions(extractExtension(subscription)); 299 retVal.setIdElement(subscription.getIdElement()); 300 retVal.setPayloadString(subscription.getContentType()); 301 retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); 302 retVal.setTags(extractTags(subscription)); 303 304 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 305 String from; 306 String subjectTemplate; 307 try { 308 from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM); 309 subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 310 } catch (FHIRException theE) { 311 throw new ConfigurationException(Msg.code(564) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 312 } 313 retVal.getEmailDetails().setFrom(from); 314 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 315 } 316 317 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 318 String stripVersionIds; 319 String deliverLatestVersion; 320 try { 321 stripVersionIds = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 322 deliverLatestVersion = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 323 } catch (FHIRException theE) { 324 throw new ConfigurationException(Msg.code(565) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE); 325 } 326 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 327 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 328 } 329 330 List<org.hl7.fhir.r5.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 331 if (topicExts.size() > 0) { 332 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 333 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 334 throw new PreconditionFailedException(Msg.code(566) + "Topic reference must be an EventDefinition"); 335 } 336 } 337 338 return retVal; 339 } 340 341 private void setPartitionIdOnReturnValue(IBaseResource theSubscription, CanonicalSubscription retVal) { 342 RequestPartitionId requestPartitionId = (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID); 343 if (requestPartitionId != null) { 344 retVal.setPartitionId(requestPartitionId.getFirstPartitionIdOrNull()); 345 } 346 } 347 348 private String getExtensionString(IBaseHasExtensions theBase, String theUrl) { 349 return theBase 350 .getExtension() 351 .stream() 352 .filter(t -> theUrl.equals(t.getUrl())) 353 .filter(t -> t.getValue() instanceof IPrimitiveType) 354 .map(t -> (IPrimitiveType<?>) t.getValue()) 355 .map(t -> t.getValueAsString()) 356 .findFirst() 357 .orElse(null); 358 } 359 360 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 361 public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) { 362 CanonicalSubscriptionChannelType retVal = null; 363 364 switch (myFhirContext.getVersion().getVersion()) { 365 case DSTU2: { 366 String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription).getChannel().getType(); 367 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 368 break; 369 } 370 case DSTU3: { 371 org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getChannel().getType(); 372 if (type != null) { 373 String channelTypeCode = type.toCode(); 374 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 375 } 376 break; 377 } 378 case R4: { 379 org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getChannel().getType(); 380 if (type != null) { 381 String channelTypeCode = type.toCode(); 382 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 383 } 384 break; 385 } 386 case R5: { 387 org.hl7.fhir.r5.model.Coding nextTypeCode = ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannelType(); 388 CanonicalSubscriptionChannelType code = CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode()); 389 if (code != null) { 390 retVal = code; 391 } 392 break; 393 } 394 } 395 396 return retVal; 397 } 398 399 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 400 @Nullable 401 public String getCriteria(IBaseResource theSubscription) { 402 String retVal = null; 403 404 switch (myFhirContext.getVersion().getVersion()) { 405 case DSTU2: 406 retVal = ((Subscription) theSubscription).getCriteria(); 407 break; 408 case DSTU3: 409 retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria(); 410 break; 411 case R4: 412 retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria(); 413 break; 414 case R5: 415 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 416 String topicElement = subscription.getTopicElement().getValue(); 417 org.hl7.fhir.r5.model.SubscriptionTopic topic = (org.hl7.fhir.r5.model.SubscriptionTopic) subscription.getContained().stream().filter(t -> ("#" + t.getId()).equals(topicElement) || (t.getId()).equals(topicElement)).findFirst().orElse(null); 418 if (topic == null) { 419 ourLog.warn("Missing contained subscription topic in R5 subscription"); 420 return null; 421 } 422 retVal = topic.getResourceTriggerFirstRep().getQueryCriteria().getCurrent(); 423 break; 424 } 425 426 return retVal; 427 } 428 429 430 public void setMatchingStrategyTag(@Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) { 431 IBaseMetaType meta = theSubscription.getMeta(); 432 433 // Remove any existing strategy tag 434 meta 435 .getTag() 436 .stream() 437 .filter(t -> HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem())) 438 .forEach(t -> { 439 t.setCode(null); 440 t.setSystem(null); 441 t.setDisplay(null); 442 }); 443 444 if (theStrategy == null) { 445 return; 446 } 447 448 String value = theStrategy.toString(); 449 String display; 450 451 if (theStrategy == SubscriptionMatchingStrategy.DATABASE) { 452 display = "Database"; 453 } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) { 454 display = "In-memory"; 455 } else { 456 throw new IllegalStateException(Msg.code(567) + "Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy); 457 } 458 meta.addTag().setSystem(HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display); 459 } 460 461 public String getSubscriptionStatus(IBaseResource theSubscription) { 462 final IPrimitiveType<?> status = myFhirContext.newTerser().getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class); 463 if (status == null) { 464 return null; 465 } 466 return status.getValueAsString(); 467 } 468 469}