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