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.provider;
021
022import ca.uhn.fhir.context.support.ConceptValidationOptions;
023import ca.uhn.fhir.context.support.IValidationSupport;
024import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
025import ca.uhn.fhir.context.support.ValidationSupportContext;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
028import ca.uhn.fhir.jpa.model.util.JpaConstants;
029import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
030import ca.uhn.fhir.rest.annotation.IdParam;
031import ca.uhn.fhir.rest.annotation.Operation;
032import ca.uhn.fhir.rest.annotation.OperationParam;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
035import jakarta.servlet.http.HttpServletRequest;
036import org.hl7.fhir.instance.model.api.IBaseCoding;
037import org.hl7.fhir.instance.model.api.IBaseDatatype;
038import org.hl7.fhir.instance.model.api.IBaseParameters;
039import org.hl7.fhir.instance.model.api.IBaseResource;
040import org.hl7.fhir.instance.model.api.IIdType;
041import org.hl7.fhir.instance.model.api.IPrimitiveType;
042import org.springframework.beans.factory.annotation.Autowired;
043
044import java.util.List;
045import java.util.Optional;
046import java.util.function.Supplier;
047
048import static org.apache.commons.lang3.StringUtils.isNotBlank;
049
050public abstract class BaseJpaResourceProviderCodeSystem<T extends IBaseResource> extends BaseJpaResourceProvider<T> {
051
052        @Autowired
053        private JpaValidationSupportChain myValidationSupportChain;
054
055        /**
056         * $lookup operation
057         */
058        @SuppressWarnings("unchecked")
059        @Operation(
060                        name = JpaConstants.OPERATION_LOOKUP,
061                        idempotent = true,
062                        returnParameters = {
063                                @OperationParam(name = "name", typeName = "string", min = 1),
064                                @OperationParam(name = "version", typeName = "string", min = 0),
065                                @OperationParam(name = "display", typeName = "string", min = 1),
066                                @OperationParam(name = "abstract", typeName = "boolean", min = 1),
067                                @OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "code")
068                        })
069        public IBaseParameters lookup(
070                        HttpServletRequest theServletRequest,
071                        @OperationParam(name = "code", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCode,
072                        @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
073                        @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theCoding,
074                        @OperationParam(name = "version", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theVersion,
075                        @OperationParam(name = "displayLanguage", min = 0, max = 1, typeName = "code")
076                                        IPrimitiveType<String> theDisplayLanguage,
077                        @OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "code")
078                                        List<IPrimitiveType<String>> thePropertyNames,
079                        RequestDetails theRequestDetails) {
080
081                startRequest(theServletRequest);
082                try {
083                        IFhirResourceDaoCodeSystem dao = (IFhirResourceDaoCodeSystem) getDao();
084                        IValidationSupport.LookupCodeResult result;
085                        applyVersionToSystem(theSystem, theVersion);
086                        result = dao.lookupCode(
087                                        theCode, theSystem, theCoding, theDisplayLanguage, thePropertyNames, theRequestDetails);
088                        result.throwNotFoundIfAppropriate();
089                        return result.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
090                } finally {
091                        endRequest(theServletRequest);
092                }
093        }
094
095        /**
096         * $subsumes operation
097         */
098        @Operation(
099                        name = JpaConstants.OPERATION_SUBSUMES,
100                        idempotent = true,
101                        returnParameters = {
102                                @OperationParam(name = "outcome", typeName = "code", min = 1),
103                        })
104        public IBaseParameters subsumes(
105                        HttpServletRequest theServletRequest,
106                        @OperationParam(name = "codeA", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCodeA,
107                        @OperationParam(name = "codeB", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCodeB,
108                        @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
109                        @OperationParam(name = "codingA", min = 0, max = 1, typeName = "Coding") IBaseCoding theCodingA,
110                        @OperationParam(name = "codingB", min = 0, max = 1, typeName = "Coding") IBaseCoding theCodingB,
111                        @OperationParam(name = "version", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theVersion,
112                        RequestDetails theRequestDetails) {
113
114                startRequest(theServletRequest);
115                try {
116                        IFhirResourceDaoCodeSystem dao = (IFhirResourceDaoCodeSystem) getDao();
117                        IFhirResourceDaoCodeSystem.SubsumesResult result;
118                        applyVersionToSystem(theSystem, theVersion);
119                        result = dao.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB, theRequestDetails);
120                        return result.toParameters(theRequestDetails.getFhirContext());
121                } finally {
122                        endRequest(theServletRequest);
123                }
124        }
125
126        static void applyVersionToSystem(IPrimitiveType<String> theSystem, IPrimitiveType<String> theVersion) {
127                if (theVersion != null && isNotBlank(theVersion.getValueAsString()) && theSystem != null) {
128                        theSystem.setValue(theSystem.getValueAsString() + "|" + theVersion.getValueAsString());
129                }
130        }
131
132        /**
133         * $validate-code operation
134         */
135        @SuppressWarnings("unchecked")
136        @Operation(
137                        name = JpaConstants.OPERATION_VALIDATE_CODE,
138                        idempotent = true,
139                        returnParameters = {
140                                @OperationParam(name = "result", typeName = "boolean", min = 1),
141                                @OperationParam(name = "message", typeName = "string"),
142                                @OperationParam(name = "display", typeName = "string")
143                        })
144        public IBaseParameters validateCode(
145                        HttpServletRequest theServletRequest,
146                        @IdParam(optional = true) IIdType theId,
147                        @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theCodeSystemUrl,
148                        @OperationParam(name = "version", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theVersion,
149                        @OperationParam(name = "code", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCode,
150                        @OperationParam(name = "display", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theDisplay,
151                        @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theCoding,
152                        @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept")
153                                        IBaseDatatype theCodeableConcept,
154                        RequestDetails theRequestDetails) {
155
156                CodeValidationResult result = null;
157                startRequest(theServletRequest);
158                try {
159                        // TODO: JA why not just always just the chain here? and we can then get rid of the corresponding DAO method
160                        // entirely
161                        // If a Remote Terminology Server has been configured, use it
162                        if (myValidationSupportChain.isRemoteTerminologyServiceConfigured()) {
163                                String codeSystemUrl = (theCodeSystemUrl != null && theCodeSystemUrl.hasValue())
164                                                ? theCodeSystemUrl.getValueAsString()
165                                                : null;
166
167                                if (theCoding != null) {
168                                        if (isNotBlank(theCoding.getSystem())) {
169                                                if (codeSystemUrl != null && !codeSystemUrl.equalsIgnoreCase(theCoding.getSystem())) {
170                                                        throw new InvalidRequestException(Msg.code(1160) + "Coding.system '" + theCoding.getSystem()
171                                                                        + "' does not equal param url '" + theCodeSystemUrl
172                                                                        + "'. Unable to validate-code.");
173                                                }
174                                                codeSystemUrl = theCoding.getSystem();
175                                                String code = theCoding.getCode();
176                                                String display = theCoding.getDisplay();
177
178                                                result = validateCodeWithTerminologyService(codeSystemUrl, code, display)
179                                                                .orElseGet(supplyUnableToValidateResult(codeSystemUrl, code));
180                                        }
181                                }
182                        } else {
183                                // Otherwise, use the local DAO layer to validate the code
184                                IFhirResourceDaoCodeSystem dao = (IFhirResourceDaoCodeSystem) getDao();
185                                result = dao.validateCode(
186                                                theId,
187                                                theCodeSystemUrl,
188                                                theVersion,
189                                                theCode,
190                                                theDisplay,
191                                                theCoding,
192                                                theCodeableConcept,
193                                                theRequestDetails);
194                        }
195                        return result.toParameters(getContext());
196                } finally {
197                        endRequest(theServletRequest);
198                }
199        }
200
201        private Optional<CodeValidationResult> validateCodeWithTerminologyService(
202                        String theCodeSystemUrl, String theCode, String theDisplay) {
203                return Optional.ofNullable(myValidationSupportChain.validateCode(
204                                new ValidationSupportContext(myValidationSupportChain),
205                                new ConceptValidationOptions(),
206                                theCodeSystemUrl,
207                                theCode,
208                                theDisplay,
209                                null));
210        }
211
212        private Supplier<CodeValidationResult> supplyUnableToValidateResult(String theCodeSystemUrl, String theCode) {
213                return () -> new CodeValidationResult()
214                                .setMessage(
215                                                "Terminology service was unable to provide validation for " + theCodeSystemUrl + "#" + theCode);
216        }
217}