
001package ca.uhn.fhir.jpa.provider; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.support.ConceptValidationOptions; 024import ca.uhn.fhir.context.support.IValidationSupport; 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.DaoConfig; 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.dao.IFulltextSearchSvc; 033import ca.uhn.fhir.jpa.model.util.JpaConstants; 034import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions; 035import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 036import ca.uhn.fhir.rest.annotation.IdParam; 037import ca.uhn.fhir.rest.annotation.Operation; 038import ca.uhn.fhir.rest.annotation.OperationParam; 039import ca.uhn.fhir.rest.api.server.RequestDetails; 040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 042import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 043import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 044import ca.uhn.fhir.rest.server.provider.ProviderConstants; 045import ca.uhn.fhir.util.ParametersUtil; 046import ca.uhn.fhir.util.UrlUtil; 047import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; 048import org.hl7.fhir.instance.model.api.IBaseParameters; 049import org.hl7.fhir.instance.model.api.IBaseResource; 050import org.hl7.fhir.instance.model.api.ICompositeType; 051import org.hl7.fhir.instance.model.api.IIdType; 052import org.hl7.fhir.instance.model.api.IPrimitiveType; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055import org.springframework.beans.factory.annotation.Autowired; 056import org.springframework.beans.factory.annotation.Qualifier; 057 058import javax.servlet.http.HttpServletRequest; 059 060import static org.apache.commons.lang3.StringUtils.isNotBlank; 061 062public class ValueSetOperationProvider extends BaseJpaProvider { 063 064 private static final Logger ourLog = LoggerFactory.getLogger(ValueSetOperationProvider.class); 065 @Autowired 066 private DaoConfig myDaoConfig; 067 @Autowired 068 private DaoRegistry myDaoRegistry; 069 @Autowired 070 private ITermReadSvc myTermReadSvc; 071 @Autowired 072 @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) 073 private ValidationSupportChain myValidationSupportChain; 074 @Autowired 075 private IValidationSupport myValidationSupport; 076 @Autowired(required = false) 077 private IFulltextSearchSvc myFulltextSearch; 078 079 public void setValidationSupport(IValidationSupport theValidationSupport) { 080 myValidationSupport = theValidationSupport; 081 } 082 083 public void setDaoConfig(DaoConfig theDaoConfig) { 084 myDaoConfig = theDaoConfig; 085 } 086 087 public void setDaoRegistry(DaoRegistry theDaoRegistry) { 088 myDaoRegistry = theDaoRegistry; 089 } 090 091 public void setTermReadSvc(ITermReadSvc theTermReadSvc) { 092 myTermReadSvc = theTermReadSvc; 093 } 094 095 public void setValidationSupportChain(ValidationSupportChain theValidationSupportChain) { 096 myValidationSupportChain = theValidationSupportChain; 097 } 098 099 @Operation(name = JpaConstants.OPERATION_EXPAND, idempotent = true, typeName = "ValueSet") 100 public IBaseResource expand( 101 HttpServletRequest theServletRequest, 102 @IdParam(optional = true) IIdType theId, 103 @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet, 104 @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theUrl, 105 @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theValueSetVersion, 106 @OperationParam(name = "filter", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theFilter, 107 @OperationParam(name = "context", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContext, 108 @OperationParam(name = "contextDirection", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContextDirection, 109 @OperationParam(name = "offset", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theOffset, 110 @OperationParam(name = "count", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theCount, 111 @OperationParam(name = JpaConstants.OPERATION_EXPAND_PARAM_INCLUDE_HIERARCHY, min = 0, max = 1, typeName = "boolean") IPrimitiveType<Boolean> theIncludeHierarchy, 112 RequestDetails theRequestDetails) { 113 114 boolean haveId = theId != null && theId.hasIdPart(); 115 boolean haveIdentifier = theUrl != null && isNotBlank(theUrl.getValue()); 116 boolean haveValueSet = theValueSet != null && !theValueSet.isEmpty(); 117 boolean haveValueSetVersion = theValueSetVersion != null && !theValueSetVersion.isEmpty(); 118 boolean haveContextDirection = theContextDirection != null && !theContextDirection.isEmpty(); 119 boolean haveContext = theContext != null && !theContext.isEmpty(); 120 121 boolean isAutocompleteExtension = haveContext && haveContextDirection && "existing".equals(theContextDirection.getValue()); 122 123 if (isAutocompleteExtension) { 124 // this is a funky extension for NIH. Do our own thing and return. 125 ValueSetAutocompleteOptions options = ValueSetAutocompleteOptions.validateAndParseOptions(myDaoConfig, theContext, theFilter, theCount, theId, theUrl, theValueSet); 126 startRequest(theServletRequest); 127 try { 128 if (myFulltextSearch == null || myFulltextSearch.isDisabled()) { 129 throw new InvalidRequestException(Msg.code(2083) + " Autocomplete is not supported on this server, as the fulltext search service is not configured."); 130 } else { 131 return myFulltextSearch.tokenAutocompleteValueSetSearch(options); 132 } 133 } finally { 134 endRequest(theServletRequest); 135 } 136 } 137 138 if (!haveId && !haveIdentifier && !haveValueSet) { 139 throw new InvalidRequestException(Msg.code(1133) + "$expand operation at the type level (no ID specified) requires a url or a valueSet as a part of the request."); 140 } 141 142 if (moreThanOneTrue(haveId, haveIdentifier, haveValueSet)) { 143 throw new InvalidRequestException(Msg.code(1134) + "$expand must EITHER be invoked at the instance level, or have a url specified, or have a ValueSet specified. Can not combine these options."); 144 } 145 146 ValueSetExpansionOptions options = createValueSetExpansionOptions(myDaoConfig, theOffset, theCount, theIncludeHierarchy, theFilter); 147 148 startRequest(theServletRequest); 149 try { 150 151 IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao(); 152 153 IValidationSupport.ValueSetExpansionOutcome outcome; 154 if (haveId) { 155 IBaseResource valueSet = dao.read(theId, theRequestDetails); 156 outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, valueSet); 157 } else if (haveIdentifier) { 158 String url; 159 if (haveValueSetVersion) { 160 url = theUrl.getValue() + "|" + theValueSetVersion.getValue(); 161 } else { 162 url = theUrl.getValue(); 163 } 164 outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, url); 165 } else { 166 outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, theValueSet); 167 } 168 169 if (outcome == null) { 170 throw new InternalErrorException(Msg.code(2028) + "No validation support module was able to expand the given valueset"); 171 } 172 173 if (outcome.getError() != null) { 174 throw new PreconditionFailedException(Msg.code(2029) + outcome.getError()); 175 } 176 177 return outcome.getValueSet(); 178 179 } finally { 180 endRequest(theServletRequest); 181 } 182 } 183 184 @SuppressWarnings("unchecked") 185 private IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> getDao() { 186 return (IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType>) myDaoRegistry.getResourceDao("ValueSet"); 187 } 188 189 @SuppressWarnings("unchecked") 190 @Operation(name = JpaConstants.OPERATION_VALIDATE_CODE, idempotent = true, typeName = "ValueSet", returnParameters = { 191 @OperationParam(name = "result", typeName = "boolean", min = 1), 192 @OperationParam(name = "message", typeName = "string"), 193 @OperationParam(name = "display", typeName = "string") 194 }) 195 public IBaseParameters validateCode( 196 HttpServletRequest theServletRequest, 197 @IdParam(optional = true) IIdType theId, 198 @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theValueSetUrl, 199 @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theValueSetVersion, 200 @OperationParam(name = "code", min = 0, max = 1) IPrimitiveType<String> theCode, 201 @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem, 202 @OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theSystemVersion, 203 @OperationParam(name = "display", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theDisplay, 204 @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") ICompositeType theCoding, 205 @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept") ICompositeType theCodeableConcept, 206 RequestDetails theRequestDetails 207 ) { 208 209 IValidationSupport.CodeValidationResult result; 210 startRequest(theServletRequest); 211 try { 212 // If a Remote Terminology Server has been configured, use it 213 if (myValidationSupportChain != null && myValidationSupportChain.isRemoteTerminologyServiceConfigured()) { 214 String theSystemString = (theSystem != null && theSystem.hasValue()) ? theSystem.getValueAsString() : null; 215 String theCodeString = (theCode != null && theCode.hasValue()) ? theCode.getValueAsString() : null; 216 String theDisplayString = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null; 217 String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) ? 218 theValueSetUrl.getValueAsString() : null; 219 result = myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain), 220 new ConceptValidationOptions(), theSystemString, theCodeString, theDisplayString, theValueSetUrlString); 221 } else { 222 // Otherwise, use the local DAO layer to validate the code 223 IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao(); 224 IPrimitiveType<String> valueSetIdentifier; 225 if (theValueSetUrl != null && theValueSetVersion != null) { 226 valueSetIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance(); 227 valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion); 228 } else { 229 valueSetIdentifier = theValueSetUrl; 230 } 231 IPrimitiveType<String> codeSystemIdentifier; 232 if (theSystem != null && theSystemVersion != null) { 233 codeSystemIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance(); 234 codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion); 235 } else { 236 codeSystemIdentifier = theSystem; 237 } 238 result = dao.validateCode(valueSetIdentifier, theId, theCode, codeSystemIdentifier, theDisplay, theCoding, theCodeableConcept, theRequestDetails); 239 } 240 return BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result); 241 } finally { 242 endRequest(theServletRequest); 243 } 244 } 245 246 @Operation(name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION, idempotent = false, typeName = "ValueSet", returnParameters = { 247 @OperationParam(name = "message", typeName = "string", min = 1, max = 1) 248 }) 249 public IBaseParameters invalidateValueSetExpansion( 250 @IdParam IIdType theValueSetId, 251 RequestDetails theRequestDetails, 252 HttpServletRequest theServletRequest) { 253 startRequest(theServletRequest); 254 try { 255 256 String outcome = myTermReadSvc.invalidatePreCalculatedExpansion(theValueSetId, theRequestDetails); 257 258 IBaseParameters retVal = ParametersUtil.newInstance(getContext()); 259 ParametersUtil.addParameterToParametersString(getContext(), retVal, "message", outcome); 260 return retVal; 261 262 } finally { 263 endRequest(theServletRequest); 264 } 265 } 266 267 268 public static ValueSetExpansionOptions createValueSetExpansionOptions(DaoConfig theDaoConfig, IPrimitiveType<Integer> theOffset, IPrimitiveType<Integer> theCount, IPrimitiveType<Boolean> theIncludeHierarchy, IPrimitiveType<String> theFilter) { 269 int offset = theDaoConfig.getPreExpandValueSetsDefaultOffset(); 270 if (theOffset != null && theOffset.hasValue()) { 271 if (theOffset.getValue() >= 0) { 272 offset = theOffset.getValue(); 273 } else { 274 throw new InvalidRequestException(Msg.code(1135) + "offset parameter for $expand operation must be >= 0 when specified. offset: " + theOffset.getValue()); 275 } 276 } 277 278 int count = theDaoConfig.getPreExpandValueSetsDefaultCount(); 279 if (theCount != null && theCount.hasValue()) { 280 if (theCount.getValue() >= 0) { 281 count = theCount.getValue(); 282 } else { 283 throw new InvalidRequestException(Msg.code(1136) + "count parameter for $expand operation must be >= 0 when specified. count: " + theCount.getValue()); 284 } 285 } 286 int countMax = theDaoConfig.getPreExpandValueSetsMaxCount(); 287 if (count > countMax) { 288 ourLog.warn("count parameter for $expand operation of {} exceeds maximum value of {}; using maximum value.", count, countMax); 289 count = countMax; 290 } 291 292 ValueSetExpansionOptions options = ValueSetExpansionOptions.forOffsetAndCount(offset, count); 293 294 if (theIncludeHierarchy != null && Boolean.TRUE.equals(theIncludeHierarchy.getValue())) { 295 options.setIncludeHierarchy(true); 296 } 297 298 if (theFilter != null) { 299 options.setFilter(theFilter.getValue()); 300 } 301 302 return options; 303 } 304 305 private static boolean moreThanOneTrue(boolean... theBooleans) { 306 boolean haveOne = false; 307 for (boolean next : theBooleans) { 308 if (next) { 309 if (haveOne) { 310 return true; 311 } else { 312 haveOne = true; 313 } 314 } 315 } 316 return false; 317 } 318} 319