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}