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