001package ca.uhn.fhir.jpa.subscription.match.registry;
002
003/*-
004 * #%L
005 * HAPI FHIR Subscription Server
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
026import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
027import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
028import ca.uhn.fhir.model.dstu2.resource.Subscription;
029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
030import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
031import ca.uhn.fhir.util.HapiExtensions;
032import org.apache.commons.lang3.Validate;
033import org.hl7.fhir.exceptions.FHIRException;
034import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
035import org.hl7.fhir.instance.model.api.IBaseMetaType;
036import org.hl7.fhir.instance.model.api.IBaseReference;
037import org.hl7.fhir.instance.model.api.IBaseResource;
038import org.hl7.fhir.instance.model.api.IPrimitiveType;
039import org.hl7.fhir.r4.model.Extension;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042import org.springframework.beans.factory.annotation.Autowired;
043
044import javax.annotation.Nonnull;
045import javax.annotation.Nullable;
046import java.util.Collections;
047import java.util.List;
048import java.util.Map;
049import java.util.stream.Collectors;
050
051import static java.util.stream.Collectors.mapping;
052import static java.util.stream.Collectors.toList;
053
054public class SubscriptionCanonicalizer {
055        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class);
056
057        final FhirContext myFhirContext;
058
059        @Autowired
060        public SubscriptionCanonicalizer(FhirContext theFhirContext) {
061                myFhirContext = theFhirContext;
062        }
063
064        public CanonicalSubscription canonicalize(IBaseResource theSubscription) {
065                switch (myFhirContext.getVersion().getVersion()) {
066                        case DSTU2:
067                                return canonicalizeDstu2(theSubscription);
068                        case DSTU3:
069                                return canonicalizeDstu3(theSubscription);
070                        case R4:
071                                return canonicalizeR4(theSubscription);
072                        case R5:
073                                return canonicalizeR5(theSubscription);
074                        case DSTU2_HL7ORG:
075                        case DSTU2_1:
076                        default:
077                                throw new ConfigurationException("Subscription not supported for version: " + myFhirContext.getVersion().getVersion());
078                }
079        }
080
081        private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
082                ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
083
084                CanonicalSubscription retVal = new CanonicalSubscription();
085                try {
086                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
087                        retVal.setChannelType(getChannelType(theSubscription));
088                        retVal.setCriteriaString(subscription.getCriteria());
089                        retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
090                        retVal.setHeaders(subscription.getChannel().getHeader());
091                        retVal.setChannelExtensions(extractExtension(subscription));
092                        retVal.setIdElement(subscription.getIdElement());
093                        retVal.setPayloadString(subscription.getChannel().getPayload());
094                } catch (FHIRException theE) {
095                        throw new InternalErrorException(theE);
096                }
097                return retVal;
098        }
099
100        private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
101                org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
102
103                CanonicalSubscription retVal = new CanonicalSubscription();
104                try {
105                        org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus status = subscription.getStatus();
106                        if (status != null) {
107                                retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
108                        }
109                        retVal.setChannelType(getChannelType(theSubscription));
110                        retVal.setCriteriaString(subscription.getCriteria());
111                        retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
112                        retVal.setHeaders(subscription.getChannel().getHeader());
113                        retVal.setChannelExtensions(extractExtension(subscription));
114                        retVal.setIdElement(subscription.getIdElement());
115                        retVal.setPayloadString(subscription.getChannel().getPayload());
116                        retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
117
118                        if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
119                                String from;
120                                String subjectTemplate;
121
122                                try {
123                                        from = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
124                                        subjectTemplate = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
125                                } catch (FHIRException theE) {
126                                        throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
127                                }
128                                retVal.getEmailDetails().setFrom(from);
129                                retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
130                        }
131
132                        if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
133
134                                String stripVersionIds;
135                                String deliverLatestVersion;
136                                try {
137                                        stripVersionIds = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
138                                        deliverLatestVersion = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
139                                } catch (FHIRException theE) {
140                                        throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
141                                }
142                                retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
143                                retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
144                        }
145
146                } catch (FHIRException theE) {
147                        throw new InternalErrorException(theE);
148                }
149                return retVal;
150        }
151
152        private @Nonnull
153        Map<String, List<String>> extractExtension(IBaseResource theSubscription) {
154                try {
155                        switch (theSubscription.getStructureFhirVersionEnum()) {
156                                case DSTU2: {
157                                        ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
158                                        return subscription
159                                                .getChannel()
160                                                .getUndeclaredExtensions()
161                                                .stream()
162                                                .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
163                                }
164                                case DSTU3: {
165                                        org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
166                                        return subscription
167                                                .getChannel()
168                                                .getExtension()
169                                                .stream()
170                                                .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
171                                }
172                                case R4: {
173                                        org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
174                                        return subscription
175                                                .getChannel()
176                                                .getExtension()
177                                                .stream()
178                                                .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
179                                }
180                                case R5: {
181                                        org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
182                                        return subscription
183                                                .getExtension()
184                                                .stream()
185                                                .collect(Collectors.groupingBy(t -> t.getUrl(), mapping(t -> t.getValueAsPrimitive().getValueAsString(), toList())));
186                                }
187                                case DSTU2_HL7ORG:
188                                case DSTU2_1:
189                                default: {
190                                        ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
191                                        break;
192                                }
193                        }
194                } catch (FHIRException theE) {
195                        ourLog.error("Failed to extract extension from subscription {}", theSubscription.getIdElement().toUnqualified().getValue(), theE);
196                }
197                return Collections.emptyMap();
198        }
199
200        private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
201                org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
202
203                CanonicalSubscription retVal = new CanonicalSubscription();
204                retVal.setStatus(subscription.getStatus());
205                retVal.setChannelType(getChannelType(theSubscription));
206                retVal.setCriteriaString(subscription.getCriteria());
207                retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
208                retVal.setHeaders(subscription.getChannel().getHeader());
209                retVal.setChannelExtensions(extractExtension(subscription));
210                retVal.setIdElement(subscription.getIdElement());
211                retVal.setPayloadString(subscription.getChannel().getPayload());
212                retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
213
214                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
215                        String from;
216                        String subjectTemplate;
217                        try {
218                                from = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
219                                subjectTemplate = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
220                        } catch (FHIRException theE) {
221                                throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
222                        }
223                        retVal.getEmailDetails().setFrom(from);
224                        retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
225                }
226
227                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
228                        String stripVersionIds;
229                        String deliverLatestVersion;
230                        try {
231                                stripVersionIds = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
232                                deliverLatestVersion = subscription.getChannel().getExtensionString(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
233                        } catch (FHIRException theE) {
234                                throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
235                        }
236                        retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
237                        retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
238                }
239
240                List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
241                if (topicExts.size() > 0) {
242                        IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
243                        if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
244                                throw new PreconditionFailedException("Topic reference must be an EventDefinition");
245                        }
246                }
247
248                return retVal;
249        }
250
251        private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) {
252                org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
253
254                CanonicalSubscription retVal = new CanonicalSubscription();
255                org.hl7.fhir.r5.model.Enumerations.SubscriptionState status = subscription.getStatus();
256                if (status != null) {
257                        retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
258                }
259                retVal.setChannelType(getChannelType(subscription));
260                retVal.setCriteriaString(getCriteria(theSubscription));
261                retVal.setEndpointUrl(subscription.getEndpoint());
262                retVal.setHeaders(subscription.getHeader());
263                retVal.setChannelExtensions(extractExtension(subscription));
264                retVal.setIdElement(subscription.getIdElement());
265                retVal.setPayloadString(subscription.getContentType());
266                retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
267
268                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
269                        String from;
270                        String subjectTemplate;
271                        try {
272                                from = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_EMAIL_FROM);
273                                subjectTemplate = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
274                        } catch (FHIRException theE) {
275                                throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
276                        }
277                        retVal.getEmailDetails().setFrom(from);
278                        retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
279                }
280
281                if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
282                        String stripVersionIds;
283                        String deliverLatestVersion;
284                        try {
285                                stripVersionIds = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
286                                deliverLatestVersion = getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
287                        } catch (FHIRException theE) {
288                                throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
289                        }
290                        retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
291                        retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
292                }
293
294                List<org.hl7.fhir.r5.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
295                if (topicExts.size() > 0) {
296                        IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
297                        if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
298                                throw new PreconditionFailedException("Topic reference must be an EventDefinition");
299                        }
300                }
301
302                return retVal;
303        }
304
305        private String getExtensionString(IBaseHasExtensions theBase, String theUrl) {
306                return theBase
307                        .getExtension()
308                        .stream()
309                        .filter(t -> theUrl.equals(t.getUrl()))
310                        .filter(t -> t.getValue() instanceof IPrimitiveType)
311                        .map(t -> (IPrimitiveType<?>) t.getValue())
312                        .map(t -> t.getValueAsString())
313                        .findFirst()
314                        .orElse(null);
315        }
316
317        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
318        public CanonicalSubscriptionChannelType getChannelType(IBaseResource theSubscription) {
319                CanonicalSubscriptionChannelType retVal = null;
320
321                switch (myFhirContext.getVersion().getVersion()) {
322                        case DSTU2: {
323                                String channelTypeCode = ((ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription).getChannel().getType();
324                                retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
325                                break;
326                        }
327                        case DSTU3: {
328                                org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getChannel().getType();
329                                if (type != null) {
330                                        String channelTypeCode = type.toCode();
331                                        retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
332                                }
333                                break;
334                        }
335                        case R4: {
336                                org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType type = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getChannel().getType();
337                                if (type != null) {
338                                        String channelTypeCode = type.toCode();
339                                        retVal = CanonicalSubscriptionChannelType.fromCode(null, channelTypeCode);
340                                }
341                                break;
342                        }
343                        case R5: {
344                                org.hl7.fhir.r5.model.Coding nextTypeCode = ((org.hl7.fhir.r5.model.Subscription) theSubscription).getChannelType();
345                                CanonicalSubscriptionChannelType code = CanonicalSubscriptionChannelType.fromCode(nextTypeCode.getSystem(), nextTypeCode.getCode());
346                                if (code != null) {
347                                        retVal = code;
348                                }
349                                break;
350                        }
351                }
352
353                return retVal;
354        }
355
356        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
357        public String getCriteria(IBaseResource theSubscription) {
358                String retVal = null;
359
360                switch (myFhirContext.getVersion().getVersion()) {
361                        case DSTU2:
362                                retVal = ((Subscription) theSubscription).getCriteria();
363                                break;
364                        case DSTU3:
365                                retVal = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getCriteria();
366                                break;
367                        case R4:
368                                retVal = ((org.hl7.fhir.r4.model.Subscription) theSubscription).getCriteria();
369                                break;
370                        case R5:
371                                org.hl7.fhir.r5.model.SubscriptionTopic topic = (org.hl7.fhir.r5.model.SubscriptionTopic) ((org.hl7.fhir.r5.model.Subscription) theSubscription).getTopic().getResource();
372                                Validate.notNull(topic);
373                                retVal = topic.getResourceTrigger().getQueryCriteria().getCurrent();
374                                break;
375                }
376
377                return retVal;
378        }
379
380
381        public void setMatchingStrategyTag(@Nonnull IBaseResource theSubscription, @Nullable SubscriptionMatchingStrategy theStrategy) {
382                IBaseMetaType meta = theSubscription.getMeta();
383
384                // Remove any existing strategy tag
385                meta
386                        .getTag()
387                        .stream()
388                        .filter(t -> HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(t.getSystem()))
389                        .forEach(t -> {
390                                t.setCode(null);
391                                t.setSystem(null);
392                                t.setDisplay(null);
393                        });
394
395                if (theStrategy == null) {
396                        return;
397                }
398
399                String value = theStrategy.toString();
400                String display;
401
402                if (theStrategy == SubscriptionMatchingStrategy.DATABASE) {
403                        display = "Database";
404                } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) {
405                        display = "In-memory";
406                } else {
407                        throw new IllegalStateException("Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": " + theStrategy);
408                }
409                meta.addTag().setSystem(HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display);
410        }
411
412        public String getSubscriptionStatus(IBaseResource theSubscription) {
413                final IPrimitiveType<?> status = myFhirContext.newTerser().getSingleValueOrNull(theSubscription, SubscriptionConstants.SUBSCRIPTION_STATUS, IPrimitiveType.class);
414                if (status == null) {
415                        return null;
416                }
417                return status.getValueAsString();
418        }
419
420}