001/*
002 * #%L
003 * HAPI FHIR JPA Server
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.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
236                ResourceTable retVal = super.updateEntity(
237                                theRequest,
238                                theResource,
239                                theEntity,
240                                theDeletedTimestampOrNull,
241                                thePerformIndexing,
242                                theUpdateVersion,
243                                theTransactionDetails,
244                                theForceUpdate,
245                                theCreateNewHistoryEntry);
246                if (thePerformIndexing) {
247                        if (!retVal.isUnchangedInCurrentOperation()) {
248
249                                org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource);
250                                addPidToResource(theEntity, cs);
251
252                                myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded(
253                                                cs, (ResourceTable) theEntity, theRequest);
254                        }
255
256                        /*
257                         * Flushing for each stored resource hurts performance, but in this case
258                         * it's justified because we don't expect people to be submitting
259                         * CodeSystem resources at super high rates, and we need to have the
260                         * various writes finished in case a second entry in the same transaction
261                         * tries to create a duplicate codesystem.
262                         */
263                        myEntityManager.flush();
264                }
265
266                return retVal;
267        }
268
269        @Nonnull
270        @Override
271        public CodeValidationResult validateCode(
272                        IIdType theCodeSystemId,
273                        IPrimitiveType<String> theCodeSystemUrl,
274                        IPrimitiveType<String> theVersion,
275                        IPrimitiveType<String> theCode,
276                        IPrimitiveType<String> theDisplay,
277                        IBaseCoding theCoding,
278                        IBaseDatatype theCodeableConcept,
279                        RequestDetails theRequestDetails) {
280
281                CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
282                boolean haveCodeableConcept =
283                                codeableConcept != null && codeableConcept.getCoding().size() > 0;
284
285                Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding);
286                boolean haveCoding = coding != null && !coding.isEmpty();
287
288                String code = toStringValue(theCode);
289                boolean haveCode = isNotBlank(code);
290
291                if (!haveCodeableConcept && !haveCoding && !haveCode) {
292                        throw new InvalidRequestException(
293                                        Msg.code(906) + "No code, coding, or codeableConcept provided to validate.");
294                }
295                if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) {
296                        throw new InvalidRequestException(
297                                        Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)");
298                }
299
300                String codeSystemUrl;
301                if (theCodeSystemId != null) {
302                        IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails);
303                        codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem);
304                } else if (isNotBlank(toStringValue(theCodeSystemUrl))) {
305                        codeSystemUrl = toStringValue(theCodeSystemUrl);
306                } else {
307                        throw new InvalidRequestException(Msg.code(908)
308                                        + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate.");
309                }
310
311                if (haveCodeableConcept) {
312                        CodeValidationResult anyValidation = null;
313                        for (int i = 0; i < codeableConcept.getCoding().size(); i++) {
314                                Coding nextCoding = codeableConcept.getCoding().get(i);
315                                if (nextCoding.hasSystem()) {
316                                        if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) {
317                                                throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem()
318                                                                + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
319                                        }
320                                        codeSystemUrl = nextCoding.getSystem();
321                                }
322                                code = nextCoding.getCode();
323                                String display = nextCoding.getDisplay();
324                                CodeValidationResult nextValidation =
325                                                codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
326                                anyValidation = nextValidation;
327                                if (nextValidation.isOk()) {
328                                        return nextValidation;
329                                }
330                        }
331                        return anyValidation;
332                } else if (haveCoding) {
333                        if (coding.hasSystem()) {
334                                if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) {
335                                        throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem()
336                                                        + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
337                                }
338                                codeSystemUrl = coding.getSystem();
339                        }
340                        code = coding.getCode();
341                        String display = coding.getDisplay();
342                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
343                } else {
344                        String display = toStringValue(theDisplay);
345                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
346                }
347        }
348
349        private CodeValidationResult codeSystemValidateCode(
350                        String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) {
351                ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
352                ConceptValidationOptions options = new ConceptValidationOptions();
353                options.setValidateDisplay(isNotBlank(theDisplay));
354
355                String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion);
356
357                CodeValidationResult retVal =
358                                myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null);
359                if (retVal == null) {
360                        retVal = new CodeValidationResult();
361                        retVal.setMessage(
362                                        "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode);
363                }
364                return retVal;
365        }
366
367        public static IValidationSupport.LookupCodeResult doLookupCode(
368                        FhirContext theFhirContext,
369                        FhirTerser theFhirTerser,
370                        IValidationSupport theValidationSupport,
371                        IPrimitiveType<String> theCode,
372                        IPrimitiveType<String> theSystem,
373                        IBaseCoding theCoding,
374                        IPrimitiveType<String> theDisplayLanguage,
375                        Collection<IPrimitiveType<String>> thePropertyNames) {
376                boolean haveCoding = theCoding != null
377                                && isNotBlank(extractCodingSystem(theCoding))
378                                && isNotBlank(extractCodingCode(theCoding));
379                boolean haveCode = theCode != null && theCode.isEmpty() == false;
380                boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;
381                boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false;
382
383                if (!haveCoding && !(haveSystem && haveCode)) {
384                        throw new InvalidRequestException(
385                                        Msg.code(1126) + "No code, coding, or codeableConcept provided to validate");
386                }
387                if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
388                        throw new InvalidRequestException(
389                                        Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
390                }
391
392                String code;
393                String system;
394                if (haveCoding) {
395                        code = extractCodingCode(theCoding);
396                        system = extractCodingSystem(theCoding);
397                        String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding);
398                        if (isNotBlank(version)) {
399                                system = system + "|" + version;
400                        }
401                } else {
402                        code = theCode.getValue();
403                        system = theSystem.getValue();
404                }
405
406                String displayLanguage = null;
407                if (haveDisplayLanguage) {
408                        displayLanguage = theDisplayLanguage.getValue();
409                }
410
411                ourLog.info("Looking up {} / {}", system, code);
412
413                Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream()
414                                .map(IPrimitiveType::getValueAsString)
415                                .collect(Collectors.toSet());
416
417                if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) {
418
419                        ourLog.info("Code system {} is supported", system);
420                        IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode(
421                                        new ValidationSupportContext(theValidationSupport),
422                                        new LookupCodeRequest(system, code, displayLanguage, propertyNames));
423                        if (retVal != null) {
424                                return retVal;
425                        }
426                }
427
428                // We didn't find it..
429                return IValidationSupport.LookupCodeResult.notFound(system, code);
430        }
431
432        private static String extractCodingSystem(IBaseCoding theCoding) {
433                return theCoding.getSystem();
434        }
435
436        private static String extractCodingCode(IBaseCoding theCoding) {
437                return theCoding.getCode();
438        }
439
440        private static String extractCodingVersion(
441                        FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) {
442                if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
443                        return null;
444                }
445                return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version");
446        }
447
448        public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) {
449                String codeSystemUrl = theCodeSystemUrl;
450                if (isNotBlank(theVersion)) {
451                        codeSystemUrl = codeSystemUrl + "|" + theVersion;
452                }
453                return codeSystemUrl;
454        }
455}