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