
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}