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.context.support.ValueSetExpansionOptions; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 029import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 030import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; 031import ca.uhn.fhir.jpa.config.JpaConfig; 032import ca.uhn.fhir.jpa.model.util.JpaConstants; 033import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 034import ca.uhn.fhir.rest.annotation.IdParam; 035import ca.uhn.fhir.rest.annotation.Operation; 036import ca.uhn.fhir.rest.annotation.OperationParam; 037import ca.uhn.fhir.rest.api.server.RequestDetails; 038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 039import ca.uhn.fhir.rest.server.provider.ProviderConstants; 040import ca.uhn.fhir.util.ParametersUtil; 041import com.google.common.annotations.VisibleForTesting; 042import jakarta.servlet.http.HttpServletRequest; 043import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; 044import org.hl7.fhir.instance.model.api.IBaseCoding; 045import org.hl7.fhir.instance.model.api.IBaseParameters; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.hl7.fhir.instance.model.api.ICompositeType; 048import org.hl7.fhir.instance.model.api.IIdType; 049import org.hl7.fhir.instance.model.api.IPrimitiveType; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052import org.springframework.beans.factory.annotation.Autowired; 053import org.springframework.beans.factory.annotation.Qualifier; 054 055import java.util.Optional; 056import java.util.function.Supplier; 057 058import static org.apache.commons.lang3.StringUtils.isNotBlank; 059 060public class ValueSetOperationProvider extends BaseJpaProvider { 061 062 private static final Logger ourLog = LoggerFactory.getLogger(ValueSetOperationProvider.class); 063 064 @Autowired 065 protected IValidationSupport myValidationSupport; 066 067 @Autowired 068 private DaoRegistry myDaoRegistry; 069 070 @Autowired 071 private ITermReadSvc myTermReadSvc; 072 073 @Autowired 074 @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) 075 private ValidationSupportChain myValidationSupportChain; 076 077 @VisibleForTesting 078 public void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) { 079 myDaoRegistry = theDaoRegistry; 080 } 081 082 public void setValidationSupport(IValidationSupport theValidationSupport) { 083 myValidationSupport = theValidationSupport; 084 } 085 086 @Operation(name = JpaConstants.OPERATION_EXPAND, idempotent = true, typeName = "ValueSet") 087 public IBaseResource expand( 088 HttpServletRequest theServletRequest, 089 @IdParam(optional = true) IIdType theId, 090 @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet, 091 @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theUrl, 092 @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") 093 IPrimitiveType<String> theValueSetVersion, 094 @OperationParam(name = "filter", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theFilter, 095 @OperationParam(name = "context", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContext, 096 @OperationParam(name = "contextDirection", min = 0, max = 1, typeName = "string") 097 IPrimitiveType<String> theContextDirection, 098 @OperationParam(name = "offset", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theOffset, 099 @OperationParam(name = "count", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theCount, 100 @OperationParam( 101 name = JpaConstants.OPERATION_EXPAND_PARAM_DISPLAY_LANGUAGE, 102 min = 0, 103 max = 1, 104 typeName = "code") 105 IPrimitiveType<String> theDisplayLanguage, 106 @OperationParam( 107 name = JpaConstants.OPERATION_EXPAND_PARAM_INCLUDE_HIERARCHY, 108 min = 0, 109 max = 1, 110 typeName = "boolean") 111 IPrimitiveType<Boolean> theIncludeHierarchy, 112 RequestDetails theRequestDetails) { 113 114 startRequest(theServletRequest); 115 try { 116 117 return getDao().expand( 118 theId, 119 theValueSet, 120 theUrl, 121 theValueSetVersion, 122 theFilter, 123 theContext, 124 theContextDirection, 125 theOffset, 126 theCount, 127 theDisplayLanguage, 128 theIncludeHierarchy, 129 theRequestDetails); 130 131 } finally { 132 endRequest(theServletRequest); 133 } 134 } 135 136 @SuppressWarnings("unchecked") 137 protected IFhirResourceDaoValueSet<IBaseResource> getDao() { 138 return (IFhirResourceDaoValueSet<IBaseResource>) myDaoRegistry.getResourceDao("ValueSet"); 139 } 140 141 @SuppressWarnings("unchecked") 142 @Operation( 143 name = JpaConstants.OPERATION_VALIDATE_CODE, 144 idempotent = true, 145 typeName = "ValueSet", 146 returnParameters = { 147 @OperationParam(name = CodeValidationResult.RESULT, typeName = "boolean", min = 1), 148 @OperationParam(name = CodeValidationResult.MESSAGE, typeName = "string"), 149 @OperationParam(name = CodeValidationResult.DISPLAY, typeName = "string"), 150 @OperationParam(name = CodeValidationResult.SOURCE_DETAILS, typeName = "string") 151 }) 152 public IBaseParameters validateCode( 153 HttpServletRequest theServletRequest, 154 @IdParam(optional = true) IIdType theId, 155 @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theValueSetUrl, 156 @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") 157 IPrimitiveType<String> theValueSetVersion, 158 @OperationParam(name = "code", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCode, 159 @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem, 160 @OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string") 161 IPrimitiveType<String> theSystemVersion, 162 @OperationParam(name = CodeValidationResult.DISPLAY, min = 0, max = 1, typeName = "string") 163 IPrimitiveType<String> theDisplay, 164 @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theCoding, 165 @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept") 166 ICompositeType theCodeableConcept, 167 RequestDetails theRequestDetails) { 168 169 CodeValidationResult result; 170 startRequest(theServletRequest); 171 try { 172 // If a Remote Terminology Server has been configured, use it 173 if (myValidationSupportChain != null && myValidationSupportChain.isRemoteTerminologyServiceConfigured()) { 174 String theSystemString = 175 (theSystem != null && theSystem.hasValue()) ? theSystem.getValueAsString() : null; 176 String theCodeString = (theCode != null && theCode.hasValue()) ? theCode.getValueAsString() : null; 177 String theDisplayString = 178 (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null; 179 String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) 180 ? theValueSetUrl.getValueAsString() 181 : null; 182 if (theCoding != null) { 183 if (isNotBlank(theCoding.getSystem())) { 184 if (theSystemString != null && !theSystemString.equalsIgnoreCase(theCoding.getSystem())) { 185 throw new InvalidRequestException(Msg.code(2352) + "Coding.system '" + theCoding.getSystem() 186 + "' does not equal param system '" + theSystemString 187 + "'. Unable to validate-code."); 188 } 189 theSystemString = theCoding.getSystem(); 190 theCodeString = theCoding.getCode(); 191 theDisplayString = theCoding.getDisplay(); 192 } 193 } 194 195 result = validateCodeWithTerminologyService( 196 theSystemString, theCodeString, theDisplayString, theValueSetUrlString) 197 .orElseGet(supplyUnableToValidateResult(theSystemString, theCodeString, theValueSetUrlString)); 198 } else { 199 // Otherwise, use the local DAO layer to validate the code 200 IFhirResourceDaoValueSet<IBaseResource> dao = getDao(); 201 IPrimitiveType<String> valueSetIdentifier; 202 if (theValueSetUrl != null && theValueSetVersion != null) { 203 valueSetIdentifier = (IPrimitiveType<String>) 204 getContext().getElementDefinition("uri").newInstance(); 205 valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion); 206 } else { 207 valueSetIdentifier = theValueSetUrl; 208 } 209 IPrimitiveType<String> codeSystemIdentifier; 210 if (theSystem != null && theSystemVersion != null) { 211 codeSystemIdentifier = (IPrimitiveType<String>) 212 getContext().getElementDefinition("uri").newInstance(); 213 codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion); 214 } else { 215 codeSystemIdentifier = theSystem; 216 } 217 result = dao.validateCode( 218 valueSetIdentifier, 219 theId, 220 theCode, 221 codeSystemIdentifier, 222 theDisplay, 223 theCoding, 224 theCodeableConcept, 225 theRequestDetails); 226 } 227 return result.toParameters(getContext()); 228 } finally { 229 endRequest(theServletRequest); 230 } 231 } 232 233 private Optional<CodeValidationResult> validateCodeWithTerminologyService( 234 String theSystem, String theCode, String theDisplay, String theValueSetUrl) { 235 return Optional.ofNullable(myValidationSupportChain.validateCode( 236 new ValidationSupportContext(myValidationSupportChain), 237 new ConceptValidationOptions(), 238 theSystem, 239 theCode, 240 theDisplay, 241 theValueSetUrl)); 242 } 243 244 private Supplier<CodeValidationResult> supplyUnableToValidateResult( 245 String theSystem, String theCode, String theValueSetUrl) { 246 return () -> new CodeValidationResult() 247 .setMessage("Validator is unable to provide validation for " + theCode + "#" + theSystem 248 + " - Unknown or unusable ValueSet[" + theValueSetUrl + "]"); 249 } 250 251 @Operation( 252 name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION, 253 idempotent = false, 254 typeName = "ValueSet", 255 returnParameters = { 256 @OperationParam(name = CodeValidationResult.MESSAGE, typeName = "string", min = 1, max = 1) 257 }) 258 public IBaseParameters invalidateValueSetExpansion( 259 @IdParam IIdType theValueSetId, RequestDetails theRequestDetails, HttpServletRequest theServletRequest) { 260 startRequest(theServletRequest); 261 try { 262 263 String outcome = myTermReadSvc.invalidatePreCalculatedExpansion(theValueSetId, theRequestDetails); 264 265 IBaseParameters retVal = ParametersUtil.newInstance(getContext()); 266 ParametersUtil.addParameterToParametersString(getContext(), retVal, CodeValidationResult.MESSAGE, outcome); 267 return retVal; 268 269 } finally { 270 endRequest(theServletRequest); 271 } 272 } 273 274 public static ValueSetExpansionOptions createValueSetExpansionOptions( 275 JpaStorageSettings theStorageSettings, 276 IPrimitiveType<Integer> theOffset, 277 IPrimitiveType<Integer> theCount, 278 IPrimitiveType<Boolean> theIncludeHierarchy, 279 IPrimitiveType<String> theFilter, 280 IPrimitiveType<String> theDisplayLanguage) { 281 int offset = theStorageSettings.getPreExpandValueSetsDefaultOffset(); 282 if (theOffset != null && theOffset.hasValue()) { 283 if (theOffset.getValue() >= 0) { 284 offset = theOffset.getValue(); 285 } else { 286 throw new InvalidRequestException( 287 Msg.code(1135) + "offset parameter for $expand operation must be >= 0 when specified. offset: " 288 + theOffset.getValue()); 289 } 290 } 291 292 int count = theStorageSettings.getPreExpandValueSetsDefaultCount(); 293 if (theCount != null && theCount.hasValue()) { 294 if (theCount.getValue() >= 0) { 295 count = theCount.getValue(); 296 } else { 297 throw new InvalidRequestException( 298 Msg.code(1136) + "count parameter for $expand operation must be >= 0 when specified. count: " 299 + theCount.getValue()); 300 } 301 } 302 int countMax = theStorageSettings.getPreExpandValueSetsMaxCount(); 303 if (count > countMax) { 304 ourLog.warn( 305 "count parameter for $expand operation of {} exceeds maximum value of {}; using maximum value.", 306 count, 307 countMax); 308 count = countMax; 309 } 310 311 ValueSetExpansionOptions options = ValueSetExpansionOptions.forOffsetAndCount(offset, count); 312 313 if (theIncludeHierarchy != null && Boolean.TRUE.equals(theIncludeHierarchy.getValue())) { 314 options.setIncludeHierarchy(true); 315 } 316 317 if (theFilter != null) { 318 options.setFilter(theFilter.getValue()); 319 } 320 321 if (theDisplayLanguage != null) { 322 options.setTheDisplayLanguage(theDisplayLanguage.getValue()); 323 } 324 325 return options; 326 } 327}