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.dao; 021 022import ca.uhn.fhir.context.FhirVersionEnum; 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.dao.IFhirResourceDaoValueSet; 029import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 030import ca.uhn.fhir.jpa.model.entity.ResourceTable; 031import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions; 032import ca.uhn.fhir.jpa.util.LogicUtil; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 037import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 038import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 039import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; 040import org.hl7.fhir.instance.model.api.IBaseCoding; 041import org.hl7.fhir.instance.model.api.IBaseDatatype; 042import org.hl7.fhir.instance.model.api.IBaseResource; 043import org.hl7.fhir.instance.model.api.IIdType; 044import org.hl7.fhir.instance.model.api.IPrimitiveType; 045import org.hl7.fhir.r4.model.CodeableConcept; 046import org.hl7.fhir.r4.model.Coding; 047import org.hl7.fhir.r4.model.ValueSet; 048import org.springframework.beans.factory.annotation.Autowired; 049 050import java.util.Date; 051 052import static ca.uhn.fhir.jpa.dao.JpaResourceDaoCodeSystem.createVersionedSystemIfVersionIsPresent; 053import static ca.uhn.fhir.jpa.provider.ValueSetOperationProvider.createValueSetExpansionOptions; 054import static ca.uhn.fhir.util.DatatypeUtil.toStringValue; 055import static org.apache.commons.lang3.StringUtils.isNotBlank; 056 057public class JpaResourceDaoValueSet<T extends IBaseResource> extends BaseHapiFhirResourceDao<T> 058 implements IFhirResourceDaoValueSet<T> { 059 @Autowired 060 private IValidationSupport myValidationSupport; 061 062 @Autowired 063 private VersionCanonicalizer myVersionCanonicalizer; 064 065 @Autowired(required = false) 066 private IFulltextSearchSvc myFulltextSearch; 067 068 @Override 069 public T expand(IIdType theId, ValueSetExpansionOptions theOptions, RequestDetails theRequestDetails) { 070 T source = read(theId, theRequestDetails); 071 return expand(source, theOptions); 072 } 073 074 @SuppressWarnings("unchecked") 075 @Override 076 public T expandByIdentifier(String theUri, ValueSetExpansionOptions theOptions) { 077 IValidationSupport.ValueSetExpansionOutcome expansionOutcome = myValidationSupport.expandValueSet( 078 new ValidationSupportContext(myValidationSupport), theOptions, theUri); 079 return extractValueSetOrThrowException(expansionOutcome); 080 } 081 082 @Override 083 public T expand(T theSource, ValueSetExpansionOptions theOptions) { 084 IValidationSupport.ValueSetExpansionOutcome expansionOutcome = myValidationSupport.expandValueSet( 085 new ValidationSupportContext(myValidationSupport), theOptions, theSource); 086 return extractValueSetOrThrowException(expansionOutcome); 087 } 088 089 @Override 090 public T expand( 091 IIdType theId, 092 T theValueSet, 093 IPrimitiveType<String> theUrl, 094 IPrimitiveType<String> theValueSetVersion, 095 IPrimitiveType<String> theFilter, 096 IPrimitiveType<String> theContext, 097 IPrimitiveType<String> theContextDirection, 098 IPrimitiveType<Integer> theOffset, 099 IPrimitiveType<Integer> theCount, 100 IPrimitiveType<String> theDisplayLanguage, 101 IPrimitiveType<Boolean> theIncludeHierarchy, 102 RequestDetails theRequestDetails) { 103 boolean haveId = theId != null && theId.hasIdPart(); 104 boolean haveIdentifier = theUrl != null && isNotBlank(theUrl.getValue()); 105 boolean haveValueSet = theValueSet != null && !theValueSet.isEmpty(); 106 boolean haveValueSetVersion = theValueSetVersion != null && !theValueSetVersion.isEmpty(); 107 boolean haveContextDirection = theContextDirection != null && !theContextDirection.isEmpty(); 108 boolean haveContext = theContext != null && !theContext.isEmpty(); 109 110 boolean isAutocompleteExtension = 111 haveContext && haveContextDirection && "existing".equals(theContextDirection.getValue()); 112 113 if (isAutocompleteExtension) { 114 // this is a funky extension for NIH. Do our own thing and return. 115 ValueSetAutocompleteOptions options = ValueSetAutocompleteOptions.validateAndParseOptions( 116 myStorageSettings, theContext, theFilter, theCount, theId, theUrl, theValueSet); 117 if (myFulltextSearch == null || myFulltextSearch.isDisabled()) { 118 throw new InvalidRequestException( 119 Msg.code(2083) 120 + " Autocomplete is not supported on this server, as the fulltext search service is not configured."); 121 } else { 122 return (T) myFulltextSearch.tokenAutocompleteValueSetSearch(options); 123 } 124 } 125 126 if (!haveId && !haveIdentifier && !haveValueSet) { 127 if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { 128 // "url" parameter is called "identifier" in DSTU2 129 throw new InvalidRequestException( 130 Msg.code(1130) 131 + "$expand operation at the type level (no ID specified) requires an identifier or a valueSet as a part of the request"); 132 } 133 throw new InvalidRequestException( 134 Msg.code(1133) 135 + "$expand operation at the type level (no ID specified) requires a url or a valueSet as a part of the request."); 136 } 137 138 if (!LogicUtil.multiXor(haveId, haveIdentifier, haveValueSet)) { 139 if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { 140 // "url" parameter is called "identifier" in DSTU2 141 throw new InvalidRequestException( 142 Msg.code(1131) 143 + "$expand must EITHER be invoked at the type level, or have an identifier specified, or have a ValueSet specified. Can not combine these options."); 144 } 145 throw new InvalidRequestException( 146 Msg.code(1134) 147 + "$expand must EITHER be invoked at the instance level, or have a url specified, or have a ValueSet specified. Can not combine these options."); 148 } 149 150 ValueSetExpansionOptions options = createValueSetExpansionOptions( 151 myStorageSettings, theOffset, theCount, theIncludeHierarchy, theFilter, theDisplayLanguage); 152 153 IValidationSupport.ValueSetExpansionOutcome outcome; 154 if (haveId) { 155 IBaseResource valueSet = read(theId, theRequestDetails); 156 outcome = myValidationSupport.expandValueSet( 157 new ValidationSupportContext(myValidationSupport), options, valueSet); 158 } else if (haveIdentifier) { 159 String url; 160 if (haveValueSetVersion) { 161 url = theUrl.getValue() + "|" + theValueSetVersion.getValue(); 162 } else { 163 url = theUrl.getValue(); 164 } 165 outcome = 166 myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, url); 167 } else { 168 outcome = myValidationSupport.expandValueSet( 169 new ValidationSupportContext(myValidationSupport), options, theValueSet); 170 } 171 172 return extractValueSetOrThrowException(outcome); 173 } 174 175 @SuppressWarnings("unchecked") 176 private T extractValueSetOrThrowException(IValidationSupport.ValueSetExpansionOutcome outcome) { 177 if (outcome == null) { 178 throw new InternalErrorException( 179 Msg.code(2028) + "No validation support module was able to expand the given valueset"); 180 } 181 182 if (outcome.getError() != null) { 183 throw new PreconditionFailedException(Msg.code(2029) + outcome.getError()); 184 } 185 186 return (T) outcome.getValueSet(); 187 } 188 189 @Override 190 public IValidationSupport.CodeValidationResult validateCode( 191 IPrimitiveType<String> theValueSetIdentifier, 192 IIdType theValueSetId, 193 IPrimitiveType<String> theCode, 194 IPrimitiveType<String> theSystem, 195 IPrimitiveType<String> theDisplay, 196 IBaseCoding theCoding, 197 IBaseDatatype theCodeableConcept, 198 RequestDetails theRequestDetails) { 199 200 CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept); 201 boolean haveCodeableConcept = 202 codeableConcept != null && codeableConcept.getCoding().size() > 0; 203 204 Coding canonicalCodingToValidate = myVersionCanonicalizer.codingToCanonical((IBaseCoding) theCoding); 205 boolean haveCoding = canonicalCodingToValidate != null && !canonicalCodingToValidate.isEmpty(); 206 207 boolean haveCode = theCode != null && !theCode.isEmpty(); 208 209 if (!haveCodeableConcept && !haveCoding && !haveCode) { 210 throw new InvalidRequestException( 211 Msg.code(899) + "No code, coding, or codeableConcept provided to validate"); 212 } 213 if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { 214 throw new InvalidRequestException(Msg.code(900) 215 + "$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); 216 } 217 218 String valueSetIdentifier; 219 if (theValueSetId != null) { 220 IBaseResource valueSet = read(theValueSetId, theRequestDetails); 221 StringBuilder valueSetIdentifierBuilder = 222 new StringBuilder(CommonCodeSystemsTerminologyService.getValueSetUrl(myFhirContext, valueSet)); 223 String valueSetVersion = CommonCodeSystemsTerminologyService.getValueSetVersion(myFhirContext, valueSet); 224 if (valueSetVersion != null) { 225 valueSetIdentifierBuilder.append("|").append(valueSetVersion); 226 } 227 valueSetIdentifier = valueSetIdentifierBuilder.toString(); 228 } else if (isNotBlank(toStringValue(theValueSetIdentifier))) { 229 valueSetIdentifier = toStringValue(theValueSetIdentifier); 230 } else { 231 throw new InvalidRequestException( 232 Msg.code(901) 233 + "Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); 234 } 235 236 if (haveCodeableConcept) { 237 IValidationSupport.CodeValidationResult anyValidation = null; 238 for (int i = 0; i < codeableConcept.getCoding().size(); i++) { 239 Coding nextCoding = codeableConcept.getCoding().get(i); 240 String system = 241 createVersionedSystemIfVersionIsPresent(nextCoding.getSystem(), nextCoding.getVersion()); 242 String code = nextCoding.getCode(); 243 String display = nextCoding.getDisplay(); 244 245 IValidationSupport.CodeValidationResult nextValidation = 246 validateCode(system, code, display, valueSetIdentifier); 247 anyValidation = nextValidation; 248 if (nextValidation.isOk()) { 249 return nextValidation; 250 } 251 } 252 return anyValidation; 253 } else if (haveCoding) { 254 String system = createVersionedSystemIfVersionIsPresent( 255 canonicalCodingToValidate.getSystem(), canonicalCodingToValidate.getVersion()); 256 String code = canonicalCodingToValidate.getCode(); 257 String display = canonicalCodingToValidate.getDisplay(); 258 return validateCode(system, code, display, valueSetIdentifier); 259 } else { 260 String system = toStringValue(theSystem); 261 String code = toStringValue(theCode); 262 String display = toStringValue(theDisplay); 263 return validateCode(system, code, display, valueSetIdentifier); 264 } 265 } 266 267 private IValidationSupport.CodeValidationResult validateCode( 268 String theSystem, String theCode, String theDisplay, String theValueSetIdentifier) { 269 ValidationSupportContext context = new ValidationSupportContext(myValidationSupport); 270 ConceptValidationOptions options = new ConceptValidationOptions(); 271 options.setValidateDisplay(isNotBlank(theDisplay)); 272 IValidationSupport.CodeValidationResult result = myValidationSupport.validateCode( 273 context, options, theSystem, theCode, theDisplay, theValueSetIdentifier); 274 275 if (result == null) { 276 result = new IValidationSupport.CodeValidationResult(); 277 result.setMessage("Validator is unable to provide validation for " + theCode + "#" + theSystem 278 + " - Unknown or unusable ValueSet[" + theValueSetIdentifier + "]"); 279 } 280 281 return result; 282 } 283 284 @Override 285 public ResourceTable updateEntity( 286 RequestDetails theRequestDetails, 287 IBaseResource theResource, 288 IBasePersistedResource theEntity, 289 Date theDeletedTimestampOrNull, 290 boolean thePerformIndexing, 291 boolean theUpdateVersion, 292 TransactionDetails theTransactionDetails, 293 boolean theForceUpdate, 294 boolean theCreateNewHistoryEntry) { 295 ResourceTable retVal = super.updateEntity( 296 theRequestDetails, 297 theResource, 298 theEntity, 299 theDeletedTimestampOrNull, 300 thePerformIndexing, 301 theUpdateVersion, 302 theTransactionDetails, 303 theForceUpdate, 304 theCreateNewHistoryEntry); 305 306 if (thePerformIndexing) { 307 if (getStorageSettings().isPreExpandValueSets() && !retVal.isUnchangedInCurrentOperation()) { 308 if (retVal.getDeleted() == null) { 309 ValueSet valueSet = myVersionCanonicalizer.valueSetToCanonical(theResource); 310 myTerminologySvc.storeTermValueSet(retVal, valueSet); 311 } else { 312 myTerminologySvc.deleteValueSetAndChildren(retVal); 313 } 314 } 315 } 316 317 return retVal; 318 } 319}