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