001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.support.ConceptValidationOptions;
025import ca.uhn.fhir.context.support.IValidationSupport;
026import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
027import ca.uhn.fhir.context.support.LookupCodeRequest;
028import ca.uhn.fhir.context.support.ValidationSupportContext;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
031import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
032import ca.uhn.fhir.jpa.api.dao.ReindexOutcome;
033import ca.uhn.fhir.jpa.api.dao.ReindexParameters;
034import ca.uhn.fhir.jpa.api.model.ReindexJobStatus;
035import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
036import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
037import ca.uhn.fhir.jpa.model.entity.ResourceTable;
038import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
039import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
040import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
041import ca.uhn.fhir.jpa.util.LogicUtil;
042import ca.uhn.fhir.rest.api.server.RequestDetails;
043import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
044import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
045import ca.uhn.fhir.rest.param.TokenParam;
046import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
047import ca.uhn.fhir.util.FhirTerser;
048import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
049import jakarta.annotation.Nonnull;
050import jakarta.annotation.PostConstruct;
051import org.apache.commons.collections4.CollectionUtils;
052import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
053import org.hl7.fhir.instance.model.api.IBaseCoding;
054import org.hl7.fhir.instance.model.api.IBaseDatatype;
055import org.hl7.fhir.instance.model.api.IBaseResource;
056import org.hl7.fhir.instance.model.api.IIdType;
057import org.hl7.fhir.instance.model.api.IPrimitiveType;
058import org.hl7.fhir.r4.model.CodeableConcept;
059import org.hl7.fhir.r4.model.Coding;
060import org.springframework.beans.factory.annotation.Autowired;
061
062import java.util.ArrayList;
063import java.util.Collection;
064import java.util.Date;
065import java.util.List;
066import java.util.stream.Collectors;
067
068import static ca.uhn.fhir.util.DatatypeUtil.toStringValue;
069import static org.apache.commons.lang3.StringUtils.isNotBlank;
070
071public class JpaResourceDaoCodeSystem<T extends IBaseResource> extends BaseHapiFhirResourceDao<T>
072                implements IFhirResourceDaoCodeSystem<T> {
073
074        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceDaoCodeSystem.class);
075
076        @Autowired
077        protected ITermCodeSystemStorageSvc myTerminologyCodeSystemStorageSvc;
078
079        @Autowired
080        protected IIdHelperService myIdHelperService;
081
082        @Autowired
083        protected ITermDeferredStorageSvc myTermDeferredStorageSvc;
084
085        @Autowired
086        private IValidationSupport myValidationSupport;
087
088        @Autowired
089        private FhirContext myFhirContext;
090
091        private FhirTerser myTerser;
092
093        @Autowired
094        private VersionCanonicalizer myVersionCanonicalizer;
095
096        @Override
097        @PostConstruct
098        public void start() {
099                super.start();
100                myTerser = myFhirContext.newTerser();
101        }
102
103        @Override
104        public List<IIdType> findCodeSystemIdsContainingSystemAndCode(
105                        String theCode, String theSystem, RequestDetails theRequest) {
106                List<IIdType> valueSetIds;
107                List<IResourcePersistentId> ids = searchForIds(
108                                new SearchParameterMap(org.hl7.fhir.r4.model.CodeSystem.SP_CODE, new TokenParam(theSystem, theCode)),
109                                theRequest);
110                valueSetIds = new ArrayList<>();
111                for (IResourcePersistentId next : ids) {
112                        IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "CodeSystem", next);
113                        valueSetIds.add(id);
114                }
115                return valueSetIds;
116        }
117
118        @Nonnull
119        @Override
120        public IValidationSupport.LookupCodeResult lookupCode(
121                        IPrimitiveType<String> theCode,
122                        IPrimitiveType<String> theSystem,
123                        IBaseCoding theCoding,
124                        RequestDetails theRequestDetails) {
125                return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails);
126        }
127
128        @Nonnull
129        @Override
130        public IValidationSupport.LookupCodeResult lookupCode(
131                        IPrimitiveType<String> theCode,
132                        IPrimitiveType<String> theSystem,
133                        IBaseCoding theCoding,
134                        IPrimitiveType<String> theDisplayLanguage,
135                        RequestDetails theRequestDetails) {
136                return lookupCode(
137                                theCode,
138                                theSystem,
139                                theCoding,
140                                theDisplayLanguage,
141                                CollectionUtils.emptyCollection(),
142                                theRequestDetails);
143        }
144
145        @Nonnull
146        @Override
147        public IValidationSupport.LookupCodeResult lookupCode(
148                        IPrimitiveType<String> theCode,
149                        IPrimitiveType<String> theSystem,
150                        IBaseCoding theCoding,
151                        IPrimitiveType<String> theDisplayLanguage,
152                        Collection<IPrimitiveType<String>> thePropertyNames,
153                        RequestDetails theRequestDetails) {
154                return doLookupCode(
155                                myFhirContext,
156                                myTerser,
157                                myValidationSupport,
158                                theCode,
159                                theSystem,
160                                theCoding,
161                                theDisplayLanguage,
162                                thePropertyNames);
163        }
164
165        @Override
166        public SubsumesResult subsumes(
167                        IPrimitiveType<String> theCodeA,
168                        IPrimitiveType<String> theCodeB,
169                        IPrimitiveType<String> theSystem,
170                        IBaseCoding theCodingA,
171                        IBaseCoding theCodingB,
172                        RequestDetails theRequestDetails) {
173                return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB);
174        }
175
176        @Override
177        protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) {
178                super.preDelete(theResourceToDelete, theEntityToDelete, theRequestDetails);
179
180                myTermDeferredStorageSvc.deleteCodeSystemForResource(theEntityToDelete);
181        }
182
183        /**
184         * If there are more code systems to process
185         * than {@link JpaStorageSettings#getDeferIndexingForCodesystemsOfSize()},
186         * then these codes will have their processing deferred (for a later time).
187         *
188         * This can result in future reindex steps *skipping* these code systems (if
189         * they're still deferred) and thus incorrect expansions resulting.
190         *
191         * So we override the reindex method for CodeSystems specifically to
192         * force reindex batch jobs to wait until all code systems are processed before
193         * moving on.
194         */
195        @SuppressWarnings("rawtypes")
196        @Override
197        public ReindexOutcome reindex(
198                        IResourcePersistentId thePid,
199                        ReindexParameters theReindexParameters,
200                        RequestDetails theRequest,
201                        TransactionDetails theTransactionDetails) {
202                ReindexOutcome outcome = super.reindex(thePid, theReindexParameters, theRequest, theTransactionDetails);
203
204                if (outcome.getWarnings().isEmpty()) {
205                        outcome.setHasPendingWork(true);
206                }
207                return outcome;
208        }
209
210        @Override
211        public ReindexJobStatus getReindexJobStatus() {
212                boolean isQueueEmpty = myTermDeferredStorageSvc.isStorageQueueEmpty(true);
213
214                ReindexJobStatus status = new ReindexJobStatus();
215                status.setHasReindexWorkPending(!isQueueEmpty);
216                if (status.isHasReindexWorkPending()) {
217                        // force a run
218                        myTermDeferredStorageSvc.saveDeferred();
219                }
220
221                return status;
222        }
223
224        @Override
225        public ResourceTable updateEntity(
226                        RequestDetails theRequest,
227                        IBaseResource theResource,
228                        IBasePersistedResource theEntity,
229                        Date theDeletedTimestampOrNull,
230                        boolean thePerformIndexing,
231                        boolean theUpdateVersion,
232                        TransactionDetails theTransactionDetails,
233                        boolean theForceUpdate,
234                        boolean theCreateNewHistoryEntry) {
235                ResourceTable retVal = super.updateEntity(
236                                theRequest,
237                                theResource,
238                                theEntity,
239                                theDeletedTimestampOrNull,
240                                thePerformIndexing,
241                                theUpdateVersion,
242                                theTransactionDetails,
243                                theForceUpdate,
244                                theCreateNewHistoryEntry);
245                if (thePerformIndexing) {
246                        if (!retVal.isUnchangedInCurrentOperation()) {
247
248                                org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource);
249                                addPidToResource(theEntity, cs);
250
251                                myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded(
252                                                cs, (ResourceTable) theEntity, theRequest);
253                        }
254                }
255
256                return retVal;
257        }
258
259        @Nonnull
260        @Override
261        public CodeValidationResult validateCode(
262                        IIdType theCodeSystemId,
263                        IPrimitiveType<String> theCodeSystemUrl,
264                        IPrimitiveType<String> theVersion,
265                        IPrimitiveType<String> theCode,
266                        IPrimitiveType<String> theDisplay,
267                        IBaseCoding theCoding,
268                        IBaseDatatype theCodeableConcept,
269                        RequestDetails theRequestDetails) {
270
271                CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
272                boolean haveCodeableConcept =
273                                codeableConcept != null && codeableConcept.getCoding().size() > 0;
274
275                Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding);
276                boolean haveCoding = coding != null && !coding.isEmpty();
277
278                String code = toStringValue(theCode);
279                boolean haveCode = isNotBlank(code);
280
281                if (!haveCodeableConcept && !haveCoding && !haveCode) {
282                        throw new InvalidRequestException(
283                                        Msg.code(906) + "No code, coding, or codeableConcept provided to validate.");
284                }
285                if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) {
286                        throw new InvalidRequestException(
287                                        Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)");
288                }
289
290                String codeSystemUrl;
291                if (theCodeSystemId != null) {
292                        IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails);
293                        codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem);
294                } else if (isNotBlank(toStringValue(theCodeSystemUrl))) {
295                        codeSystemUrl = toStringValue(theCodeSystemUrl);
296                } else {
297                        throw new InvalidRequestException(Msg.code(908)
298                                        + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate.");
299                }
300
301                if (haveCodeableConcept) {
302                        CodeValidationResult anyValidation = null;
303                        for (int i = 0; i < codeableConcept.getCoding().size(); i++) {
304                                Coding nextCoding = codeableConcept.getCoding().get(i);
305                                if (nextCoding.hasSystem()) {
306                                        if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) {
307                                                throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem()
308                                                                + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
309                                        }
310                                        codeSystemUrl = nextCoding.getSystem();
311                                }
312                                code = nextCoding.getCode();
313                                String display = nextCoding.getDisplay();
314                                CodeValidationResult nextValidation =
315                                                codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
316                                anyValidation = nextValidation;
317                                if (nextValidation.isOk()) {
318                                        return nextValidation;
319                                }
320                        }
321                        return anyValidation;
322                } else if (haveCoding) {
323                        if (coding.hasSystem()) {
324                                if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) {
325                                        throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem()
326                                                        + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
327                                }
328                                codeSystemUrl = coding.getSystem();
329                        }
330                        code = coding.getCode();
331                        String display = coding.getDisplay();
332                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
333                } else {
334                        String display = toStringValue(theDisplay);
335                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
336                }
337        }
338
339        private CodeValidationResult codeSystemValidateCode(
340                        String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) {
341                ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
342                ConceptValidationOptions options = new ConceptValidationOptions();
343                options.setValidateDisplay(isNotBlank(theDisplay));
344
345                String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion);
346
347                CodeValidationResult retVal =
348                                myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null);
349                if (retVal == null) {
350                        retVal = new CodeValidationResult();
351                        retVal.setMessage(
352                                        "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode);
353                }
354                return retVal;
355        }
356
357        public static IValidationSupport.LookupCodeResult doLookupCode(
358                        FhirContext theFhirContext,
359                        FhirTerser theFhirTerser,
360                        IValidationSupport theValidationSupport,
361                        IPrimitiveType<String> theCode,
362                        IPrimitiveType<String> theSystem,
363                        IBaseCoding theCoding,
364                        IPrimitiveType<String> theDisplayLanguage,
365                        Collection<IPrimitiveType<String>> thePropertyNames) {
366                boolean haveCoding = theCoding != null
367                                && isNotBlank(extractCodingSystem(theCoding))
368                                && isNotBlank(extractCodingCode(theCoding));
369                boolean haveCode = theCode != null && theCode.isEmpty() == false;
370                boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;
371                boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false;
372
373                if (!haveCoding && !(haveSystem && haveCode)) {
374                        throw new InvalidRequestException(
375                                        Msg.code(1126) + "No code, coding, or codeableConcept provided to validate");
376                }
377                if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
378                        throw new InvalidRequestException(
379                                        Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
380                }
381
382                String code;
383                String system;
384                if (haveCoding) {
385                        code = extractCodingCode(theCoding);
386                        system = extractCodingSystem(theCoding);
387                        String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding);
388                        if (isNotBlank(version)) {
389                                system = system + "|" + version;
390                        }
391                } else {
392                        code = theCode.getValue();
393                        system = theSystem.getValue();
394                }
395
396                String displayLanguage = null;
397                if (haveDisplayLanguage) {
398                        displayLanguage = theDisplayLanguage.getValue();
399                }
400
401                ourLog.info("Looking up {} / {}", system, code);
402
403                Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream()
404                                .map(IPrimitiveType::getValueAsString)
405                                .collect(Collectors.toSet());
406
407                if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) {
408
409                        ourLog.info("Code system {} is supported", system);
410                        IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode(
411                                        new ValidationSupportContext(theValidationSupport),
412                                        new LookupCodeRequest(system, code, displayLanguage, propertyNames));
413                        if (retVal != null) {
414                                return retVal;
415                        }
416                }
417
418                // We didn't find it..
419                return IValidationSupport.LookupCodeResult.notFound(system, code);
420        }
421
422        private static String extractCodingSystem(IBaseCoding theCoding) {
423                return theCoding.getSystem();
424        }
425
426        private static String extractCodingCode(IBaseCoding theCoding) {
427                return theCoding.getCode();
428        }
429
430        private static String extractCodingVersion(
431                        FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) {
432                if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
433                        return null;
434                }
435                return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version");
436        }
437
438        public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) {
439                String codeSystemUrl = theCodeSystemUrl;
440                if (isNotBlank(theVersion)) {
441                        codeSystemUrl = codeSystemUrl + "|" + theVersion;
442                }
443                return codeSystemUrl;
444        }
445}