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