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.dao.IFhirResourceDaoCodeSystem;
031import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
032import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
033import ca.uhn.fhir.jpa.model.entity.ResourceTable;
034import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
035import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
036import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
037import ca.uhn.fhir.jpa.util.LogicUtil;
038import ca.uhn.fhir.rest.api.server.RequestDetails;
039import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
040import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
041import ca.uhn.fhir.rest.param.TokenParam;
042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
043import ca.uhn.fhir.util.FhirTerser;
044import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
045import jakarta.annotation.Nonnull;
046import jakarta.annotation.PostConstruct;
047import org.apache.commons.collections4.CollectionUtils;
048import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
049import org.hl7.fhir.instance.model.api.IBaseCoding;
050import org.hl7.fhir.instance.model.api.IBaseDatatype;
051import org.hl7.fhir.instance.model.api.IBaseResource;
052import org.hl7.fhir.instance.model.api.IIdType;
053import org.hl7.fhir.instance.model.api.IPrimitiveType;
054import org.hl7.fhir.r4.model.CodeableConcept;
055import org.hl7.fhir.r4.model.Coding;
056import org.springframework.beans.factory.annotation.Autowired;
057
058import java.util.ArrayList;
059import java.util.Collection;
060import java.util.Date;
061import java.util.List;
062import java.util.stream.Collectors;
063
064import static ca.uhn.fhir.util.DatatypeUtil.toStringValue;
065import static org.apache.commons.lang3.StringUtils.isNotBlank;
066
067public class JpaResourceDaoCodeSystem<T extends IBaseResource> extends BaseHapiFhirResourceDao<T>
068                implements IFhirResourceDaoCodeSystem<T> {
069
070        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceDaoCodeSystem.class);
071
072        @Autowired
073        protected ITermCodeSystemStorageSvc myTerminologyCodeSystemStorageSvc;
074
075        @Autowired
076        protected IIdHelperService myIdHelperService;
077
078        @Autowired
079        protected ITermDeferredStorageSvc myTermDeferredStorageSvc;
080
081        @Autowired
082        private IValidationSupport myValidationSupport;
083
084        @Autowired
085        private FhirContext myFhirContext;
086
087        private FhirTerser myTerser;
088
089        @Autowired
090        private VersionCanonicalizer myVersionCanonicalizer;
091
092        @Override
093        @PostConstruct
094        public void start() {
095                super.start();
096                myTerser = myFhirContext.newTerser();
097        }
098
099        @Override
100        public List<IIdType> findCodeSystemIdsContainingSystemAndCode(
101                        String theCode, String theSystem, RequestDetails theRequest) {
102                List<IIdType> valueSetIds;
103                List<IResourcePersistentId> ids = searchForIds(
104                                new SearchParameterMap(org.hl7.fhir.r4.model.CodeSystem.SP_CODE, new TokenParam(theSystem, theCode)),
105                                theRequest);
106                valueSetIds = new ArrayList<>();
107                for (IResourcePersistentId next : ids) {
108                        IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "CodeSystem", next);
109                        valueSetIds.add(id);
110                }
111                return valueSetIds;
112        }
113
114        @Nonnull
115        @Override
116        public IValidationSupport.LookupCodeResult lookupCode(
117                        IPrimitiveType<String> theCode,
118                        IPrimitiveType<String> theSystem,
119                        IBaseCoding theCoding,
120                        RequestDetails theRequestDetails) {
121                return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails);
122        }
123
124        @Nonnull
125        @Override
126        public IValidationSupport.LookupCodeResult lookupCode(
127                        IPrimitiveType<String> theCode,
128                        IPrimitiveType<String> theSystem,
129                        IBaseCoding theCoding,
130                        IPrimitiveType<String> theDisplayLanguage,
131                        RequestDetails theRequestDetails) {
132                return lookupCode(
133                                theCode,
134                                theSystem,
135                                theCoding,
136                                theDisplayLanguage,
137                                CollectionUtils.emptyCollection(),
138                                theRequestDetails);
139        }
140
141        @Nonnull
142        @Override
143        public IValidationSupport.LookupCodeResult lookupCode(
144                        IPrimitiveType<String> theCode,
145                        IPrimitiveType<String> theSystem,
146                        IBaseCoding theCoding,
147                        IPrimitiveType<String> theDisplayLanguage,
148                        Collection<IPrimitiveType<String>> thePropertyNames,
149                        RequestDetails theRequestDetails) {
150                return doLookupCode(
151                                myFhirContext,
152                                myTerser,
153                                myValidationSupport,
154                                theCode,
155                                theSystem,
156                                theCoding,
157                                theDisplayLanguage,
158                                thePropertyNames);
159        }
160
161        @Override
162        public SubsumesResult subsumes(
163                        IPrimitiveType<String> theCodeA,
164                        IPrimitiveType<String> theCodeB,
165                        IPrimitiveType<String> theSystem,
166                        IBaseCoding theCodingA,
167                        IBaseCoding theCodingB,
168                        RequestDetails theRequestDetails) {
169                return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB);
170        }
171
172        @Override
173        protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) {
174                super.preDelete(theResourceToDelete, theEntityToDelete, theRequestDetails);
175
176                myTermDeferredStorageSvc.deleteCodeSystemForResource(theEntityToDelete);
177        }
178
179        @Override
180        public ResourceTable updateEntity(
181                        RequestDetails theRequest,
182                        IBaseResource theResource,
183                        IBasePersistedResource theEntity,
184                        Date theDeletedTimestampOrNull,
185                        boolean thePerformIndexing,
186                        boolean theUpdateVersion,
187                        TransactionDetails theTransactionDetails,
188                        boolean theForceUpdate,
189                        boolean theCreateNewHistoryEntry) {
190                ResourceTable retVal = super.updateEntity(
191                                theRequest,
192                                theResource,
193                                theEntity,
194                                theDeletedTimestampOrNull,
195                                thePerformIndexing,
196                                theUpdateVersion,
197                                theTransactionDetails,
198                                theForceUpdate,
199                                theCreateNewHistoryEntry);
200                if (!retVal.isUnchangedInCurrentOperation()) {
201
202                        org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource);
203                        addPidToResource(theEntity, cs);
204
205                        myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded(
206                                        cs, (ResourceTable) theEntity, theRequest);
207                }
208
209                return retVal;
210        }
211
212        @Nonnull
213        @Override
214        public CodeValidationResult validateCode(
215                        IIdType theCodeSystemId,
216                        IPrimitiveType<String> theCodeSystemUrl,
217                        IPrimitiveType<String> theVersion,
218                        IPrimitiveType<String> theCode,
219                        IPrimitiveType<String> theDisplay,
220                        IBaseCoding theCoding,
221                        IBaseDatatype theCodeableConcept,
222                        RequestDetails theRequestDetails) {
223
224                CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
225                boolean haveCodeableConcept =
226                                codeableConcept != null && codeableConcept.getCoding().size() > 0;
227
228                Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding);
229                boolean haveCoding = coding != null && !coding.isEmpty();
230
231                String code = toStringValue(theCode);
232                boolean haveCode = isNotBlank(code);
233
234                if (!haveCodeableConcept && !haveCoding && !haveCode) {
235                        throw new InvalidRequestException(
236                                        Msg.code(906) + "No code, coding, or codeableConcept provided to validate.");
237                }
238                if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) {
239                        throw new InvalidRequestException(
240                                        Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)");
241                }
242
243                String codeSystemUrl;
244                if (theCodeSystemId != null) {
245                        IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails);
246                        codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem);
247                } else if (isNotBlank(toStringValue(theCodeSystemUrl))) {
248                        codeSystemUrl = toStringValue(theCodeSystemUrl);
249                } else {
250                        throw new InvalidRequestException(Msg.code(908)
251                                        + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate.");
252                }
253
254                if (haveCodeableConcept) {
255                        CodeValidationResult anyValidation = null;
256                        for (int i = 0; i < codeableConcept.getCoding().size(); i++) {
257                                Coding nextCoding = codeableConcept.getCoding().get(i);
258                                if (nextCoding.hasSystem()) {
259                                        if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) {
260                                                throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem()
261                                                                + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
262                                        }
263                                        codeSystemUrl = nextCoding.getSystem();
264                                }
265                                code = nextCoding.getCode();
266                                String display = nextCoding.getDisplay();
267                                CodeValidationResult nextValidation =
268                                                codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
269                                anyValidation = nextValidation;
270                                if (nextValidation.isOk()) {
271                                        return nextValidation;
272                                }
273                        }
274                        return anyValidation;
275                } else if (haveCoding) {
276                        if (coding.hasSystem()) {
277                                if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) {
278                                        throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem()
279                                                        + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate.");
280                                }
281                                codeSystemUrl = coding.getSystem();
282                        }
283                        code = coding.getCode();
284                        String display = coding.getDisplay();
285                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
286                } else {
287                        String display = toStringValue(theDisplay);
288                        return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display);
289                }
290        }
291
292        private CodeValidationResult codeSystemValidateCode(
293                        String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) {
294                ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
295                ConceptValidationOptions options = new ConceptValidationOptions();
296                options.setValidateDisplay(isNotBlank(theDisplay));
297
298                String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion);
299
300                CodeValidationResult retVal =
301                                myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null);
302                if (retVal == null) {
303                        retVal = new CodeValidationResult();
304                        retVal.setMessage(
305                                        "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode);
306                }
307                return retVal;
308        }
309
310        public static IValidationSupport.LookupCodeResult doLookupCode(
311                        FhirContext theFhirContext,
312                        FhirTerser theFhirTerser,
313                        IValidationSupport theValidationSupport,
314                        IPrimitiveType<String> theCode,
315                        IPrimitiveType<String> theSystem,
316                        IBaseCoding theCoding,
317                        IPrimitiveType<String> theDisplayLanguage,
318                        Collection<IPrimitiveType<String>> thePropertyNames) {
319                boolean haveCoding = theCoding != null
320                                && isNotBlank(extractCodingSystem(theCoding))
321                                && isNotBlank(extractCodingCode(theCoding));
322                boolean haveCode = theCode != null && theCode.isEmpty() == false;
323                boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;
324                boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false;
325
326                if (!haveCoding && !(haveSystem && haveCode)) {
327                        throw new InvalidRequestException(
328                                        Msg.code(1126) + "No code, coding, or codeableConcept provided to validate");
329                }
330                if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
331                        throw new InvalidRequestException(
332                                        Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
333                }
334
335                String code;
336                String system;
337                if (haveCoding) {
338                        code = extractCodingCode(theCoding);
339                        system = extractCodingSystem(theCoding);
340                        String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding);
341                        if (isNotBlank(version)) {
342                                system = system + "|" + version;
343                        }
344                } else {
345                        code = theCode.getValue();
346                        system = theSystem.getValue();
347                }
348
349                String displayLanguage = null;
350                if (haveDisplayLanguage) {
351                        displayLanguage = theDisplayLanguage.getValue();
352                }
353
354                ourLog.info("Looking up {} / {}", system, code);
355
356                Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream()
357                                .map(IPrimitiveType::getValueAsString)
358                                .collect(Collectors.toSet());
359
360                if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) {
361
362                        ourLog.info("Code system {} is supported", system);
363                        IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode(
364                                        new ValidationSupportContext(theValidationSupport),
365                                        new LookupCodeRequest(system, code, displayLanguage, propertyNames));
366                        if (retVal != null) {
367                                return retVal;
368                        }
369                }
370
371                // We didn't find it..
372                return IValidationSupport.LookupCodeResult.notFound(system, code);
373        }
374
375        private static String extractCodingSystem(IBaseCoding theCoding) {
376                return theCoding.getSystem();
377        }
378
379        private static String extractCodingCode(IBaseCoding theCoding) {
380                return theCoding.getCode();
381        }
382
383        private static String extractCodingVersion(
384                        FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) {
385                if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
386                        return null;
387                }
388                return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version");
389        }
390
391        public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) {
392                String codeSystemUrl = theCodeSystemUrl;
393                if (isNotBlank(theVersion)) {
394                        codeSystemUrl = codeSystemUrl + "|" + theVersion;
395                }
396                return codeSystemUrl;
397        }
398}