001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.subscription.match.registry;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
027import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
028import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
029import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
030import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscription;
031import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter;
032import ca.uhn.fhir.model.api.BasePrimitive;
033import ca.uhn.fhir.model.api.ExtensionDt;
034import ca.uhn.fhir.model.dstu2.resource.Subscription;
035import ca.uhn.fhir.model.primitive.BooleanDt;
036import ca.uhn.fhir.rest.api.Constants;
037import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
038import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
039import ca.uhn.fhir.subscription.SubscriptionConstants;
040import ca.uhn.fhir.util.HapiExtensions;
041import ca.uhn.fhir.util.SubscriptionUtil;
042import jakarta.annotation.Nonnull;
043import jakarta.annotation.Nullable;
044import org.hl7.fhir.exceptions.FHIRException;
045import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
046import org.hl7.fhir.instance.model.api.IBaseMetaType;
047import org.hl7.fhir.instance.model.api.IBaseReference;
048import org.hl7.fhir.instance.model.api.IBaseResource;
049import org.hl7.fhir.instance.model.api.IPrimitiveType;
050import org.hl7.fhir.r4.model.BooleanType;
051import org.hl7.fhir.r4.model.Extension;
052import org.hl7.fhir.r5.model.Enumerations;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055import org.springframework.beans.factory.annotation.Autowired;
056
057import java.util.Collections;
058import java.util.HashMap;
059import java.util.List;
060import java.util.Map;
061import java.util.stream.Collectors;
062
063import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES;
064import static java.util.Objects.nonNull;
065import static java.util.stream.Collectors.mapping;
066import static java.util.stream.Collectors.toList;
067
068public class SubscriptionCanonicalizer {
069        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class);
070
071        final FhirContext myFhirContext;
072        private final SubscriptionSettings mySubscriptionSettings;
073
074        @Autowired
075        public SubscriptionCanonicalizer(FhirContext theFhirContext, SubscriptionSettings theSubscriptionSettings) {
076                myFhirContext = theFhirContext;
077                mySubscriptionSettings = theSubscriptionSettings;
078        }
079
080        // TODO:  LD:  remove this constructor once all callers call the 2 arg constructor above
081
082        /**
083         * @deprecated All callers should invoke {@link SubscriptionCanonicalizer()} instead.
084         */
085        @Deprecated
086        public SubscriptionCanonicalizer(FhirContext theFhirContext) {
087                myFhirContext = theFhirContext;
088                mySubscriptionSettings = new SubscriptionSettings();
089        }
090
091        public CanonicalSubscription canonicalize(IBaseResource theSubscription) {
092                switch (myFhirContext.getVersion().getVersion()) {
093                        case DSTU2:
094                                return canonicalizeDstu2(theSubscription);
095                        case DSTU3:
096                                return canonicalizeDstu3(theSubscription);
097                        case R4:
098                                return canonicalizeR4(theSubscription);
099                        case R4B:
100                                return canonicalizeR4B(theSubscription);
101                        case R5:
102                                return canonicalizeR5(theSubscription);
103                        case DSTU2_HL7ORG:
104                        case DSTU2_1:
105                        default:
106                                throw new ConfigurationException(Msg.code(556) + "Subscription not supported for version: "
107                                                + myFhirContext.getVersion().getVersion());
108                }
109        }
110
111        private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
112                ca.uhn.fhir.model.dstu2.resource.Subscription subscription =
113                                (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
114                CanonicalSubscription retVal = new CanonicalSubscription();
115                try {
116                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
117                        retVal.setChannelType(getChannelType(theSubscription));
118                        retVal.setCriteriaString(subscription.getCriteria());
119                        Subscription.Channel channel = subscription.getChannel();
120                        retVal.setEndpointUrl(channel.getEndpoint());
121                        retVal.setHeaders(channel.getHeader());
122                        retVal.setChannelExtensions(extractExtension(subscription));
123                        retVal.setIdElement(subscription.getIdElement());
124                        retVal.setPayloadString(channel.getPayload());
125                        retVal.setTags(extractTags(subscription));
126                        retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription));
127                        retVal.setSendDeleteMessages(extractDeleteExtensionDstu2(subscription));
128                } catch (FHIRException theE) {
129                        throw new InternalErrorException(Msg.code(557) + theE);
130                }
131                return retVal;
132        }
133
134        private boolean extractDeleteExtensionDstu2(ca.uhn.fhir.model.dstu2.resource.Subscription theSubscription) {
135                return theSubscription.getChannel().getUndeclaredExtensionsByUrl(EX_SEND_DELETE_MESSAGES).stream()
136                                .map(ExtensionDt::getValue)
137                                .map(BooleanDt.class::cast)
138                                .map(BasePrimitive::getValue)
139                                .findFirst()
140                                .orElse(false);
141        }
142
143        /**
144         * Extract the meta tags from the subscription and convert them to a simple string map.
145         *
146         * @param theSubscription The subscription to extract the tags from
147         * @return A map of tags System:Code
148         */
149        private Map<String, String> extractTags(IBaseResource theSubscription) {
150                Map<String, String> retVal = new HashMap<>();
151                theSubscription.getMeta().getTag().stream()
152                                .filter(t -> t.getSystem() != null && t.getCode() != null)
153                                .forEach(t -> retVal.put(t.getSystem(), t.getCode()));
154                return retVal;
155        }
156
157        private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
158                org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
159
160                CanonicalSubscription retVal = new CanonicalSubscription();
161                try {
162                        org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus status = subscription.getStatus();
163                        if (status != null) {
164                                retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
165                        }
166                        setPartitionIdOnReturnValue(theSubscription, retVal);
167                        retVal.setChannelType(getChannelType(theSubscription));
168                        retVal.setCriteriaString(subscription.getCriteria());
169                        org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel();
170                        retVal.setEndpointUrl(channel.getEndpoint());
171                        retVal.setHeaders(channel.getHeader());
172                        retVal.setChannelExtensions(extractExtension(subscription));
173                        retVal.setIdElement(subscription.getIdElement());
174                        retVal.setPayloadString(channel.getPayload());
175                        retVal.setPayloadSearchCriteria(
176                                        getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
177                        retVal.setTags(extractTags(subscription));
178                        retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription));
179
180                        if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
181                                String from;
182                                String subjectTemplate;
183
184                                try {
185                                        from = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
186                                        subjectTemplate = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
187                                } catch (FHIRException theE) {
188                                        throw new ConfigurationException(
189                                                        Msg.code(558) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
190                                }
191                                retVal.getEmailDetails().setFrom(from);
192                                retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
193                        }
194
195                        if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
196
197                                String stripVersionIds;
198                                String deliverLatestVersion;
199                                try {
200                                        stripVersionIds =
201                                                        channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
202                                        deliverLatestVersion =
203                                                        channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
204                                } catch (FHIRException theE) {
205                                        throw new ConfigurationException(
206                                                        Msg.code(559) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
207                                }
208                                retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
209                                retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
210                        }
211                        retVal.setSendDeleteMessages(extractSendDeletesDstu3(subscription));
212
213                } catch (FHIRException theE) {
214                        throw new InternalErrorException(Msg.code(560) + theE);
215                }
216                return retVal;
217        }
218
219        private Boolean extractSendDeletesDstu3(org.hl7.fhir.dstu3.model.Subscription subscription) {
220                return subscription.getChannel().getExtensionsByUrl(EX_SEND_DELETE_MESSAGES).stream()
221                                .map(org.hl7.fhir.dstu3.model.Extension::getValue)
222                                .filter(val -> val instanceof org.hl7.fhir.dstu3.model.BooleanType)
223                                .map(val -> (org.hl7.fhir.dstu3.model.BooleanType) val)
224                                .map(org.hl7.fhir.dstu3.model.BooleanType::booleanValue)
225                                .findFirst()
226                                .orElse(false);
227        }
228
229        private @Nonnull Map<String, List<String>> extractExtension(IBaseResource theSubscription) {
230                try {
231                        switch (theSubscription.getStructureFhirVersionEnum()) {
232                                case DSTU2: {
233                                        ca.uhn.fhir.model.dstu2.resource.Subscription subscription =
234                                                        (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
235                                        return subscription.getChannel().getUndeclaredExtensions().stream()
236                                                        .collect(Collectors.groupingBy(
237                                                                        t -> t.getUrl(),
238                                                                        mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
239                                }
240                                case DSTU3: {
241                                        org.hl7.fhir.dstu3.model.Subscription subscription =
242                                                        (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
243                                        return subscription.getChannel().getExtension().stream()
244                                                        .collect(Collectors.groupingBy(
245                                                                        t -> t.getUrl(),
246                                                                        mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
247                                }
248                                case R4: {
249                                        org.hl7.fhir.r4.model.Subscription subscription =
250                                                        (org.hl7.fhir.r4.model.Subscription) theSubscription;
251                                        return subscription.getChannel().getExtension().stream()
252                                                        .collect(Collectors.groupingBy(
253                                                                        t -> t.getUrl(),
254                                                                        mapping(
255                                                                                        t -> {
256                                                                                                return t.getValueAsPrimitive().getValueAsString();
257                                                                                        },
258                                                                                        toList())));
259                                }
260                                case R5: {
261                                        // TODO KHS fix org.hl7.fhir.r4b.model.BaseResource.getStructureFhirVersionEnum() for R4B
262                                        if (theSubscription instanceof org.hl7.fhir.r4b.model.Subscription) {
263                                                org.hl7.fhir.r4b.model.Subscription subscription =
264                                                                (org.hl7.fhir.r4b.model.Subscription) theSubscription;
265                                                return subscription.getExtension().stream()
266                                                                .collect(Collectors.groupingBy(
267                                                                                t -> t.getUrl(),
268                                                                                mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
269                                        } else if (theSubscription instanceof org.hl7.fhir.r5.model.Subscription) {
270                                                org.hl7.fhir.r5.model.Subscription subscription =
271                                                                (org.hl7.fhir.r5.model.Subscription) theSubscription;
272                                                return subscription.getExtension().stream()
273                                                                .collect(Collectors.groupingBy(
274                                                                                t -> t.getUrl(),
275                                                                                mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
276                                        }
277                                }
278                                case DSTU2_HL7ORG:
279                                case DSTU2_1:
280                                default: {
281                                        ourLog.error(
282                                                        "Failed to extract extension from subscription {}",
283                                                        theSubscription.getIdElement().toUnqualified().getValue());
284                                        break;
285                                }
286                        }
287                } catch (FHIRException theE) {
288                        ourLog.error(
289                                        "Failed to extract extension from subscription {}",
290                                        theSubscription.getIdElement().toUnqualified().getValue(),
291                                        theE);
292                }
293                return Collections.emptyMap();
294        }
295
296        private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
297                org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
298                CanonicalSubscription retVal = new CanonicalSubscription();
299                retVal.setStatus(subscription.getStatus());
300                org.hl7.fhir.r4.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel();
301                retVal.setHeaders(channel.getHeader());
302                retVal.setChannelExtensions(extractExtension(subscription));
303                retVal.setIdElement(subscription.getIdElement());
304                retVal.setPayloadString(channel.getPayload());
305                retVal.setPayloadSearchCriteria(
306                                getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
307                retVal.setTags(extractTags(subscription));
308                setPartitionIdOnReturnValue(theSubscription, retVal);
309                retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription));
310
311                List<org.hl7.fhir.r4.model.CanonicalType> profiles =
312                                subscription.getMeta().getProfile();
313                for (org.hl7.fhir.r4.model.CanonicalType next : profiles) {
314                        if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) {
315                                retVal.setTopicSubscription(true);
316                        }
317                }
318
319                if (retVal.isTopicSubscription()) {
320                        CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription();
321                        topicSubscription.setTopic(getCriteria(theSubscription));
322
323                        retVal.setEndpointUrl(channel.getEndpoint());
324                        retVal.setChannelType(getChannelType(subscription));
325
326                        for (org.hl7.fhir.r4.model.Extension next :
327                                        subscription.getCriteriaElement().getExtension()) {
328                                if (SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL.equals(next.getUrl())) {
329                                        List<CanonicalTopicSubscriptionFilter> filters = CanonicalTopicSubscriptionFilter.fromQueryUrl(
330                                                        next.getValue().primitiveValue());
331                                        filters.forEach(topicSubscription::addFilter);
332                                }
333                        }
334
335                        if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL)) {
336                                org.hl7.fhir.r4.model.Extension channelHeartbeatPeriotUrlExtension = channel.getExtensionByUrl(
337                                                SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL);
338                                topicSubscription.setHeartbeatPeriod(Integer.valueOf(
339                                                channelHeartbeatPeriotUrlExtension.getValue().primitiveValue()));
340                        }
341                        if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL)) {
342                                org.hl7.fhir.r4.model.Extension channelTimeoutUrlExtension =
343                                                channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL);
344                                topicSubscription.setTimeout(
345                                                Integer.valueOf(channelTimeoutUrlExtension.getValue().primitiveValue()));
346                        }
347                        if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT)) {
348                                org.hl7.fhir.r4.model.Extension channelMaxCountExtension =
349                                                channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT);
350                                topicSubscription.setMaxCount(
351                                                Integer.valueOf(channelMaxCountExtension.getValue().primitiveValue()));
352                        }
353
354                        // setting full-resource PayloadContent if backport-payload-content is not provided
355                        org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent =
356                                        org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE;
357
358                        org.hl7.fhir.r4.model.Extension channelPayloadContentExtension = channel.getPayloadElement()
359                                        .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT);
360
361                        if (nonNull(channelPayloadContentExtension)) {
362                                payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode(
363                                                channelPayloadContentExtension.getValue().primitiveValue());
364                        }
365
366                        topicSubscription.setContent(payloadContent);
367                } else {
368                        retVal.setCriteriaString(getCriteria(theSubscription));
369                        retVal.setEndpointUrl(channel.getEndpoint());
370                        retVal.setChannelType(getChannelType(subscription));
371                }
372
373                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
374                        String from;
375                        String subjectTemplate;
376                        try {
377                                from = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
378                                subjectTemplate = channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
379                        } catch (FHIRException theE) {
380                                throw new ConfigurationException(
381                                                Msg.code(561) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
382                        }
383                        retVal.getEmailDetails().setFrom(from);
384                        retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
385                }
386
387                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
388                        String stripVersionIds;
389                        String deliverLatestVersion;
390                        try {
391                                stripVersionIds =
392                                                channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
393                                deliverLatestVersion =
394                                                channel.getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
395                        } catch (FHIRException theE) {
396                                throw new ConfigurationException(
397                                                Msg.code(562) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
398                        }
399                        retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
400                        retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
401                }
402
403                List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
404                if (!topicExts.isEmpty()) {
405                        IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
406                        if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
407                                throw new PreconditionFailedException(Msg.code(563) + "Topic reference must be an EventDefinition");
408                        }
409                }
410
411                Extension extension = channel.getExtensionByUrl(EX_SEND_DELETE_MESSAGES);
412                if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) {
413                        retVal.setSendDeleteMessages(((BooleanType) extension.getValue()).booleanValue());
414                }
415                return retVal;
416        }
417
418        private CanonicalSubscription canonicalizeR4B(IBaseResource theSubscription) {
419                org.hl7.fhir.r4b.model.Subscription subscription = (org.hl7.fhir.r4b.model.Subscription) theSubscription;
420
421                CanonicalSubscription retVal = new CanonicalSubscription();
422                org.hl7.fhir.r4b.model.Enumerations.SubscriptionStatus status = subscription.getStatus();
423                if (status != null) {
424                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
425                }
426                setPartitionIdOnReturnValue(theSubscription, retVal);
427                org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelComponent channel = subscription.getChannel();
428                retVal.setHeaders(channel.getHeader());
429                retVal.setChannelExtensions(extractExtension(subscription));
430                retVal.setIdElement(subscription.getIdElement());
431                retVal.setPayloadString(channel.getPayload());
432                retVal.setPayloadSearchCriteria(
433                                getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
434                retVal.setTags(extractTags(subscription));
435
436                List<org.hl7.fhir.r4b.model.CanonicalType> profiles =
437                                subscription.getMeta().getProfile();
438                for (org.hl7.fhir.r4b.model.CanonicalType next : profiles) {
439                        if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) {
440                                retVal.setTopicSubscription(true);
441                        }
442                }
443
444                if (retVal.isTopicSubscription()) {
445                        CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription();
446                        topicSubscription.setTopic(getCriteria(theSubscription));
447
448                        retVal.setEndpointUrl(channel.getEndpoint());
449                        retVal.setChannelType(getChannelType(subscription));
450
451                        // setting full-resource PayloadContent if backport-payload-content is not provided
452                        org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent =
453                                        org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE;
454
455                        org.hl7.fhir.r4b.model.Extension channelPayloadContentExtension = channel.getPayloadElement()
456                                        .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT);
457
458                        if (nonNull(channelPayloadContentExtension)) {
459                                payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode(
460                                                channelPayloadContentExtension.getValue().primitiveValue());
461                        }
462
463                        topicSubscription.setContent(payloadContent);
464                } else {
465                        retVal.setCriteriaString(getCriteria(theSubscription));
466                        retVal.setEndpointUrl(channel.getEndpoint());
467                        retVal.setChannelType(getChannelType(subscription));
468                }
469
470                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
471                        String from;
472                        String subjectTemplate;
473                        try {
474                                from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
475                                subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
476                        } catch (FHIRException theE) {
477                                throw new ConfigurationException(
478                                                Msg.code(564) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
479                        }
480                        retVal.getEmailDetails().setFrom(from);
481                        retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
482                }
483
484                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
485                        String stripVersionIds;
486                        String deliverLatestVersion;
487                        try {
488                                stripVersionIds =
489                                                getExtensionString(channel, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
490                                deliverLatestVersion =
491                                                getExtensionString(channel, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
492                        } catch (FHIRException theE) {
493                                throw new ConfigurationException(
494                                                Msg.code(565) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
495                        }
496                        retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
497                        retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
498                }
499
500                List<org.hl7.fhir.r4b.model.Extension> topicExts =
501                                subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
502                if (!topicExts.isEmpty()) {
503                        IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
504                        if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
505                                throw new PreconditionFailedException(Msg.code(566) + "Topic reference must be an EventDefinition");
506                        }
507                }
508
509                org.hl7.fhir.r4b.model.Extension extension = channel.getExtensionByUrl(EX_SEND_DELETE_MESSAGES);
510                if (extension != null && extension.hasValue() && extension.hasValueBooleanType()) {
511                        retVal.setSendDeleteMessages(extension.getValueBooleanType().booleanValue());
512                }
513
514                retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription));
515
516                return retVal;
517        }
518
519        private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) {
520                org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
521
522                CanonicalSubscription retVal = new CanonicalSubscription();
523
524                setPartitionIdOnReturnValue(theSubscription, retVal);
525                retVal.setChannelExtensions(extractExtension(subscription));
526                retVal.setIdElement(subscription.getIdElement());
527                retVal.setPayloadString(subscription.getContentType());
528                retVal.setPayloadSearchCriteria(
529                                getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
530                retVal.setTags(extractTags(subscription));
531
532                List<org.hl7.fhir.r5.model.Extension> topicExts =
533                                subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
534                if (!topicExts.isEmpty()) {
535                        IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
536                        if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
537                                throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition");
538                        }
539                }
540
541                // All R5 subscriptions are topic subscriptions
542                retVal.setTopicSubscription(true);
543
544                Enumerations.SubscriptionStatusCodes status = subscription.getStatus();
545                if (status != null) {
546                        switch (status) {
547                                case REQUESTED:
548                                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.REQUESTED);
549                                        break;
550                                case ACTIVE:
551                                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ACTIVE);
552                                        break;
553                                case ERROR:
554                                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ERROR);
555                                        break;
556                                case OFF:
557                                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.OFF);
558                                        break;
559                                case NULL:
560                                case ENTEREDINERROR:
561                                default:
562                                        ourLog.warn("Converting R5 Subscription status from {} to ERROR", status);
563                                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.ERROR);
564                        }
565                }
566                retVal.getTopicSubscription().setContent(subscription.getContent());
567                retVal.setEndpointUrl(subscription.getEndpoint());
568                retVal.getTopicSubscription().setTopic(subscription.getTopic());
569                retVal.setChannelType(getChannelType(subscription));
570
571                subscription.getFilterBy().forEach(filter -> retVal.getTopicSubscription()
572                                .addFilter(convertFilter(filter)));
573
574                retVal.getTopicSubscription().setHeartbeatPeriod(subscription.getHeartbeatPeriod());
575                retVal.getTopicSubscription().setMaxCount(subscription.getMaxCount());
576
577                setR5FlagsBasedOnChannelType(subscription, retVal);
578
579                retVal.setCrossPartitionEnabled(handleCrossPartition(theSubscription));
580
581                return retVal;
582        }
583
584        private void setR5FlagsBasedOnChannelType(
585                        org.hl7.fhir.r5.model.Subscription subscription, CanonicalSubscription retVal) {
586                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
587                        String from;
588                        String subjectTemplate;
589                        try {
590                                from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
591                                subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
592                        } catch (FHIRException theE) {
593                                throw new ConfigurationException(
594                                                Msg.code(2323) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
595                        }
596                        retVal.getEmailDetails().setFrom(from);
597                        retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
598                }
599
600                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
601                        String stripVersionIds;
602                        String deliverLatestVersion;
603                        try {
604                                stripVersionIds =
605                                                getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
606                                deliverLatestVersion = getExtensionString(
607                                                subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
608                        } catch (FHIRException theE) {
609                                throw new ConfigurationException(
610                                                Msg.code(2324) + "Failed to extract subscription extension(s): " + theE.getMessage(), theE);
611                        }
612                        retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
613                        retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
614                }
615        }
616
617        private CanonicalTopicSubscriptionFilter convertFilter(
618                        org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent theFilter) {
619                CanonicalTopicSubscriptionFilter retVal = new CanonicalTopicSubscriptionFilter();
620                retVal.setResourceType(theFilter.getResourceType());
621                retVal.setFilterParameter(theFilter.getFilterParameter());
622                retVal.setModifier(theFilter.getModifier());
623                retVal.setComparator(theFilter.getComparator());
624                retVal.setValue(theFilter.getValue());
625                return retVal;
626        }
627
628        private void setPartitionIdOnReturnValue(IBaseResource theSubscription, CanonicalSubscription retVal) {
629                RequestPartitionId requestPartitionId =
630                                (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID);
631                if (requestPartitionId != null) {
632                        retVal.setPartitionId(requestPartitionId.getFirstPartitionIdOrNull());
633                }
634        }
635
636        private String getExtensionString(IBaseHasExtensions theBase, String theUrl) {
637                return theBase.getExtension().stream()
638                                .filter(t -> theUrl.equals(t.getUrl()))
639                                .filter(t -> t.getValue() instanceof IPrimitiveType)
640                                .map(t -> (IPrimitiveType<?>) t.getValue())
641                                .map(t -> t.getValueAsString())
642                                .findFirst()
643                                .orElse(null);
644        }
645
646        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
647        public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) {
648                CanonicalSubscriptionChannelType retVal = null;
649
650                switch (myFhirContext.getVersion().getVersion()) {
651                        case DSTU2: {
652                                String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription)
653                                                .getChannel()
654                                                .getType();
655                                retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
656                                break;
657                        }
658                        case DSTU3: {
659                                org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type =
660                                                ((org.hl7.fhir.dstu3.model.Subscription) theSubscription)
661                                                                .getChannel()
662                                                                .getType();
663                                if (type != null) {
664                                        String channelTypeCode = type.toCode();
665                                        retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
666                                }
667                                break;
668                        }
669                        case R4: {
670                                org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription)
671                                                                theSubscription)
672                                                .getChannel()
673                                                .getType();
674                                if (type != null) {
675                                        String channelTypeCode = type.toCode();
676                                        retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
677                                }
678                                break;
679                        }
680                        case R4B: {
681                                org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelType type =
682                                                ((org.hl7.fhir.r4b.model.Subscription) theSubscription)
683                                                                .getChannel()
684                                                                .getType();
685                                if (type != null) {
686                                        String channelTypeCode = type.toCode();
687                                        retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
688                                }
689                                break;
690                        }
691                        case R5: {
692                                org.hl7.fhir.r5.model.Coding nextTypeCode =
693                                                ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannelType();
694                                CanonicalSubscriptionChannelType code =
695                                                CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode());
696                                if (code != null) {
697                                        retVal = code;
698                                }
699                                break;
700                        }
701                        default:
702                                throw new IllegalStateException(Msg.code(2326) + "Unsupported Subscription FHIR version: "
703                                                + myFhirContext.getVersion().getVersion());
704                }
705
706                return retVal;
707        }
708
709        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
710        @Nullable
711        public String getCriteria(IBaseResource theSubscription) {
712                String retVal = null;
713
714                switch (myFhirContext.getVersion().getVersion()) {
715                        case DSTU2:
716                                retVal = ((Subscription) theSubscription).getCriteria();
717                                break;
718                        case DSTU3:
719                                retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria();
720                                break;
721                        case R4:
722                                retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria();
723                                break;
724                        case R4B:
725                                retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria();
726                                break;
727                        case R5:
728                        default:
729                                throw new IllegalStateException(
730                                                Msg.code(2327) + "Subscription criteria is not supported for FHIR version: "
731                                                                + myFhirContext.getVersion().getVersion());
732                }
733
734                return retVal;
735        }
736
737        public void setMatchingStrategyTag(
738                        @Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) {
739                IBaseMetaType meta = theSubscription.getMeta();
740
741                // Remove any existing strategy tag
742                meta.getTag().stream()
743                                .filter(t -> HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem()))
744                                .forEach(t -> {
745                                        t.setCode(null);
746                                        t.setSystem(null);
747                                        t.setDisplay(null);
748                                });
749
750                if (theStrategy == null) {
751                        return;
752                }
753
754                String value = theStrategy.toString();
755                String display;
756
757                if (theStrategy == SubscriptionMatchingStrategy.DATABASE) {
758                        display = "Database";
759                } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) {
760                        display = "In-memory";
761                } else if (theStrategy == SubscriptionMatchingStrategy.TOPIC) {
762                        display = "SubscriptionTopic";
763                } else {
764                        throw new IllegalStateException(Msg.code(567) + "Unknown "
765                                        + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy);
766                }
767                meta.addTag()
768                                .setSystem(HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY)
769                                .setCode(value)
770                                .setDisplay(display);
771        }
772
773        public String getSubscriptionStatus(IBaseResource theSubscription) {
774                final IPrimitiveType<?> status = myFhirContext
775                                .newTerser()
776                                .getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class);
777                if (status == null) {
778                        return null;
779                }
780                return status.getValueAsString();
781        }
782
783        private boolean handleCrossPartition(IBaseResource theSubscription) {
784                RequestPartitionId requestPartitionId =
785                                (RequestPartitionId) theSubscription.getUserData(Constants.RESOURCE_PARTITION_ID);
786
787                boolean isSubscriptionCreatedOnDefaultPartition = false;
788
789                if (nonNull(requestPartitionId)) {
790                        isSubscriptionCreatedOnDefaultPartition = requestPartitionId.isDefaultPartition();
791                }
792
793                boolean isSubscriptionDefinededAsCrossPartitionSubscription =
794                                SubscriptionUtil.isDefinedAsCrossPartitionSubcription(theSubscription);
795                boolean isGlobalSettingCrossPartitionSubscriptionEnabled =
796                                mySubscriptionSettings.isCrossPartitionSubscriptionEnabled();
797
798                return isSubscriptionCreatedOnDefaultPartition
799                                && isSubscriptionDefinededAsCrossPartitionSubscription
800                                && isGlobalSettingCrossPartitionSubscriptionEnabled;
801        }
802}