001package ca.uhn.fhir.jpa.subscription.module.cache; 002 003/*- 004 * #%L 005 * HAPI FHIR Subscription Server 006 * %% 007 * Copyright (C) 2014 - 2020 University Health Network 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.jpa.model.util.JpaConstants; 026import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; 027import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType; 028import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy; 029import ca.uhn.fhir.model.dstu2.resource.Subscription; 030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 031import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 032import org.apache.commons.lang3.Validate; 033import org.hl7.fhir.exceptions.FHIRException; 034import org.hl7.fhir.instance.model.api.*; 035import org.hl7.fhir.r4.model.Extension; 036import org.hl7.fhir.r5.model.Coding; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039import org.springframework.beans.factory.annotation.Autowired; 040import org.springframework.stereotype.Service; 041 042import javax.annotation.Nonnull; 043import javax.annotation.Nullable; 044import java.util.Collections; 045import java.util.List; 046import java.util.Map; 047import java.util.stream.Collectors; 048 049import static java.util.stream.Collectors.mapping; 050import static java.util.stream.Collectors.toList; 051 052@Service 053public class SubscriptionCanonicalizer { 054 private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class); 055 056 final FhirContext myFhirContext; 057 058 @Autowired 059 public SubscriptionCanonicalizer(FhirContext theFhirContext) { 060 myFhirContext = theFhirContext; 061 } 062 063 public CanonicalSubscription canonicalize(IBaseResource theSubscription) { 064 switch (myFhirContext.getVersion().getVersion()) { 065 case DSTU2: 066 return canonicalizeDstu2(theSubscription); 067 case DSTU3: 068 return canonicalizeDstu3(theSubscription); 069 case R4: 070 return canonicalizeR4(theSubscription); 071 case R5: 072 return canonicalizeR5(theSubscription); 073 case DSTU2_HL7ORG: 074 case DSTU2_1: 075 default: 076 throw new ConfigurationException("Subscription not supported for version: " + myFhirContext.getVersion().getVersion()); 077 } 078 } 079 080 private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) { 081 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 082 083 CanonicalSubscription retVal = new CanonicalSubscription(); 084 try { 085 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus())); 086 retVal.setChannelType(getChannelType(theSubscription)); 087 retVal.setCriteriaString(subscription.getCriteria()); 088 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 089 retVal.setHeaders(subscription.getChannel().getHeader()); 090 retVal.setChannelExtensions(extractExtension(subscription)); 091 retVal.setIdElement(subscription.getIdElement()); 092 retVal.setPayloadString(subscription.getChannel().getPayload()); 093 } catch (FHIRException theE) { 094 throw new InternalErrorException(theE); 095 } 096 return retVal; 097 } 098 099 private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) { 100 org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 101 102 CanonicalSubscription retVal = new CanonicalSubscription(); 103 try { 104 org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus status = subscription.getStatus(); 105 if (status != null) { 106 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 107 } 108 retVal.setChannelType(getChannelType(theSubscription)); 109 retVal.setCriteriaString(subscription.getCriteria()); 110 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 111 retVal.setHeaders(subscription.getChannel().getHeader()); 112 retVal.setChannelExtensions(extractExtension(subscription)); 113 retVal.setIdElement(subscription.getIdElement()); 114 retVal.setPayloadString(subscription.getChannel().getPayload()); 115 116 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 117 String from; 118 String subjectTemplate; 119 120 try { 121 from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM); 122 subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 123 } catch (FHIRException theE) { 124 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 125 } 126 retVal.getEmailDetails().setFrom(from); 127 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 128 } 129 130 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 131 132 String stripVersionIds; 133 String deliverLatestVersion; 134 try { 135 stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 136 deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 137 } catch (FHIRException theE) { 138 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 139 } 140 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 141 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 142 } 143 144 } catch (FHIRException theE) { 145 throw new InternalErrorException(theE); 146 } 147 return retVal; 148 } 149 150 private @Nonnull 151 Map<String, List<String>> extractExtension(IBaseResource theSubscription) { 152 try { 153 switch (theSubscription.getStructureFhirVersionEnum()) { 154 case DSTU2: { 155 ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; 156 return subscription 157 .getChannel() 158 .getUndeclaredExtensions() 159 .stream() 160 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 161 } 162 case DSTU3: { 163 org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; 164 return subscription 165 .getChannel() 166 .getExtension() 167 .stream() 168 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 169 } 170 case R4: { 171 org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; 172 return subscription 173 .getChannel() 174 .getExtension() 175 .stream() 176 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 177 } 178 case R5: { 179 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 180 return subscription 181 .getChannel() 182 .getExtension() 183 .stream() 184 .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList()))); 185 } 186 case DSTU2_HL7ORG: 187 case DSTU2_1: 188 default: { 189 ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); 190 break; 191 } 192 } 193 } catch (FHIRException theE) { 194 ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue(), theE); 195 } 196 return Collections.emptyMap(); 197 } 198 199 private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) { 200 org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; 201 202 CanonicalSubscription retVal = new CanonicalSubscription(); 203 retVal.setStatus(subscription.getStatus()); 204 retVal.setChannelType(getChannelType(theSubscription)); 205 retVal.setCriteriaString(subscription.getCriteria()); 206 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 207 retVal.setHeaders(subscription.getChannel().getHeader()); 208 retVal.setChannelExtensions(extractExtension(subscription)); 209 retVal.setIdElement(subscription.getIdElement()); 210 retVal.setPayloadString(subscription.getChannel().getPayload()); 211 212 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 213 String from; 214 String subjectTemplate; 215 try { 216 from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM); 217 subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 218 } catch (FHIRException theE) { 219 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 220 } 221 retVal.getEmailDetails().setFrom(from); 222 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 223 } 224 225 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 226 String stripVersionIds; 227 String deliverLatestVersion; 228 try { 229 stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 230 deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 231 } catch (FHIRException theE) { 232 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 233 } 234 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 235 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 236 } 237 238 List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 239 if (topicExts.size() > 0) { 240 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 241 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 242 throw new PreconditionFailedException("Topic reference must be an EventDefinition"); 243 } 244 } 245 246 return retVal; 247 } 248 249 private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) { 250 org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; 251 252 CanonicalSubscription retVal = new CanonicalSubscription(); 253 org.hl7.fhir.r5.model.Subscription.SubscriptionStatus status = subscription.getStatus(); 254 if (status != null) { 255 retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); 256 } 257 retVal.setChannelType(getChannelType(subscription)); 258 retVal.setCriteriaString(getCriteria(theSubscription)); 259 retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); 260 retVal.setHeaders(subscription.getChannel().getHeader()); 261 retVal.setChannelExtensions(extractExtension(subscription)); 262 retVal.setIdElement(subscription.getIdElement()); 263 retVal.setPayloadString(subscription.getChannel().getPayload().getContentType()); 264 265 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { 266 String from; 267 String subjectTemplate; 268 try { 269 from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM); 270 subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); 271 } catch (FHIRException theE) { 272 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 273 } 274 retVal.getEmailDetails().setFrom(from); 275 retVal.getEmailDetails().setSubjectTemplate(subjectTemplate); 276 } 277 278 if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { 279 String stripVersionIds; 280 String deliverLatestVersion; 281 try { 282 stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); 283 deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); 284 } catch (FHIRException theE) { 285 throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); 286 } 287 retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); 288 retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); 289 } 290 291 List<org.hl7.fhir.r5.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); 292 if (topicExts.size() > 0) { 293 IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); 294 if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { 295 throw new PreconditionFailedException("Topic reference must be an EventDefinition"); 296 } 297 } 298 299 return retVal; 300 } 301 302 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 303 public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) { 304 CanonicalSubscriptionChannelType retVal = null; 305 306 switch (myFhirContext.getVersion().getVersion()) { 307 case DSTU2: { 308 String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription).getChannel().getType(); 309 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 310 break; 311 } 312 case DSTU3: { 313 org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getChannel().getType(); 314 if (type != null) { 315 String channelTypeCode = type.toCode(); 316 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 317 } 318 break; 319 } 320 case R4: { 321 org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getChannel().getType(); 322 if (type != null) { 323 String channelTypeCode = type.toCode(); 324 retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode); 325 } 326 break; 327 } 328 case R5: { 329 for (Coding nextTypeCode : ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannel().getType().getCoding()) { 330 CanonicalSubscriptionChannelType code = CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode()); 331 if (code != null) { 332 retVal = code; 333 } 334 } 335 break; 336 } 337 } 338 339 return retVal; 340 } 341 342 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 343 public String getCriteria(IBaseResource theSubscription) { 344 String retVal = null; 345 346 switch (myFhirContext.getVersion().getVersion()) { 347 case DSTU2: 348 retVal = ((Subscription) theSubscription).getCriteria(); 349 break; 350 case DSTU3: 351 retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria(); 352 break; 353 case R4: 354 retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria(); 355 break; 356 case R5: 357 org.hl7.fhir.r5.model.Topic topic = (org.hl7.fhir.r5.model.Topic) ((org.hl7.fhir.r5.model.Subscription) theSubscription).getTopic().getResource(); 358 Validate.notNull(topic); 359 retVal = topic.getResourceTrigger().getQueryCriteria().getCurrent(); 360 break; 361 } 362 363 return retVal; 364 } 365 366 367 public void setMatchingStrategyTag(@Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) { 368 IBaseMetaType meta = theSubscription.getMeta(); 369 370 // Remove any existing strategy tag 371 meta 372 .getTag() 373 .stream() 374 .filter(t->JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem())) 375 .forEach(t->{ 376 t.setCode(null); 377 t.setSystem(null); 378 t.setDisplay(null); 379 }); 380 381 if (theStrategy == null) { 382 return; 383 } 384 385 String value = theStrategy.toString(); 386 String display; 387 388 if (theStrategy == SubscriptionMatchingStrategy.DATABASE) { 389 display = "Database"; 390 } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) { 391 display = "In-memory"; 392 } else { 393 throw new IllegalStateException("Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy); 394 } 395 meta.addTag().setSystem(JpaConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display); 396 } 397 398 public String getSubscriptionStatus(IBaseResource theSubscription) { 399 final IPrimitiveType<?> status = myFhirContext.newTerser().getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class); 400 if (status == null) { 401 return null; 402 } 403 return status.getValueAsString(); 404 } 405 406}