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