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